feat: WarpModule with update() IsmConfig (#3678)

### Description
Implement WarpModule update() with IsmConfig

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues
- Fixes https://github.com/hyperlane-xyz/issues/issues/1156

### Backward compatibility
Yes

### Testing
Unit Tests

---------

Signed-off-by: Paul Balaji <paul@hyperlane.xyz>
Co-authored-by: Paul Balaji <paul@hyperlane.xyz>
Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>
Co-authored-by: J M Rossy <jm.rossy@gmail.com>
Co-authored-by: Noah Bayindirli 🥂 <noah@primeprotocol.xyz>
Co-authored-by: Noah Bayindirli 🥂 <noah@hyperlane.xyz>
pull/4093/head
Lee 5 months ago committed by GitHub
parent f733379488
commit 1687fca93c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/smart-dancers-type.md
  2. 8
      typescript/sdk/src/deploy/schemas.ts
  3. 28
      typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts
  4. 2
      typescript/sdk/src/ism/EvmIsmModule.ts
  5. 2
      typescript/sdk/src/router/schemas.ts
  6. 163
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  7. 110
      typescript/sdk/src/token/EvmERC20WarpModule.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Add EvmWarpModule with update() for ISM

@ -5,3 +5,11 @@ export const OwnerSchema = z.string();
export const OwnableConfigSchema = z.object({
owner: OwnerSchema,
});
export const ProxyFactoryFactoriesSchema = z.object({
staticMerkleRootMultisigIsmFactory: z.string(),
staticMessageIdMultisigIsmFactory: z.string(),
staticAggregationIsmFactory: z.string(),
staticAggregationHookFactory: z.string(),
domainRoutingIsmFactory: z.string(),
});

@ -406,6 +406,34 @@ describe('EvmIsmModule', async () => {
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`update threshold in an existing ${type} with Module creating using constructor`, async () => {
// create a an initial ISM
const { initialIsmAddress } = await createIsm(exampleRoutingConfig);
// update the threshold for a domain
(
exampleRoutingConfig.domains[TestChainName.test2] as MultisigIsmConfig
).threshold = 2;
// create a new IsmModule using it's constructor. Set it's deployedIsm address to the initialIsmAddr
const ism = new EvmIsmModule(multiProvider, {
chain,
config: exampleRoutingConfig,
addresses: {
...factoryAddresses,
mailbox: mailboxAddress,
deployedIsm: initialIsmAddress,
},
});
// expect 1 tx to update threshold for test2 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
}
it(`redeploy same config if the mailbox address changes for defaultFallbackRoutingIsm`, async () => {

@ -81,7 +81,7 @@ export class EvmIsmModule extends HyperlaneModule<
// return a number, and EVM the domainId and chainId are the same.
public readonly domainId: Domain;
protected constructor(
constructor(
protected readonly multiProvider: MultiProvider,
params: HyperlaneModuleParams<
IsmConfig,

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ProxyFactoryFactoriesSchema } from '../deploy/schemas.js';
import { HookConfigSchema } from '../hook/schemas.js';
import { IsmConfigSchema } from '../ism/schemas.js';
import { ZHash } from '../metadata/customZodTypes.js';
@ -9,6 +10,7 @@ export const MailboxClientConfigSchema = OwnableSchema.extend({
mailbox: ZHash,
hook: HookConfigSchema.optional(),
interchainSecurityModule: IsmConfigSchema.optional(),
ismFactoryAddresses: ProxyFactoryFactoriesSchema.optional(),
});
export const ForeignDeploymentConfigSchema = z.object({

@ -15,10 +15,16 @@ import {
Mailbox__factory,
} from '@hyperlane-xyz/core';
import {
EvmIsmModule,
HyperlaneAddresses,
HyperlaneContractsMap,
IsmConfig,
IsmType,
RouterConfig,
TestChainName,
serializeContracts,
} from '@hyperlane-xyz/sdk';
import { normalizeConfig } from '@hyperlane-xyz/utils';
import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
@ -26,6 +32,8 @@ import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDe
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { randomAddress } from '../test/testUtils.js';
import { ChainMap } from '../types.js';
import { EvmERC20WarpModule } from './EvmERC20WarpModule.js';
@ -39,8 +47,10 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
const chain = TestChainName.test4;
let mailbox: Mailbox;
let hookAddress: string;
let ismAddress: string;
let ismFactory: HyperlaneIsmFactory;
let factories: HyperlaneContractsMap<ProxyFactoryFactories>;
let ismFactoryAddresses: HyperlaneAddresses<ProxyFactoryFactories>;
let erc20Factory: ERC20Test__factory;
let token: ERC20Test;
let signer: SignerWithAddress;
@ -58,6 +68,12 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
expect(await deployedToken.owner()).to.equal(signer.address);
}
async function sendTxs(txs: AnnotatedEV5Transaction[]) {
for (const tx of txs) {
await multiProvider.sendTransaction(chain, tx);
}
}
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
@ -65,6 +81,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
factories = await ismFactoryDeployer.deploy(
multiProvider.mapKnownChains(() => ({})),
);
ismFactoryAddresses = serializeContracts(factories[chain]);
ismFactory = new HyperlaneIsmFactory(factories, multiProvider);
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp();
routerConfigMap = coreApp.getRouterConfig(signer.address);
@ -81,6 +98,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer);
hookAddress = await mailbox.defaultHook();
ismAddress = await mailbox.defaultIsm();
});
it('should create with a collateral config', async () => {
@ -209,4 +227,149 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
);
await validateCoreValues(nativeContract);
});
describe('Update Ism', async () => {
const ismConfigToUpdate: IsmConfig[] = [
{
type: IsmType.TRUSTED_RELAYER,
relayer: randomAddress(),
},
{
type: IsmType.FALLBACK_ROUTING,
owner: randomAddress(),
domains: {},
},
{
type: IsmType.PAUSABLE,
owner: randomAddress(),
paused: false,
},
];
it('should deploy and set a new Ism', async () => {
const config = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
interchainSecurityModule: ismAddress,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
const actualConfig = await evmERC20WarpModule.read();
for (const interchainSecurityModule of ismConfigToUpdate) {
const expectedConfig: TokenRouterConfig = {
...actualConfig,
ismFactoryAddresses,
interchainSecurityModule,
};
await sendTxs(await evmERC20WarpModule.update(expectedConfig));
const updatedConfig = normalizeConfig(
(await evmERC20WarpModule.read()).interchainSecurityModule,
);
expect(updatedConfig).to.deep.equal(interchainSecurityModule);
}
});
it('should not deploy and set a new Ism if the config is the same', async () => {
const config = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
interchainSecurityModule: ismAddress,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
const actualConfig = await evmERC20WarpModule.read();
const owner = randomAddress();
const interchainSecurityModule: IsmConfig = {
type: IsmType.PAUSABLE,
owner,
paused: false,
};
const expectedConfig: TokenRouterConfig = {
...actualConfig,
ismFactoryAddresses,
interchainSecurityModule,
};
await sendTxs(await evmERC20WarpModule.update(expectedConfig));
const updatedConfig = normalizeConfig(
(await evmERC20WarpModule.read()).interchainSecurityModule,
);
expect(updatedConfig).to.deep.equal(interchainSecurityModule);
// Deploy with the same config
const txs = await evmERC20WarpModule.update(expectedConfig);
expect(txs.length).to.equal(0);
});
it('should update a mutable Ism', async () => {
const ismConfig: IsmConfig = {
type: IsmType.ROUTING,
owner: signer.address,
domains: {
'1': ismAddress,
},
};
const ism = await EvmIsmModule.create({
chain,
multiProvider,
config: ismConfig,
proxyFactoryFactories: ismFactoryAddresses,
mailbox: mailbox.address,
});
const { deployedIsm } = ism.serialize();
// Deploy using WarpModule
const config = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
interchainSecurityModule: deployedIsm,
} as TokenRouterConfig;
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
const actualConfig = await evmERC20WarpModule.read();
const expectedConfig: TokenRouterConfig = {
...actualConfig,
ismFactoryAddresses,
interchainSecurityModule: {
type: IsmType.ROUTING,
owner: randomAddress(),
domains: {
'2': ismAddress,
},
},
};
await sendTxs(await evmERC20WarpModule.update(expectedConfig));
const updatedConfig = normalizeConfig(
(await evmERC20WarpModule.read()).interchainSecurityModule,
);
expect(updatedConfig).to.deep.equal(
expectedConfig.interchainSecurityModule,
);
});
});
});

