Skip to content

Commit

Permalink
Derive WarpConfig from TokenRouter address (#3671)
Browse files Browse the repository at this point in the history
### Description
Add Reader to derive WarpConfig from TokenRouter address

### Drive-by changes

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

### Related issues

- Fixes #3579 

### Backward compatibility
Yes

### Testing

Unit Tests
  • Loading branch information
ltyu authored Apr 29, 2024
1 parent 2db77f1 commit 3a08e31
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-cherries-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

Add EvmERC20WarpRouterReader to derive WarpConfig from TokenRouter address
79 changes: 76 additions & 3 deletions typescript/sdk/src/token/deploy.hardhat-test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
import { expect } from 'chai';
import hre from 'hardhat';

import {
ERC20Test,
ERC20Test__factory,
Mailbox__factory,
} from '@hyperlane-xyz/core';
import { Chains, RouterConfig } from '@hyperlane-xyz/sdk';
import { objMap } from '@hyperlane-xyz/utils';

import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { RouterConfig } from '../index.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainMap } from '../types.js';

import { HypERC20Config, TokenConfig, TokenType } from './config.js';
import {
HypERC20CollateralConfig,
HypERC20Config,
TokenConfig,
TokenType,
} from './config.js';
import { HypERC20Deployer } from './deploy.js';
import { EvmERC20WarpRouteReader } from './read.js';
import { WarpRouteDeployConfig } from './types.js';

describe('TokenDeployer', async () => {
let signer: SignerWithAddress;
let deployer: HypERC20Deployer;
let multiProvider: MultiProvider;
let coreApp: TestCoreApp;
let routerConfigMap: ChainMap<RouterConfig>;
let config: WarpRouteDeployConfig;

before(async () => {
Expand All @@ -31,7 +44,7 @@ describe('TokenDeployer', async () => {
);
const ismFactory = new HyperlaneIsmFactory(factories, multiProvider);
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp();
const routerConfigMap = coreApp.getRouterConfig(signer.address);
routerConfigMap = coreApp.getRouterConfig(signer.address);
config = objMap(
routerConfigMap,
(chain, c): HypERC20Config => ({
Expand All @@ -53,4 +66,64 @@ describe('TokenDeployer', async () => {
it('deploys', async () => {
await deployer.deploy(config as ChainMap<TokenConfig & RouterConfig>);
});

describe('ERC20WarpRouterReader', async () => {
const TOKEN_NAME = 'fake';
const TOKEN_SUPPLY = '100000000000000000000';
const TOKEN_DECIMALS = 18;
let erc20Factory: ERC20Test__factory;
let token: ERC20Test;

before(async () => {
erc20Factory = new ERC20Test__factory(signer);
token = await erc20Factory.deploy(
TOKEN_NAME,
TOKEN_NAME,
TOKEN_SUPPLY,
TOKEN_DECIMALS,
);
});
async function deriveWarpConfig(chainName: string, address: string) {
return new EvmERC20WarpRouteReader(
multiProvider,
chainName,
).deriveWarpRouteConfig(address);
}
it('should derive ERC20RouterConfig from collateral correctly', async () => {
const baseConfig = routerConfigMap[Chains.test1];
const mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer);

// Create config
const config: { [key: string]: any } = {
[Chains.test1]: {
type: TokenType.collateral,
token: token.address,
hook: await mailbox.defaultHook(),
gas: 65_000,
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(
config as ChainMap<TokenConfig & RouterConfig>,
);

// Derive config and check if each value matches
const derivedConfig: Partial<HypERC20CollateralConfig> =
await deriveWarpConfig(
Chains.test1,
warpRoute[Chains.test1].collateral.address,
);

for (const [key, value] of Object.entries(derivedConfig)) {
const deployedValue = config[Chains.test1][key];
if (deployedValue) expect(deployedValue).to.equal(value);
}

// Check if token values matches
expect(derivedConfig.name).to.equal(TOKEN_NAME);
expect(derivedConfig.symbol).to.equal(TOKEN_NAME);
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS);
});
});
});
125 changes: 125 additions & 0 deletions typescript/sdk/src/token/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ethers, providers } from 'ethers';

import {
ERC20__factory,
HypERC20Collateral__factory,
} from '@hyperlane-xyz/core';
import { ERC20Metadata, ERC20RouterConfig } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';

import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js';
import { EvmHookReader } from '../hook/read.js';
import { EvmIsmReader } from '../ism/read.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainName } from '../types.js';

type WarpRouteBaseMetadata = Record<
'mailbox' | 'owner' | 'token' | 'hook' | 'interchainSecurityModule',
string
>;

type DerivedERC20WarpRouteConfig = Omit<ERC20RouterConfig, 'type' | 'gas'>;

export class EvmERC20WarpRouteReader {
provider: providers.Provider;
evmHookReader: EvmHookReader;
evmIsmReader: EvmIsmReader;

constructor(
protected readonly multiProvider: MultiProvider,
protected readonly chain: ChainName,
protected readonly concurrency: number = DEFAULT_CONTRACT_READ_CONCURRENCY,
) {
this.provider = this.multiProvider.getProvider(chain);
this.evmHookReader = new EvmHookReader(multiProvider, chain, concurrency);
this.evmIsmReader = new EvmIsmReader(multiProvider, chain, concurrency);
}

/**
* Derives the configuration for a Hyperlane ERC20 router contract at the given address.
*
* @param address - The address of the Hyperlane ERC20 router contract.
* @returns The configuration for the Hyperlane ERC20 router.
*
*/
async deriveWarpRouteConfig(
address: Address,
): Promise<DerivedERC20WarpRouteConfig> {
const fetchedBaseMetadata = await this.fetchBaseMetadata(address);
const fetchedTokenMetadata = await this.fetchTokenMetadata(
fetchedBaseMetadata.token,
);

const results: DerivedERC20WarpRouteConfig = {
...fetchedBaseMetadata,
...fetchedTokenMetadata,
};

if (
fetchedBaseMetadata.interchainSecurityModule !==
ethers.constants.AddressZero
) {
results.interchainSecurityModule =
await this.evmIsmReader.deriveIsmConfig(
fetchedBaseMetadata.interchainSecurityModule,
);
}
// @todo add after https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3667 is fixed
// if (fetchedBaseMetadata.hook !== ethers.constants.AddressZero) {
// results.hook = await this.evmHookReader.deriveHookConfig(
// fetchedBaseMetadata.hook,
// );
// }

return results;
}

/**
* Fetches the base metadata for a Warp Route contract.
*
* @param routerAddress - The address of the Warp Route contract.
* @returns The base metadata for the Warp Route contract, including the mailbox, owner, wrapped token address, hook, and interchain security module.
*/
async fetchBaseMetadata(
routerAddress: Address,
): Promise<WarpRouteBaseMetadata> {
const warpRoute = HypERC20Collateral__factory.connect(
routerAddress,
this.provider,
);
const [mailbox, owner, token, hook, interchainSecurityModule] =
await Promise.all([
warpRoute.mailbox(),
warpRoute.owner(),
warpRoute.wrappedToken(),
warpRoute.hook(),
warpRoute.interchainSecurityModule(),
]);

return {
mailbox,
owner,
token,
hook,
interchainSecurityModule,
};
}

/**
* Fetches the metadata for a token address.
*
* @param tokenAddress - The address of the token.
* @returns A partial ERC20 metadata object containing the token name, symbol, total supply, and decimals.
*/
async fetchTokenMetadata(tokenAddress: Address): Promise<ERC20Metadata> {
const erc20 = ERC20__factory.connect(tokenAddress, this.provider);
const [name, symbol, totalSupply, decimals] = await Promise.all([
erc20.name(),
erc20.symbol(),
erc20.totalSupply(),
erc20.decimals(),
]);

return { name, symbol, totalSupply, decimals };
}
}

0 comments on commit 3a08e31

Please sign in to comment.