feat(sdk): Implement enroll router logic into WarpModule (#4145)

### Description
- Adds (optional) `remoteRouters` into WarpDeployConfig so update() can
use it to compare with onchain config
- Adds enroll router logic into WarpModule

### Related issues

- Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4144

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing
Unit Tests
pull/4127/head
Lee 3 months ago committed by GitHub
parent 7489a5e143
commit 79740755bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/itchy-phones-rest.md
  2. 11
      typescript/sdk/src/router/schemas.ts
  3. 3
      typescript/sdk/src/router/types.ts
  4. 113
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  5. 84
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  6. 34
      typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts
  7. 33
      typescript/sdk/src/token/EvmERC20WarpRouteReader.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Add enroll remote router to WarpModule

@ -17,8 +17,19 @@ export const ForeignDeploymentConfigSchema = z.object({
foreignDeployment: z.string().optional(),
});
const RemoteRouterDomain = z.string();
const RemoteRouterRouter = z.string().startsWith('0x');
export const RemoteRouterSchema = z.record(
RemoteRouterDomain,
RemoteRouterRouter,
);
export const RouterConfigSchema = MailboxClientConfigSchema.merge(
ForeignDeploymentConfigSchema,
).merge(
z.object({
remoteRouters: RemoteRouterSchema.optional(),
}),
);
export const GasRouterConfigSchema = RouterConfigSchema.extend({

@ -15,6 +15,7 @@ import { CheckerViolation } from '../deploy/types.js';
import {
GasRouterConfigSchema,
MailboxClientConfigSchema,
RemoteRouterSchema,
RouterConfigSchema,
} from './schemas.js';
@ -61,3 +62,5 @@ export interface RouterViolation extends CheckerViolation {
expected: string;
description?: string;
}
export type RemoteRouter = z.infer<typeof RemoteRouterSchema>;

@ -33,6 +33,7 @@ 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 { RemoteRouter } from '../router/types.js';
import { randomAddress } from '../test/testUtils.js';
import { ChainMap } from '../types.js';
@ -40,6 +41,14 @@ import { EvmERC20WarpModule } from './EvmERC20WarpModule.js';
import { TokenType } from './config.js';
import { TokenRouterConfig } from './schemas.js';
const randomRemoteRouters = (n: number) => {
const routers: RemoteRouter = {};
for (let domain = 0; domain < n; domain++) {
routers[domain] = randomAddress();
}
return routers;
};
describe('EvmERC20WarpHyperlaneModule', async () => {
const TOKEN_NAME = 'fake';
const TOKEN_SUPPLY = '100000000000000000000';
@ -228,7 +237,26 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
await validateCoreValues(nativeContract);
});
describe('Update Ism', async () => {
it('should create with remote routers', async () => {
const numOfRouters = Math.floor(Math.random() * 10);
const config = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
remoteRouters: randomRemoteRouters(numOfRouters),
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
const { remoteRouters } = await evmERC20WarpModule.read();
expect(Object.keys(remoteRouters!).length).to.equal(numOfRouters);
});
describe('Update', async () => {
const ismConfigToUpdate: IsmConfig[] = [
{
type: IsmType.TRUSTED_RELAYER,
@ -371,5 +399,88 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
expectedConfig.interchainSecurityModule,
);
});
it('should update connected routers', async () => {
const config = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
ismFactoryAddresses,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config: {
...config,
interchainSecurityModule: ismAddress,
},
multiProvider,
});
const numOfRouters = Math.floor(Math.random() * 10);
await sendTxs(
await evmERC20WarpModule.update({
...config,
remoteRouters: randomRemoteRouters(numOfRouters),
}),
);
const updatedConfig = await evmERC20WarpModule.read();
expect(Object.keys(updatedConfig.remoteRouters!).length).to.be.equal(
numOfRouters,
);
});
it('should only extend routers if they are new ones are different', async () => {
const config = {
...baseConfig,
type: TokenType.native,
hook: hookAddress,
ismFactoryAddresses,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config: {
...config,
interchainSecurityModule: ismAddress,
},
multiProvider,
});
const remoteRouters = randomRemoteRouters(1);
await sendTxs(
await evmERC20WarpModule.update({
...config,
remoteRouters,
}),
);
let updatedConfig = await evmERC20WarpModule.read();
expect(Object.keys(updatedConfig.remoteRouters!).length).to.be.equal(1);
// Try to extend with the same remoteRouters
let txs = await evmERC20WarpModule.update({
...config,
remoteRouters,
});
expect(txs.length).to.equal(0);
await sendTxs(txs);
// Try to extend with the different remoteRouters, but same length
txs = await evmERC20WarpModule.update({
...config,
remoteRouters: {
3: randomAddress(),
},
});
expect(txs.length).to.equal(1);
await sendTxs(txs);
updatedConfig = await evmERC20WarpModule.read();
expect(Object.keys(updatedConfig.remoteRouters!).length).to.be.equal(2);
});
});
});

