@ -1,13 +1,10 @@
/* eslint-disable no-console */
import assert from 'assert' ;
import { expect } from 'chai' ;
import { Signer } from 'ethers' ;
import hre from 'hardhat' ;
import {
Address ,
configDeepEquals ,
normalizeConfig ,
stringifyObject ,
} from '@hyperlane-xyz/utils' ;
import { Address , eqAddress , normalizeConfig } from '@hyperlane-xyz/utils' ;
import { TestChainName , testChains } from '../consts/testChains.js' ;
import { HyperlaneAddresses , HyperlaneContracts } from '../contracts/types.js' ;
@ -27,7 +24,7 @@ import {
HookConfig ,
HookType ,
IgpHookConfig ,
MerkleTreeHookConfig ,
MUTABLE_HOOK_TYPE ,
PausableHookConfig ,
ProtocolFeeHookConfig ,
} from './types.js' ;
@ -156,15 +153,18 @@ function randomHookConfig(
}
describe ( 'EvmHookModule' , async ( ) = > {
const chain = TestChainName . test4 ;
let multiProvider : MultiProvider ;
let coreAddresses : CoreAddresses ;
const chain = TestChainName . test4 ;
let signer : Signer ;
let funder : Signer ;
let proxyFactoryAddresses : HyperlaneAddresses < ProxyFactoryFactories > ;
let factoryContracts : HyperlaneContracts < ProxyFactoryFactories > ;
let exampleRoutingConfig : DomainRoutingHookConfig | FallbackRoutingHookConfig ;
beforeEach ( async ( ) = > {
const [ sign er] = await hre . ethers . getSigners ( ) ;
[ signer , fund er] = await hre . ethers . getSigners ( ) ;
multiProvider = MultiProvider . createTestMultiProvider ( { signer } ) ;
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer ( multiProvider ) ;
@ -202,27 +202,47 @@ describe('EvmHookModule', async () => {
proxyAdmin : proxyAdmin.address ,
validatorAnnounce : validatorAnnounce.address ,
} ;
// reusable for routing/fallback routing specific tests
exampleRoutingConfig = {
owner : ( await multiProvider . getSignerAddress ( chain ) ) . toLowerCase ( ) ,
domains : Object.fromEntries (
testChains . map ( ( c ) = > [
c ,
{
type : HookType . MERKLE_TREE ,
} ,
] ) ,
) ,
type : HookType . FALLBACK_ROUTING ,
fallback : { type : HookType . MERKLE_TREE } ,
} ;
} ) ;
// Helper method for checking whether Hook module matches a given config
async function hookModuleMatchesConfig ( {
hook ,
config ,
} : {
hook : EvmHookModule ;
config : HookConfig ;
} ) : Promise < boolean > {
const normalizedDerivedConfig = normalizeConfig ( await hook . read ( ) ) ;
const normalizedConfig = normalizeConfig ( config ) ;
const matches = configDeepEquals ( normalizedDerivedConfig , normalizedConfig ) ;
if ( ! matches ) {
console . error (
'Derived config:\n' ,
stringifyObject ( normalizedDerivedConfig ) ,
) ;
console . error ( 'Expected config:\n' , stringifyObject ( normalizedConfig ) ) ;
// Helper method for create a new multiprovider with an impersonated account
async function impersonateAccount ( account : Address ) : Promise < MultiProvider > {
await hre . ethers . provider . send ( 'hardhat_impersonateAccount' , [ account ] ) ;
await funder . sendTransaction ( {
to : account ,
value : hre.ethers.utils.parseEther ( '1.0' ) ,
} ) ;
return MultiProvider . createTestMultiProvider ( {
signer : hre.ethers.provider.getSigner ( account ) ,
} ) ;
}
// Helper method to expect exactly N updates to be applied
async function expectTxsAndUpdate (
hook : EvmHookModule ,
config : HookConfig ,
n : number ,
) {
const txs = await hook . update ( config ) ;
expect ( txs . length ) . to . equal ( n ) ;
for ( const tx of txs ) {
await multiProvider . sendTransaction ( chain , tx ) ;
}
return matches ;
}
// hook module and config for testing
@ -231,9 +251,9 @@ describe('EvmHookModule', async () => {
// expect that the hook matches the config after all tests
afterEach ( async ( ) = > {
expect (
await hookModuleMatchesConfig ( { hook : testHook , config : testConfig } ) ,
) . to . be . true ;
const normalizedDerivedConfig = normalizeConfig ( await testHook . read ( ) ) ;
const normalizedConfig = normalizeConfig ( testConfig ) ;
assert . deepStrictEqual ( normalizedDerivedConfig , normalizedConfig ) ;
} ) ;
// create a new Hook and verify that it matches the config
@ -247,124 +267,45 @@ describe('EvmHookModule', async () => {
coreAddresses ,
multiProvider ,
} ) ;
testConfig = config ;
testHook = hook ;
testConfig = config ;
return { hook , initialHookAddress : hook.serialize ( ) . deployedHook } ;
}
describe ( 'create' , async ( ) = > {
// generate a random config for each hook type
const exampleHookConfigs : HookConfig [ ] = [
// include an address config
randomAddress ( ) ,
. . . hookTypes
// need to setup deploying/mocking IL1CrossDomainMessenger before this test can be enabled
. filter (
( hookType ) = >
hookType !== HookType . OP_STACK && hookType !== HookType . CUSTOM ,
)
// generate a random config for each hook type
. map ( ( hookType ) = > {
return randomHookConfig ( 0 , 1 , hookType ) ;
} ) ,
] ;
// test deployment of each hookType, except OP_STACK and CUSTOM
// minimum depth only
for ( const config of exampleHookConfigs ) {
it ( ` deploys a hook of type ${
typeof config === 'string' ? 'address' : config . type
} ` , async () => {
await createHook ( config ) ;
} ) ;
}
// manually include test for CUSTOM hook type
it ( 'deploys a hook of type CUSTOM' , async ( ) = > {
const config : HookConfig = randomAddress ( ) ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type MERKLE_TREE' , async ( ) = > {
const config : MerkleTreeHookConfig = {
type : HookType . MERKLE_TREE ,
} ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type INTERCHAIN_GAS_PAYMASTER' , async ( ) = > {
const owner = randomAddress ( ) ;
const config : IgpHookConfig = {
owner ,
type : HookType . INTERCHAIN_GAS_PAYMASTER ,
beneficiary : randomAddress ( ) ,
oracleKey : owner ,
overhead : Object.fromEntries (
testChains . map ( ( c ) = > [ c , Math . floor ( Math . random ( ) * 100 ) ] ) ,
) ,
oracleConfig : Object.fromEntries (
testChains . map ( ( c ) = > [
c ,
{
tokenExchangeRate : randomInt ( 1234567891234 ) . toString ( ) ,
gasPrice : randomInt ( 1234567891234 ) . toString ( ) ,
} ,
] ) ,
) ,
} ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type PROTOCOL_FEE' , async ( ) = > {
const { maxProtocolFee , protocolFee } = randomProtocolFee ( ) ;
const config : ProtocolFeeHookConfig = {
owner : randomAddress ( ) ,
type : HookType . PROTOCOL_FEE ,
maxProtocolFee ,
protocolFee ,
beneficiary : randomAddress ( ) ,
} ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type ROUTING' , async ( ) = > {
const config : DomainRoutingHookConfig = {
owner : randomAddress ( ) ,
type : HookType . ROUTING ,
domains : Object.fromEntries (
testChains
. filter ( ( c ) = > c !== TestChainName . test4 )
. map ( ( c ) = > [
c ,
{
type : HookType . MERKLE_TREE ,
} ,
] ) ,
) ,
} ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type FALLBACK_ROUTING' , async ( ) = > {
const config : FallbackRoutingHookConfig = {
owner : randomAddress ( ) ,
type : HookType . FALLBACK_ROUTING ,
fallback : { type : HookType . MERKLE_TREE } ,
domains : Object.fromEntries (
testChains
. filter ( ( c ) = > c !== TestChainName . test4 )
. map ( ( c ) = > [
c ,
{
type : HookType . MERKLE_TREE ,
} ,
] ) ,
) ,
} ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type AGGREGATION' , async ( ) = > {
const config : AggregationHookConfig = {
type : HookType . AGGREGATION ,
hooks : [ { type : HookType . MERKLE_TREE } , { type : HookType . MERKLE_TREE } ] ,
} ;
await createHook ( config ) ;
} ) ;
it ( 'deploys a hook of type PAUSABLE' , async ( ) = > {
const config : PausableHookConfig = {
owner : randomAddress ( ) ,
type : HookType . PAUSABLE ,
paused : false ,
} ;
await createHook ( config ) ;
} ) ;
// it('deploys a hook of type OP_STACK', async () => {
// need to setup deploying/mocking IL1CrossDomainMessenger before this test can be enabled
// const config: OpStackHookConfig = {
// owner: randomAddress(),
// type: HookType.OP_STACK,
// nativeBridge: randomAddress(),
// destinationChain: 'testChain',
// };
// await createHook(config);
// });
// random configs upto depth 2
for ( let i = 0 ; i < 16 ; i ++ ) {
it ( ` deploys a random hook config # ${ i } ` , async ( ) = > {
// random config with depth 0-2
@ -373,6 +314,7 @@ describe('EvmHookModule', async () => {
} ) ;
}
// manual test to catch regressions on a complex config type
it ( 'regression test #1' , async ( ) = > {
const config : HookConfig = {
type : HookType . AGGREGATION ,
@ -484,4 +426,371 @@ describe('EvmHookModule', async () => {
await createHook ( config ) ;
} ) ;
} ) ;
describe ( 'update' , async ( ) = > {
it ( 'should update by deploying a new aggregation hook' , async ( ) = > {
const config : AggregationHookConfig = {
type : HookType . AGGREGATION ,
hooks : [ randomHookConfig ( 0 , 2 ) , randomHookConfig ( 0 , 2 ) ] ,
} ;
// create a new hook
const { hook , initialHookAddress } = await createHook ( config ) ;
// change the hooks
config . hooks = [ randomHookConfig ( 0 , 2 ) , randomHookConfig ( 0 , 2 ) ] ;
// expect 0 tx to be returned, as it should deploy a new aggregation hook
await expectTxsAndUpdate ( hook , config , 0 ) ;
// expect the hook address to be different
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to . be
. false ;
} ) ;
const createDeployerOwnedIgpHookConfig =
async ( ) : Promise < IgpHookConfig > = > {
const owner = await multiProvider . getSignerAddress ( chain ) ;
return {
owner ,
type : HookType . INTERCHAIN_GAS_PAYMASTER ,
beneficiary : randomAddress ( ) ,
oracleKey : owner ,
overhead : Object.fromEntries (
testChains . map ( ( c ) = > [ c , Math . floor ( Math . random ( ) * 100 ) ] ) ,
) ,
oracleConfig : Object.fromEntries (
testChains . map ( ( c ) = > [
c ,
{
tokenExchangeRate : randomInt ( 1234567891234 ) . toString ( ) ,
gasPrice : randomInt ( 1234567891234 ) . toString ( ) ,
} ,
] ) ,
) ,
} ;
} ;
it ( 'should update beneficiary in IGP' , async ( ) = > {
const config = await createDeployerOwnedIgpHookConfig ( ) ;
// create a new hook
const { hook } = await createHook ( config ) ;
// change the beneficiary
config . beneficiary = randomAddress ( ) ;
// expect 1 tx to update the beneficiary
await expectTxsAndUpdate ( hook , config , 1 ) ;
} ) ;
it ( 'should update the overheads in IGP' , async ( ) = > {
const config = await createDeployerOwnedIgpHookConfig ( ) ;
// create a new hook
const { hook } = await createHook ( config ) ;
// change the overheads
config . overhead = Object . fromEntries (
testChains . map ( ( c ) = > [ c , Math . floor ( Math . random ( ) * 100 ) ] ) ,
) ;
// expect 1 tx to update the overheads
await expectTxsAndUpdate ( hook , config , 1 ) ;
} ) ;
it ( 'should update the oracle config in IGP' , async ( ) = > {
const config = await createDeployerOwnedIgpHookConfig ( ) ;
// create a new hook
const { hook } = await createHook ( config ) ;
// change the oracle config
config . oracleConfig = Object . fromEntries (
testChains . map ( ( c ) = > [
c ,
{
tokenExchangeRate : randomInt ( 987654321 ) . toString ( ) ,
gasPrice : randomInt ( 987654321 ) . toString ( ) ,
} ,
] ) ,
) ;
// expect 1 tx to update the oracle config
await expectTxsAndUpdate ( hook , config , 1 ) ;
} ) ;
it ( 'should update protocol fee in protocol fee hook' , async ( ) = > {
const config : ProtocolFeeHookConfig = {
owner : await multiProvider . getSignerAddress ( chain ) ,
type : HookType . PROTOCOL_FEE ,
maxProtocolFee : '1000' ,
protocolFee : '100' ,
beneficiary : randomAddress ( ) ,
} ;
// create a new hook
const { hook } = await createHook ( config ) ;
// change the protocol fee
config . protocolFee = '200' ;
// expect 1 tx to update the protocol fee
await expectTxsAndUpdate ( hook , config , 1 ) ;
} ) ;
it ( 'should update max fee in protocol fee hook' , async ( ) = > {
const config : ProtocolFeeHookConfig = {
owner : await multiProvider . getSignerAddress ( chain ) ,
type : HookType . PROTOCOL_FEE ,
maxProtocolFee : '1000' ,
protocolFee : '100' ,
beneficiary : randomAddress ( ) ,
} ;
// create a new hook
const { hook , initialHookAddress } = await createHook ( config ) ;
// change the protocol fee
config . maxProtocolFee = '2000' ;
// expect 0 tx to update the max protocol fee as it has to deploy a new hook
await expectTxsAndUpdate ( hook , config , 0 ) ;
// expect the hook address to be different
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to . be
. false ;
} ) ;
it ( 'should update paused state of pausable hook' , async ( ) = > {
const config : PausableHookConfig = {
owner : randomAddress ( ) ,
type : HookType . PAUSABLE ,
paused : false ,
} ;
// create a new hook
const { hook } = await createHook ( config ) ;
// change the paused state
config . paused = true ;
// impersonate the hook owner
multiProvider = await impersonateAccount ( config . owner ) ;
// expect 1 tx to update the paused state
await expectTxsAndUpdate ( hook , config , 1 ) ;
} ) ;
for ( const type of [ HookType . ROUTING , HookType . FALLBACK_ROUTING ] ) {
beforeEach ( ( ) = > {
exampleRoutingConfig . type = type as
| HookType . ROUTING
| HookType . FALLBACK_ROUTING ;
} ) ;
it ( ` should skip deployment with warning if no chain metadata configured ${ type } ` , async ( ) = > {
// create a new hook
const { hook } = await createHook ( exampleRoutingConfig ) ;
// add config for a domain the multiprovider doesn't have
const updatedConfig : HookConfig = {
. . . exampleRoutingConfig ,
domains : {
. . . exampleRoutingConfig . domains ,
test5 : { type : HookType . MERKLE_TREE } ,
} ,
} ;
// expect 0 txs, as adding test5 domain is no-op
await expectTxsAndUpdate ( hook , updatedConfig , 0 ) ;
} ) ;
it ( ` no changes to an existing ${ type } means no redeployment or updates ` , async ( ) = > {
// create a new hook
const { hook , initialHookAddress } = await createHook (
exampleRoutingConfig ,
) ;
// expect 0 updates
await expectTxsAndUpdate ( hook , exampleRoutingConfig , 0 ) ;
// expect the hook address to be the same
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to
. be . true ;
} ) ;
it ( ` updates an existing ${ type } with new domains ` , async ( ) = > {
exampleRoutingConfig = {
owner : ( await multiProvider . getSignerAddress ( chain ) ) . toLowerCase ( ) ,
domains : {
test1 : {
type : HookType . MERKLE_TREE ,
} ,
} ,
type : HookType . FALLBACK_ROUTING ,
fallback : { type : HookType . MERKLE_TREE } ,
} ;
// create a new hook
const { hook , initialHookAddress } = await createHook (
exampleRoutingConfig ,
) ;
// add a new domain
exampleRoutingConfig . domains [ TestChainName . test2 ] = {
type : HookType . MERKLE_TREE ,
} ;
// expect 1 tx to update the domains
await expectTxsAndUpdate ( hook , exampleRoutingConfig , 1 ) ;
// expect the hook address to be the same
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to
. be . true ;
} ) ;
it ( ` updates an existing ${ type } with new domains ` , async ( ) = > {
exampleRoutingConfig = {
owner : ( await multiProvider . getSignerAddress ( chain ) ) . toLowerCase ( ) ,
domains : {
test1 : {
type : HookType . MERKLE_TREE ,
} ,
} ,
type : HookType . FALLBACK_ROUTING ,
fallback : { type : HookType . MERKLE_TREE } ,
} ;
// create a new hook
const { hook , initialHookAddress } = await createHook (
exampleRoutingConfig ,
) ;
// add multiple new domains
exampleRoutingConfig . domains [ TestChainName . test2 ] = {
type : HookType . MERKLE_TREE ,
} ;
exampleRoutingConfig . domains [ TestChainName . test3 ] = {
type : HookType . MERKLE_TREE ,
} ;
exampleRoutingConfig . domains [ TestChainName . test4 ] = {
type : HookType . MERKLE_TREE ,
} ;
// expect 1 tx to update the domains
await expectTxsAndUpdate ( hook , exampleRoutingConfig , 1 ) ;
// expect the hook address to be the same
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to
. be . true ;
} ) ;
}
it ( ` update fallback in an existing fallback routing hook ` , async ( ) = > {
// create a new hook
const config = exampleRoutingConfig as FallbackRoutingHookConfig ;
const { hook , initialHookAddress } = await createHook ( config ) ;
// change the fallback
config . fallback = {
type : HookType . PROTOCOL_FEE ,
owner : randomAddress ( ) ,
maxProtocolFee : '9000' ,
protocolFee : '350' ,
beneficiary : randomAddress ( ) ,
} ;
// expect 0 tx as it will have to deploy a new fallback routing hook
await expectTxsAndUpdate ( hook , config , 0 ) ;
// expect the hook address to be different
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to . be
. false ;
} ) ;
it ( ` update fallback in an existing fallback routing hook with no change ` , async ( ) = > {
// create a new hook
const config = exampleRoutingConfig as FallbackRoutingHookConfig ;
const { hook } = await createHook ( config ) ;
// expect 0 updates
await expectTxsAndUpdate ( hook , config , 0 ) ;
} ) ;
// generate a random config for each ownable hook type
const ownableHooks = hookTypes
. filter ( ( hookType ) = > MUTABLE_HOOK_TYPE . includes ( hookType ) )
. map ( ( hookType ) = > {
return randomHookConfig ( 0 , 1 , hookType ) ;
} ) ;
for ( const config of ownableHooks ) {
assert (
typeof config !== 'string' ,
'Address is not an ownable hook config' ,
) ;
assert (
'owner' in config ,
'Ownable hook config must have an owner property' ,
) ;
it ( ` updates owner in an existing ${ config . type } ` , async ( ) = > {
// hook owned by the deployer
config . owner = await multiProvider . getSignerAddress ( chain ) ;
// create a new hook
const { hook , initialHookAddress } = await createHook ( config ) ;
// change the config owner
config . owner = randomAddress ( ) ;
// expect 1 tx to transfer ownership
await expectTxsAndUpdate ( hook , config , 1 ) ;
// expect the hook address to be the same
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to
. be . true ;
} ) ;
it ( ` update owner in an existing ${ config . type } not owned by deployer ` , async ( ) = > {
// hook owner is not the deployer
config . owner = randomAddress ( ) ;
const originalOwner = config . owner ;
// create a new hook
const { hook , initialHookAddress } = await createHook ( config ) ;
// update the config owner and impersonate the original owner
config . owner = randomAddress ( ) ;
multiProvider = await impersonateAccount ( originalOwner ) ;
// expect 1 tx to transfer ownership
await expectTxsAndUpdate ( hook , config , 1 ) ;
// expect the hook address to be unchanged
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to
. be . true ;
} ) ;
it ( ` update owner in an existing ${ config . type } not owned by deployer and no change ` , async ( ) = > {
// hook owner is not the deployer
config . owner = randomAddress ( ) ;
const originalOwner = config . owner ;
// create a new hook
const { hook , initialHookAddress } = await createHook ( config ) ;
// impersonate the original owner
multiProvider = await impersonateAccount ( originalOwner ) ;
// expect 0 updates
await expectTxsAndUpdate ( hook , config , 0 ) ;
// expect the hook address to be unchanged
expect ( eqAddress ( initialHookAddress , hook . serialize ( ) . deployedHook ) ) . to
. be . true ;
} ) ;
}
} ) ;
} ) ;