feat: Add hook update into WarpModule.update() (#4891)

### Description
This is a multi-part PR.

This adds `createHookUpdateTxs()` to `WarpModule.update()` such that it
updates a warp route _without_ a hook, and one with an existing hook.

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility
Yes

### Testing
Unit Tests
pull/4892/merge
Lee 2 days ago committed by GitHub
parent 95f1a97e78
commit 170a0fc739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/nice-oranges-glow.md
  2. 54
      typescript/cli/src/deploy/warp.ts
  3. 42
      typescript/cli/src/tests/warp-apply.e2e-test.ts
  4. 4
      typescript/sdk/src/hook/EvmHookModule.ts
  5. 148
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  6. 190
      typescript/sdk/src/token/EvmERC20WarpModule.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Add `createHookUpdateTxs()` to `WarpModule.update()` such that it 1) deploys a hook for a warp route _without_ an existing hook, or 2) update an existing hook.

@ -3,7 +3,7 @@ import { groupBy } from 'lodash-es';
import { stringify as yamlStringify } from 'yaml'; import { stringify as yamlStringify } from 'yaml';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { IRegistry } from '@hyperlane-xyz/registry'; import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry';
import { import {
AggregationIsmConfig, AggregationIsmConfig,
AnnotatedEV5Transaction, AnnotatedEV5Transaction,
@ -30,7 +30,6 @@ import {
MultisigIsmConfig, MultisigIsmConfig,
OpStackIsmConfig, OpStackIsmConfig,
PausableIsmConfig, PausableIsmConfig,
ProxyFactoryFactoriesAddresses,
RemoteRouters, RemoteRouters,
RoutingIsmConfig, RoutingIsmConfig,
SubmissionStrategy, SubmissionStrategy,
@ -519,7 +518,6 @@ async function extendWarpRoute(
const newDeployedContracts = await executeDeploy( const newDeployedContracts = await executeDeploy(
{ {
// TODO: use EvmERC20WarpModule when it's ready
context: params.context, context: params.context,
warpDeployConfig: extendedConfigs, warpDeployConfig: extendedConfigs,
}, },
@ -549,7 +547,7 @@ async function updateExistingWarpRoute(
logBlue('Updating deployed Warp Routes'); logBlue('Updating deployed Warp Routes');
const { multiProvider, registry } = params.context; const { multiProvider, registry } = params.context;
const registryAddresses = const registryAddresses =
(await registry.getAddresses()) as ChainMap<ProxyFactoryFactoriesAddresses>; (await registry.getAddresses()) as ChainMap<ChainAddresses>;
const contractVerifier = new ContractVerifier( const contractVerifier = new ContractVerifier(
multiProvider, multiProvider,
apiKeys, apiKeys,
@ -568,14 +566,31 @@ async function updateExistingWarpRoute(
`Missing artifacts for ${chain}. Probably new deployment. Skipping update...`, `Missing artifacts for ${chain}. Probably new deployment. Skipping update...`,
); );
const deployedTokenRoute = deployedConfig.addressOrDenom!;
const {
domainRoutingIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
} = registryAddresses[chain];
const evmERC20WarpModule = new EvmERC20WarpModule( const evmERC20WarpModule = new EvmERC20WarpModule(
multiProvider, multiProvider,
{ {
config, config,
chain, chain,
addresses: { addresses: {
...registryAddresses[chain], deployedTokenRoute,
deployedTokenRoute: deployedConfig.addressOrDenom!, staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
}, },
}, },
contractVerifier, contractVerifier,
@ -660,8 +675,7 @@ async function enrollRemoteRouters(
): Promise<AnnotatedEV5Transaction[]> { ): Promise<AnnotatedEV5Transaction[]> {
logBlue(`Enrolling deployed routers with each other...`); logBlue(`Enrolling deployed routers with each other...`);
const { multiProvider, registry } = params.context; const { multiProvider, registry } = params.context;
const registryAddresses = const registryAddresses = await registry.getAddresses();
(await registry.getAddresses()) as ChainMap<ProxyFactoryFactoriesAddresses>;
const deployedRoutersAddresses: ChainMap<Address> = objMap( const deployedRoutersAddresses: ChainMap<Address> = objMap(
deployedContractsMap, deployedContractsMap,
(_, contracts) => getRouter(contracts).address, (_, contracts) => getRouter(contracts).address,
@ -678,20 +692,36 @@ async function enrollRemoteRouters(
objMap(deployedContractsMap, async (chain, contracts) => { objMap(deployedContractsMap, async (chain, contracts) => {
await retryAsync(async () => { await retryAsync(async () => {
const router = getRouter(contracts); // Assume deployedContract always has 1 value const router = getRouter(contracts); // Assume deployedContract always has 1 value
const deployedTokenRoute = router.address;
// Mutate the config.remoteRouters by setting it to all other routers to update // Mutate the config.remoteRouters by setting it to all other routers to update
const warpRouteReader = new EvmERC20WarpRouteReader( const warpRouteReader = new EvmERC20WarpRouteReader(
multiProvider, multiProvider,
chain, chain,
); );
const mutatedWarpRouteConfig = const mutatedWarpRouteConfig =
await warpRouteReader.deriveWarpRouteConfig(router.address); await warpRouteReader.deriveWarpRouteConfig(deployedTokenRoute);
const {
domainRoutingIsmFactory,
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
} = registryAddresses[chain];
const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, {
config: mutatedWarpRouteConfig, config: mutatedWarpRouteConfig,
chain, chain,
addresses: { addresses: {
...registryAddresses[chain], deployedTokenRoute,
deployedTokenRoute: router.address, staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
}, },
}); });

@ -3,9 +3,12 @@ import { Wallet } from 'ethers';
import { ChainAddresses } from '@hyperlane-xyz/registry'; import { ChainAddresses } from '@hyperlane-xyz/registry';
import { import {
HookType,
TokenRouterConfig, TokenRouterConfig,
TokenType, TokenType,
WarpRouteDeployConfig, WarpRouteDeployConfig,
normalizeConfig,
randomAddress,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
@ -95,6 +98,45 @@ describe('WarpApply e2e tests', async function () {
); );
}); });
it('should update hook configuration', async () => {
const warpDeployPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`;
// First read the existing config
const warpDeployConfig = await readWarpConfig(
CHAIN_NAME_2,
WARP_CORE_CONFIG_PATH_2,
warpDeployPath,
);
// Update with a new hook config
const owner = randomAddress();
warpDeployConfig[CHAIN_NAME_2].hook = {
type: HookType.PROTOCOL_FEE,
beneficiary: owner,
maxProtocolFee: '1000000',
protocolFee: '100000',
owner,
};
// Write the updated config
await writeYamlOrJson(warpDeployPath, warpDeployConfig);
// Apply the changes
await hyperlaneWarpApply(warpDeployPath, WARP_CORE_CONFIG_PATH_2);
// Read back the config to verify changes
const updatedConfig = await readWarpConfig(
CHAIN_NAME_2,
WARP_CORE_CONFIG_PATH_2,
warpDeployPath,
);
// Verify the hook was updated with all properties
expect(normalizeConfig(updatedConfig[CHAIN_NAME_2].hook)).to.deep.equal(
normalizeConfig(warpDeployConfig[CHAIN_NAME_2].hook),
);
});
it('should extend an existing warp route', async () => { it('should extend an existing warp route', async () => {
// Read existing config into a file // Read existing config into a file
const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`;

@ -104,7 +104,7 @@ export class EvmHookModule extends HyperlaneModule<
// Transaction overrides for the chain // Transaction overrides for the chain
protected readonly txOverrides: Partial<ethers.providers.TransactionRequest>; protected readonly txOverrides: Partial<ethers.providers.TransactionRequest>;
protected constructor( constructor(
protected readonly multiProvider: MultiProvider, protected readonly multiProvider: MultiProvider,
params: HyperlaneModuleParams< params: HyperlaneModuleParams<
HookConfig, HookConfig,
@ -243,7 +243,7 @@ export class EvmHookModule extends HyperlaneModule<
chain: ChainNameOrId; chain: ChainNameOrId;
config: HookConfig; config: HookConfig;
proxyFactoryFactories: HyperlaneAddresses<ProxyFactoryFactories>; proxyFactoryFactories: HyperlaneAddresses<ProxyFactoryFactories>;
coreAddresses: CoreAddresses; coreAddresses: Omit<CoreAddresses, 'validatorAnnounce'>;
multiProvider: MultiProvider; multiProvider: MultiProvider;
contractVerifier?: ContractVerifier; contractVerifier?: ContractVerifier;
}): Promise<EvmHookModule> { }): Promise<EvmHookModule> {

@ -1,6 +1,5 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
import { expect } from 'chai'; import { expect } from 'chai';
import { constants } from 'ethers';
import hre from 'hardhat'; import hre from 'hardhat';
import { import {
@ -12,16 +11,20 @@ import {
HypERC4626Collateral__factory, HypERC4626Collateral__factory,
HypNative__factory, HypNative__factory,
Mailbox, Mailbox,
MailboxClient__factory,
Mailbox__factory, Mailbox__factory,
} from '@hyperlane-xyz/core'; } from '@hyperlane-xyz/core';
import { import {
EvmIsmModule, EvmIsmModule,
HookConfig,
HookType,
HyperlaneAddresses, HyperlaneAddresses,
HyperlaneContractsMap, HyperlaneContractsMap,
IsmConfig, IsmConfig,
IsmType, IsmType,
RouterConfig, RouterConfig,
TestChainName, TestChainName,
proxyAdmin,
serializeContracts, serializeContracts,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
@ -29,6 +32,7 @@ import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { DerivedHookConfig } from '../hook/EvmHookReader.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js'; import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
@ -55,7 +59,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const TOKEN_DECIMALS = 18; const TOKEN_DECIMALS = 18;
const chain = TestChainName.test4; const chain = TestChainName.test4;
let mailbox: Mailbox; let mailbox: Mailbox;
let hookAddress: string;
let ismAddress: string; let ismAddress: string;
let ismFactory: HyperlaneIsmFactory; let ismFactory: HyperlaneIsmFactory;
let factories: HyperlaneContractsMap<ProxyFactoryFactories>; let factories: HyperlaneContractsMap<ProxyFactoryFactories>;
@ -70,10 +73,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
async function validateCoreValues(deployedToken: GasRouter) { async function validateCoreValues(deployedToken: GasRouter) {
expect(await deployedToken.mailbox()).to.equal(mailbox.address); expect(await deployedToken.mailbox()).to.equal(mailbox.address);
expect(await deployedToken.hook()).to.equal(hookAddress);
expect(await deployedToken.interchainSecurityModule()).to.equal(
constants.AddressZero,
);
expect(await deployedToken.owner()).to.equal(signer.address); expect(await deployedToken.owner()).to.equal(signer.address);
} }
@ -106,7 +105,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
baseConfig = routerConfigMap[chain]; baseConfig = routerConfigMap[chain];
mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer);
hookAddress = await mailbox.defaultHook();
ismAddress = await mailbox.defaultIsm(); ismAddress = await mailbox.defaultIsm();
}); });
@ -115,7 +113,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
...baseConfig, ...baseConfig,
type: TokenType.collateral, type: TokenType.collateral,
token: token.address, token: token.address,
hook: hookAddress,
}; };
// Deploy using WarpModule // Deploy using WarpModule
@ -143,7 +140,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config: TokenRouterConfig = { const config: TokenRouterConfig = {
type: TokenType.collateralVault, type: TokenType.collateralVault,
token: vault.address, token: vault.address,
hook: hookAddress,
...baseConfig, ...baseConfig,
}; };
@ -175,13 +171,12 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
it('should create with a synthetic config', async () => { it('should create with a synthetic config', async () => {
const config: TokenRouterConfig = { const config: TokenRouterConfig = {
...baseConfig,
type: TokenType.synthetic, type: TokenType.synthetic,
hook: hookAddress,
name: TOKEN_NAME, name: TOKEN_NAME,
symbol: TOKEN_NAME, symbol: TOKEN_NAME,
decimals: TOKEN_DECIMALS, decimals: TOKEN_DECIMALS,
totalSupply: TOKEN_SUPPLY, totalSupply: TOKEN_SUPPLY,
...baseConfig,
}; };
// Deploy using WarpModule // Deploy using WarpModule
@ -213,7 +208,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
it('should create with a native config', async () => { it('should create with a native config', async () => {
const config = { const config = {
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
...baseConfig, ...baseConfig,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -244,7 +238,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
remoteRouters: randomRemoteRouters(numOfRouters), remoteRouters: randomRemoteRouters(numOfRouters),
} as TokenRouterConfig; } as TokenRouterConfig;
@ -260,28 +253,48 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
}); });
describe('Update', async () => { describe('Update', async () => {
const owner = randomAddress();
const ismConfigToUpdate: IsmConfig[] = [ const ismConfigToUpdate: IsmConfig[] = [
{ {
type: IsmType.TRUSTED_RELAYER, type: IsmType.TRUSTED_RELAYER,
relayer: randomAddress(), relayer: owner,
}, },
{ {
type: IsmType.FALLBACK_ROUTING, type: IsmType.FALLBACK_ROUTING,
owner: randomAddress(), owner: owner,
domains: {}, domains: {},
}, },
{ {
type: IsmType.PAUSABLE, type: IsmType.PAUSABLE,
owner: randomAddress(), owner: owner,
paused: false, paused: false,
}, },
]; ];
const hookConfigToUpdate: HookConfig[] = [
{
type: HookType.PROTOCOL_FEE,
beneficiary: owner,
owner: owner,
maxProtocolFee: '1337',
protocolFee: '1337',
},
{
type: HookType.INTERCHAIN_GAS_PAYMASTER,
owner: owner,
beneficiary: owner,
oracleKey: owner,
overhead: {},
oracleConfig: {},
},
{
type: HookType.MERKLE_TREE,
},
];
it('should deploy and set a new Ism', async () => { it('should deploy and set a new Ism', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
interchainSecurityModule: ismAddress, interchainSecurityModule: ismAddress,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -297,7 +310,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
for (const interchainSecurityModule of ismConfigToUpdate) { for (const interchainSecurityModule of ismConfigToUpdate) {
const expectedConfig: TokenRouterConfig = { const expectedConfig: TokenRouterConfig = {
...actualConfig, ...actualConfig,
interchainSecurityModule, interchainSecurityModule,
}; };
await sendTxs(await evmERC20WarpModule.update(expectedConfig)); await sendTxs(await evmERC20WarpModule.update(expectedConfig));
@ -313,7 +325,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
interchainSecurityModule: ismAddress, interchainSecurityModule: ismAddress,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -351,6 +362,99 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
expect(txs.length).to.equal(0); expect(txs.length).to.equal(0);
}); });
it('should update and set a new Hook based on config', async () => {
const config = {
...baseConfig,
type: TokenType.native,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
proxyFactoryFactories: ismFactoryAddresses,
});
const actualConfig = await evmERC20WarpModule.read();
for (const hook of hookConfigToUpdate) {
const expectedConfig: TokenRouterConfig = {
...actualConfig,
hook,
};
await sendTxs(await evmERC20WarpModule.update(expectedConfig));
const updatedConfig = (await evmERC20WarpModule.read())
.hook as DerivedHookConfig;
expect(normalizeConfig(updatedConfig)).to.deep.equal(hook);
}
});
it('should set new deployed hook mailbox to WarpConfig.owner', async () => {
const config = {
...baseConfig,
type: TokenType.native,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
proxyFactoryFactories: ismFactoryAddresses,
});
const actualConfig = await evmERC20WarpModule.read();
const expectedConfig: TokenRouterConfig = {
...actualConfig,
hook: hookConfigToUpdate.find(
(c: any) => c.type === HookType.MERKLE_TREE,
),
};
await sendTxs(await evmERC20WarpModule.update(expectedConfig));
const updatedConfig = (await evmERC20WarpModule.read())
.hook as DerivedHookConfig;
const hook = MailboxClient__factory.connect(
updatedConfig.address,
multiProvider.getProvider(chain),
);
expect(await hook.mailbox()).to.equal(expectedConfig.mailbox);
});
it("should set Proxied Hook's proxyAdmins to WarpConfig.proxyAdmin", async () => {
const config = {
...baseConfig,
type: TokenType.native,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
proxyFactoryFactories: ismFactoryAddresses,
});
const actualConfig = await evmERC20WarpModule.read();
const expectedConfig: TokenRouterConfig = {
...actualConfig,
hook: hookConfigToUpdate.find(
(c: any) => c.type === HookType.INTERCHAIN_GAS_PAYMASTER,
),
};
await sendTxs(await evmERC20WarpModule.update(expectedConfig));
const updatedConfig = (await evmERC20WarpModule.read())
.hook as DerivedHookConfig;
expect(
await proxyAdmin(
multiProvider.getProvider(chain),
updatedConfig.address,
),
).to.equal(expectedConfig.proxyAdmin?.address);
});
it('should update a mutable Ism', async () => { it('should update a mutable Ism', async () => {
const ismConfig: IsmConfig = { const ismConfig: IsmConfig = {
type: IsmType.ROUTING, type: IsmType.ROUTING,
@ -372,7 +476,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
interchainSecurityModule: deployedIsm, interchainSecurityModule: deployedIsm,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -409,7 +512,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
ismFactoryAddresses, ismFactoryAddresses,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -441,7 +543,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
ismFactoryAddresses, ismFactoryAddresses,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -494,7 +595,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config = { const config = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
ismFactoryAddresses, ismFactoryAddresses,
} as TokenRouterConfig; } as TokenRouterConfig;
@ -535,7 +635,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config: TokenRouterConfig = { const config: TokenRouterConfig = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
}; };
const owner = signer.address.toLowerCase(); const owner = signer.address.toLowerCase();
@ -579,7 +678,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const config: TokenRouterConfig = { const config: TokenRouterConfig = {
...baseConfig, ...baseConfig,
type: TokenType.native, type: TokenType.native,
hook: hookAddress,
remoteRouters: { remoteRouters: {
[domain]: randomAddress(), [domain]: randomAddress(),
}, },

@ -19,6 +19,7 @@ import {
addressToBytes32, addressToBytes32,
assert, assert,
deepEquals, deepEquals,
eqAddress,
isObjEmpty, isObjEmpty,
objMap, objMap,
rootLogger, rootLogger,
@ -31,6 +32,8 @@ import {
} from '../core/AbstractHyperlaneModule.js'; } from '../core/AbstractHyperlaneModule.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; import { proxyAdminUpdateTxs } from '../deploy/proxy.js';
import { EvmHookModule } from '../hook/EvmHookModule.js';
import { DerivedHookConfig } from '../hook/EvmHookReader.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js'; import { MultiProvider } from '../providers/MultiProvider.js';
@ -42,12 +45,13 @@ import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { HypERC20Deployer } from './deploy.js'; import { HypERC20Deployer } from './deploy.js';
import { TokenRouterConfig, TokenRouterConfigSchema } from './schemas.js'; import { TokenRouterConfig, TokenRouterConfigSchema } from './schemas.js';
type WarpRouteAddresses = HyperlaneAddresses<ProxyFactoryFactories> & {
deployedTokenRoute: Address;
};
export class EvmERC20WarpModule extends HyperlaneModule< export class EvmERC20WarpModule extends HyperlaneModule<
ProtocolType.Ethereum, ProtocolType.Ethereum,
TokenRouterConfig, TokenRouterConfig,
HyperlaneAddresses<ProxyFactoryFactories> & { WarpRouteAddresses
deployedTokenRoute: Address;
}
> { > {
protected logger = rootLogger.child({ protected logger = rootLogger.child({
module: 'EvmERC20WarpModule', module: 'EvmERC20WarpModule',
@ -59,12 +63,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
constructor( constructor(
protected readonly multiProvider: MultiProvider, protected readonly multiProvider: MultiProvider,
args: HyperlaneModuleParams< args: HyperlaneModuleParams<TokenRouterConfig, WarpRouteAddresses>,
TokenRouterConfig,
HyperlaneAddresses<ProxyFactoryFactories> & {
deployedTokenRoute: Address;
}
>,
protected readonly contractVerifier?: ContractVerifier, protected readonly contractVerifier?: ContractVerifier,
) { ) {
super(args); super(args);
@ -87,7 +86,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
* @param address - The address to derive the token router configuration from. * @param address - The address to derive the token router configuration from.
* @returns A promise that resolves to the token router configuration. * @returns A promise that resolves to the token router configuration.
*/ */
public async read(): Promise<TokenRouterConfig> { async read(): Promise<TokenRouterConfig> {
return this.reader.deriveWarpRouteConfig( return this.reader.deriveWarpRouteConfig(
this.args.addresses.deployedTokenRoute, this.args.addresses.deployedTokenRoute,
); );
@ -99,7 +98,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
* @param expectedConfig - The configuration for the token router to be updated. * @param expectedConfig - The configuration for the token router to be updated.
* @returns An array of Ethereum transactions that were executed to update the contract, or an error if the update failed. * @returns An array of Ethereum transactions that were executed to update the contract, or an error if the update failed.
*/ */
public async update( async update(
expectedConfig: TokenRouterConfig, expectedConfig: TokenRouterConfig,
): Promise<AnnotatedEV5Transaction[]> { ): Promise<AnnotatedEV5Transaction[]> {
TokenRouterConfigSchema.parse(expectedConfig); TokenRouterConfigSchema.parse(expectedConfig);
@ -115,6 +114,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
*/ */
transactions.push( transactions.push(
...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)), ...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)),
...(await this.createHookUpdateTxs(actualConfig, expectedConfig)),
...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig),
...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig), ...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig),
...this.createOwnershipUpdateTxs(actualConfig, expectedConfig), ...this.createOwnershipUpdateTxs(actualConfig, expectedConfig),
@ -280,6 +280,47 @@ export class EvmERC20WarpModule extends HyperlaneModule<
return updateTransactions; return updateTransactions;
} }
async createHookUpdateTxs(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): Promise<AnnotatedEV5Transaction[]> {
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (!expectedConfig.hook) {
return [];
}
const actualDeployedHook = (actualConfig.hook as DerivedHookConfig)
?.address;
// Try to deploy or update Hook with the expected config
const {
deployedHook: expectedDeployedHook,
updateTransactions: hookUpdateTransactions,
} = await this.deployOrUpdateHook(actualConfig, expectedConfig);
// If a Hook is updated in-place, push the update txs
updateTransactions.push(...hookUpdateTransactions);
// If a new Hook is deployed, push the setHook tx
if (!eqAddress(actualDeployedHook, expectedDeployedHook)) {
const contractToUpdate = MailboxClient__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);
updateTransactions.push({
chainId: this.chainId,
annotation: `Setting Hook for Warp Route to ${expectedDeployedHook}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData('setHook', [
expectedDeployedHook,
]),
});
}
return updateTransactions;
}
/** /**
* Transfer ownership of an existing Warp route with a given config. * Transfer ownership of an existing Warp route with a given config.
* *
@ -305,17 +346,14 @@ export class EvmERC20WarpModule extends HyperlaneModule<
* *
* @returns Object with deployedIsm address, and update Transactions * @returns Object with deployedIsm address, and update Transactions
*/ */
public async deployOrUpdateIsm( async deployOrUpdateIsm(
actualConfig: TokenRouterConfig, actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig, expectedConfig: TokenRouterConfig,
): Promise<{ ): Promise<{
deployedIsm: Address; deployedIsm: Address;
updateTransactions: AnnotatedEV5Transaction[]; updateTransactions: AnnotatedEV5Transaction[];
}> { }> {
assert( assert(expectedConfig.interchainSecurityModule, 'Ism derived incorrectly');
expectedConfig.interchainSecurityModule,
'Ism not derived correctly',
);
const ismModule = new EvmIsmModule( const ismModule = new EvmIsmModule(
this.multiProvider, this.multiProvider,
@ -343,6 +381,124 @@ export class EvmERC20WarpModule extends HyperlaneModule<
return { deployedIsm, updateTransactions }; return { deployedIsm, updateTransactions };
} }
/**
* Updates or deploys the hook using the provided configuration.
*
* @returns Object with deployedHook address, and update Transactions
*/
async deployOrUpdateHook(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): Promise<{
deployedHook: Address;
updateTransactions: AnnotatedEV5Transaction[];
}> {
assert(expectedConfig.hook, 'No hook config');
if (!actualConfig.hook) {
return this.deployNewHook(expectedConfig);
}
return this.updateExistingHook(expectedConfig, actualConfig);
}
async deployNewHook(expectedConfig: TokenRouterConfig): Promise<{
deployedHook: Address;
updateTransactions: AnnotatedEV5Transaction[];
}> {
this.logger.info(
`No hook deployed for warp route, deploying new hook on ${this.args.chain} chain`,
);
const {
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
} = this.args.addresses;
assert(expectedConfig.hook, 'Hook is undefined');
assert(
expectedConfig.proxyAdmin?.address,
'ProxyAdmin address is undefined',
);
const hookModule = await EvmHookModule.create({
chain: this.args.chain,
config: expectedConfig.hook,
proxyFactoryFactories: {
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
},
coreAddresses: {
mailbox: expectedConfig.mailbox,
proxyAdmin: expectedConfig.proxyAdmin?.address, // Assume that a proxyAdmin is always deployed with a WarpRoute
},
contractVerifier: this.contractVerifier,
multiProvider: this.multiProvider,
});
const { deployedHook } = hookModule.serialize();
return { deployedHook, updateTransactions: [] };
}
async updateExistingHook(
expectedConfig: TokenRouterConfig,
actualConfig: TokenRouterConfig,
): Promise<{
deployedHook: Address;
updateTransactions: AnnotatedEV5Transaction[];
}> {
const {
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
} = this.args.addresses;
assert(actualConfig.proxyAdmin?.address, 'ProxyAdmin address is undefined');
assert(actualConfig.hook, 'Hook is undefined');
const hookModule = new EvmHookModule(
this.multiProvider,
{
chain: this.args.chain,
config: actualConfig.hook,
addresses: {
staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory,
staticAggregationHookFactory,
domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory,
mailbox: actualConfig.mailbox,
proxyAdmin: actualConfig.proxyAdmin?.address,
deployedHook: (actualConfig.hook as DerivedHookConfig).address,
},
},
this.contractVerifier,
);
this.logger.info(
`Comparing target Hook config with ${this.args.chain} chain`,
);
const updateTransactions = await hookModule.update(expectedConfig.hook!);
const { deployedHook } = hookModule.serialize();
return { deployedHook, updateTransactions };
}
/** /**
* Deploys the Warp Route. * Deploys the Warp Route.
* *
@ -351,7 +507,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
* @param multiProvider - The multi-provider instance to use. * @param multiProvider - The multi-provider instance to use.
* @returns A new instance of the EvmERC20WarpHyperlaneModule. * @returns A new instance of the EvmERC20WarpHyperlaneModule.
*/ */
public static async create(params: { static async create(params: {
chain: ChainNameOrId; chain: ChainNameOrId;
config: TokenRouterConfig; config: TokenRouterConfig;
multiProvider: MultiProvider; multiProvider: MultiProvider;

Loading…
Cancel
Save