@ -1,16 +1,25 @@
import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
import { MailboxClient__factory } from '@hyperlane-xyz/core';
import {
Address,
Domain,
ProtocolType,
assert,
rootLogger,
} from '@hyperlane-xyz/utils';
import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { HypERC20Deployer } from './deploy.js';
import { TokenRouterConfig } from './schemas.js';
import { TokenRouterConfig, TokenRouterConfigSchema } from './schemas.js';
export class EvmERC20WarpModule extends HyperlaneModule<
ProtocolType.Ethereum,
@ -23,6 +32,9 @@ export class EvmERC20WarpModule extends HyperlaneModule<
module: 'EvmERC20WarpModule',
});
reader: EvmERC20WarpRouteReader;
// We use domainId here because MultiProvider.getDomainId() will always
// return a number, and EVM the domainId and chainId are the same.
public readonly domainId: Domain;
constructor(
protected readonly multiProvider: MultiProvider,
@ -35,6 +47,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
) {
super(args);
this.reader = new EvmERC20WarpRouteReader(multiProvider, args.chain);
this.domainId = multiProvider.getDomainId(args.chain);
}
/**
@ -52,15 +65,100 @@ export class EvmERC20WarpModule extends HyperlaneModule<
/**
* Updates the Warp Route contract with the provided configuration.
*
* @remark Currently only supports updating ISM or hook.
*
* @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.
*/
public async update(
_expectedConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): Promise<AnnotatedEV5Transaction[]> {
throw Error('Not implemented');
TokenRouterConfigSchema.parse(expectedConfig);
const actualConfig = await this.read();
return this.updateIsm(actualConfig, expectedConfig);
}
/**
* Updates an existing Warp route ISM with a given config.
*
* @param actualConfig - The on-chain router configuration, including the ISM configuration, and address.
* @param expectedConfig - The expected token router configuration, including the ISM configuration.
* @returns Ethereum transaction that need to be executed to update the ISM configuration.
*/
async updateIsm(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): Promise<AnnotatedEV5Transaction[]> {
const updateTransactions: AnnotatedEV5Transaction[] = [];
const actualDeployedIsm = (
actualConfig.interchainSecurityModule as DerivedIsmConfig
).address;
// Try to update (may also deploy) Ism with the expected config
const {
deployedIsm: expectedDeployedIsm,
updateTransactions: ismUpdateTransactions,
} = await this.deployOrUpdateIsm(actualConfig, expectedConfig);
// If an ISM is updated in-place, push the update txs
updateTransactions.push(...ismUpdateTransactions);
// If a new ISM is deployed, push the setInterchainSecurityModule tx
if (actualDeployedIsm !== expectedDeployedIsm) {
const contractToUpdate = MailboxClient__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.args.chain),
);
updateTransactions.push({
annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`,
chainId: this.domainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'setInterchainSecurityModule',
[expectedDeployedIsm],
),
});
}
return updateTransactions;
}
/**
* Updates or deploys the ISM using the provided configuration.
*
* @returns Object with deployedIsm address, and update Transactions
*/
public async deployOrUpdateIsm(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): Promise<{
deployedIsm: Address;
updateTransactions: AnnotatedEV5Transaction[];
}> {
assert(
expectedConfig.interchainSecurityModule,
'Ism not derived correctly',
);
assert(
expectedConfig.ismFactoryAddresses,
'Ism Factories addresses not provided',
);
const ismModule = new EvmIsmModule(this.multiProvider, {
chain: this.args.chain,
config: expectedConfig.interchainSecurityModule,
addresses: {
...expectedConfig.ismFactoryAddresses,
mailbox: expectedConfig.mailbox,
deployedIsm: (actualConfig.interchainSecurityModule as DerivedIsmConfig)
.address,
},
});
const updateTransactions = await ismModule.update(
expectedConfig.interchainSecurityModule,
);
const { deployedIsm } = ismModule.serialize();
return { deployedIsm, updateTransactions };
}
/**

Loading…
Cancel
Save