@ -1,11 +1,18 @@
import { MailboxClient__factory } from '@hyperlane-xyz/core';
import {
MailboxClient__factory,
TokenRouter__factory,
} from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { ContractVerifier, ExplorerLicenseType } from '@hyperlane-xyz/sdk';
import {
Address,
Domain,
ProtocolType,
addressToBytes32,
assert,
configDeepEquals,
isObjEmpty,
normalizeConfig,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -83,7 +90,65 @@ export class EvmERC20WarpModule extends HyperlaneModule<
TokenRouterConfigSchema.parse(expectedConfig);
const actualConfig = await this.read();
return this.updateIsm(actualConfig, expectedConfig);
const transactions = [];
transactions.push(
...(await this.updateIsm(actualConfig, expectedConfig)),
...(await this.updateRemoteRouters(actualConfig, expectedConfig)),
);
return transactions;
}
/**
* Updates the remote routers for the Warp Route contract.
*
* @param actualConfig - The on-chain router configuration, including the remoteRouters array.
* @param expectedConfig - The expected token router configuration.
* @returns A array with a single Ethereum transaction that need to be executed to enroll the routers
*/
async updateRemoteRouters(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): Promise<AnnotatedEV5Transaction[]> {
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (!expectedConfig.remoteRouters) {
return [];
}
// We normalize the addresses for comparison
actualConfig.remoteRouters = normalizeConfig(actualConfig.remoteRouters);
expectedConfig.remoteRouters = normalizeConfig(
expectedConfig.remoteRouters,
);
assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined');
assert(expectedConfig.remoteRouters, 'actualRemoteRouters is undefined');
const { remoteRouters: actualRemoteRouters } = actualConfig;
const { remoteRouters: expectedRemoteRouters } = expectedConfig;
if (!configDeepEquals(actualRemoteRouters, expectedRemoteRouters)) {
const contractToUpdate = TokenRouter__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);
updateTransactions.push({
annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute}}`,
chainId: this.domainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'enrollRemoteRouters',
[
Object.keys(expectedRemoteRouters).map((k) => Number(k)),
Object.values(expectedRemoteRouters).map((a) =>
addressToBytes32(a),
),
],
),
});
}
return updateTransactions;
}
/**
@ -98,6 +163,9 @@ export class EvmERC20WarpModule extends HyperlaneModule<
expectedConfig: TokenRouterConfig,
): Promise<AnnotatedEV5Transaction[]> {
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (!expectedConfig.interchainSecurityModule) {
return [];
}
const actualDeployedIsm = (
actualConfig.interchainSecurityModule as DerivedIsmConfig
).address;
@ -115,7 +183,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
if (actualDeployedIsm !== expectedDeployedIsm) {
const contractToUpdate = MailboxClient__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.args.chain),
this.multiProvider.getProvider(this.domainId),
);
updateTransactions.push({
annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`,
@ -197,7 +265,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
const deployer = new HypERC20Deployer(multiProvider);
const deployedContracts = await deployer.deployContracts(chainName, config);
return new EvmERC20WarpModule(
const warpModule = new EvmERC20WarpModule(
multiProvider,
{
addresses: {
@ -208,5 +276,13 @@ export class EvmERC20WarpModule extends HyperlaneModule<
},
contractVerifier,
);
if (config.remoteRouters && !isObjEmpty(config.remoteRouters)) {
const enrollRemoteTxs = await warpModule.update(config); // @TODO Remove when EvmERC20WarpModule.create can be used
const onlyTxIndex = 0;
await multiProvider.sendTransaction(chain, enrollRemoteTxs[onlyTxIndex]);
}
return warpModule;
}
}

@ -15,6 +15,7 @@ import {
RouterConfig,
TestChainName,
TokenRouterConfig,
test3,
} from '@hyperlane-xyz/sdk';
import { TestCoreApp } from '../core/TestCoreApp.js';
@ -48,7 +49,7 @@ describe('ERC20WarpRouterReader', async () => {
let mailbox: Mailbox;
let evmERC20WarpRouteReader: EvmERC20WarpRouteReader;
let vault: ERC4626;
before(async () => {
beforeEach(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
@ -234,4 +235,35 @@ describe('ERC20WarpRouterReader', async () => {
expect(derivedConfig.interchainSecurityModule).to.be.undefined;
});
it('should return the remote routers', async () => {
// Create config
const otherChain = TestChainName.test3;
const otherChainMetadata = test3;
const config = {
[chain]: {
type: TokenType.collateral,
token: token.address,
hook: await mailbox.defaultHook(),
...baseConfig,
},
[otherChain]: {
type: TokenType.collateral,
token: token.address,
hook: await mailbox.defaultHook(),
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if remote router matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].collateral.address,
);
expect(Object.keys(derivedConfig.remoteRouters!).length).to.equal(1);
expect(
derivedConfig.remoteRouters![otherChainMetadata.domainId!],
).to.be.equal(warpRoute[otherChain].collateral.address);
});
});

@ -4,6 +4,7 @@ import {
HypERC20CollateralVaultDeposit__factory,
HypERC20Collateral__factory,
HypERC20__factory,
TokenRouter__factory,
} from '@hyperlane-xyz/core';
import {
MailboxClientConfig,
@ -12,6 +13,7 @@ import {
} from '@hyperlane-xyz/sdk';
import {
Address,
bytes32ToAddress,
eqAddress,
getLogLevel,
rootLogger,
@ -21,6 +23,7 @@ import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
import { EvmHookReader } from '../hook/EvmHookReader.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { RemoteRouter } from '../router/types.js';
import { ChainNameOrId } from '../types.js';
import { CollateralExtensions } from './config.js';
@ -56,17 +59,14 @@ export class EvmERC20WarpRouteReader {
): Promise<TokenRouterConfig> {
// Derive the config type
const type = await this.deriveTokenType(warpRouteAddress);
const fetchedBaseMetadata = await this.fetchMailboxClientConfig(
warpRouteAddress,
);
const fetchedTokenMetadata = await this.fetchTokenMetadata(
type,
warpRouteAddress,
);
const baseMetadata = await this.fetchMailboxClientConfig(warpRouteAddress);
const tokenMetadata = await this.fetchTokenMetadata(type, warpRouteAddress);
const remoteRouters = await this.fetchRemoteRouters(warpRouteAddress);
return {
...fetchedBaseMetadata,
...fetchedTokenMetadata,
...baseMetadata,
...tokenMetadata,
remoteRouters,
type,
} as TokenRouterConfig;
}
@ -219,6 +219,21 @@ export class EvmERC20WarpRouteReader {
return { name, symbol, decimals, totalSupply: totalSupply.toString() };
}
async fetchRemoteRouters(warpRouteAddress: Address): Promise<RemoteRouter> {
const warpRoute = TokenRouter__factory.connect(
warpRouteAddress,
this.provider,
);
const domains = await warpRoute.domains();
return Object.fromEntries(
await Promise.all(
domains.map(async (domain) => {
return [domain, bytes32ToAddress(await warpRoute.routers(domain))];
}),
),
);
}
/**
* Conditionally sets the log level for a smart provider.
*

Loading…
Cancel
Save