feat: WarpModule with create() (#3838)

### Description
- Adds WarpModule with create logic. 
- Updates logic to WarpModuleReader

### Drive by
- Adds CustomIsmConfig mentioned in
https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3773

### Related issues
- Fixes #3839 

### 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/3914/head
Lee 5 months ago committed by GitHub
parent cc65ee8fa1
commit bdcbe1d168
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/five-baboons-smoke.md
  2. 2
      typescript/cli/src/config/ism.ts
  3. 1
      typescript/sdk/src/hook/types.ts
  4. 4
      typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts
  5. 4
      typescript/sdk/src/ism/types.ts
  6. 202
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  7. 93
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  8. 237
      typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts
  9. 165
      typescript/sdk/src/token/EvmERC20WarpRouteReader.ts
  10. 5
      typescript/sdk/src/token/config.ts
  11. 9
      typescript/sdk/src/token/deploy.hardhat-test.ts

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

@ -58,7 +58,7 @@ export function readIsmConfig(filePath: string) {
return parsedConfig;
}
const ISM_TYPE_DESCRIPTIONS: Record<IsmType, string> = {
const ISM_TYPE_DESCRIPTIONS: Record<string, string> = {
[IsmType.MESSAGE_ID_MULTISIG]: 'Validators need to sign just this messageId',
[IsmType.MERKLE_ROOT_MULTISIG]:
'Validators need to sign the root of the merkle tree of all messages from origin chain',

@ -27,6 +27,7 @@ export enum OnchainHookType {
}
export enum HookType {
CUSTOM = 'custom',
MERKLE_TREE = 'merkleTreeHook',
INTERCHAIN_GAS_PAYMASTER = 'interchainGasPaymaster',
AGGREGATION = 'aggregationHook',

@ -353,13 +353,13 @@ describe('HyperlaneIsmFactory', async () => {
});
const existingIsm = ism.address;
const domainsBefore = await (ism as DomainRoutingIsm).domains();
// deleting the domain and removing from multiprovider should unenroll the domain
// NB: we'll deploy new multisigIsms for the domains bc of new factories but the routingIsm address should be the same because of existingIsmAddress
delete exampleRoutingConfig.domains['test3'];
multiProvider = multiProvider.intersect([
TestChainName.test1,
'test2',
TestChainName.test2,
TestChainName.test4,
]).result;
ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
ismFactory = new HyperlaneIsmFactory(

@ -2,6 +2,7 @@ import { z } from 'zod';
import {
IAggregationIsm,
IInterchainSecurityModule,
IMultisigIsm,
IRoutingIsm,
OPStackIsm,
@ -39,6 +40,7 @@ export enum ModuleType {
// this enum can be adjusted as per deployments necessary
// meant for the deployer and checker
export enum IsmType {
CUSTOM = 'custom',
OP_STACK = 'opStackIsm',
ROUTING = 'domainRoutingIsm',
FALLBACK_ROUTING = 'defaultFallbackRoutingIsm',
@ -73,6 +75,7 @@ export function ismTypeToModuleType(ismType: IsmType): ModuleType {
case IsmType.OP_STACK:
case IsmType.TEST_ISM:
case IsmType.PAUSABLE:
case IsmType.CUSTOM:
case IsmType.TRUSTED_RELAYER:
return ModuleType.NULL;
}
@ -111,6 +114,7 @@ export type AggregationIsmConfig = {
export type IsmConfig = z.infer<typeof IsmConfigSchema>;
export type DeployedIsmType = {
[IsmType.CUSTOM]: IInterchainSecurityModule;
[IsmType.ROUTING]: IRoutingIsm;
[IsmType.FALLBACK_ROUTING]: IRoutingIsm;
[IsmType.AGGREGATION]: IAggregationIsm;

@ -0,0 +1,202 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
import { expect } from 'chai';
import { constants } from 'ethers';
import hre from 'hardhat';
import {
ERC20Test,
ERC20Test__factory,
ERC4626Test__factory,
GasRouter,
HypERC20CollateralVaultDeposit__factory,
HypERC20__factory,
HypNative__factory,
Mailbox,
Mailbox__factory,
} from '@hyperlane-xyz/core';
import {
HyperlaneContractsMap,
RouterConfig,
TestChainName,
} from '@hyperlane-xyz/sdk';
import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainMap } from '../types.js';
import { EvmERC20WarpModule } from './EvmERC20WarpModule.js';
import { TokenType } from './config.js';
import { TokenRouterConfig } from './schemas.js';
describe('EvmERC20WarpHyperlaneModule', async () => {
const TOKEN_NAME = 'fake';
const TOKEN_SUPPLY = '100000000000000000000';
const TOKEN_DECIMALS = 18;
const chain = TestChainName.test4;
let mailbox: Mailbox;
let hookAddress: string;
let ismFactory: HyperlaneIsmFactory;
let factories: HyperlaneContractsMap<ProxyFactoryFactories>;
let erc20Factory: ERC20Test__factory;
let token: ERC20Test;
let signer: SignerWithAddress;
let multiProvider: MultiProvider;
let coreApp: TestCoreApp;
let routerConfigMap: ChainMap<RouterConfig>;
let baseConfig: RouterConfig;
async function validateCoreValues(deployedToken: GasRouter) {
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);
}
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
factories = await ismFactoryDeployer.deploy(
multiProvider.mapKnownChains(() => ({})),
);
ismFactory = new HyperlaneIsmFactory(factories, multiProvider);
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp();
routerConfigMap = coreApp.getRouterConfig(signer.address);
erc20Factory = new ERC20Test__factory(signer);
token = await erc20Factory.deploy(
TOKEN_NAME,
TOKEN_NAME,
TOKEN_SUPPLY,
TOKEN_DECIMALS,
);
baseConfig = routerConfigMap[chain];
mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer);
hookAddress = await mailbox.defaultHook();
});
it('should create with a a collateral config', async () => {
const config = {
...baseConfig,
type: TokenType.collateral,
token: token.address,
hook: hookAddress,
};
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
// Let's derive it's onchain token type
const { deployedTokenRoute } = evmERC20WarpModule.serialize();
const tokenType: TokenType =
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute);
expect(tokenType).to.equal(TokenType.collateral);
});
it('should create with a collateral vault config', async () => {
const vaultFactory = new ERC4626Test__factory(signer);
const vault = await vaultFactory.deploy(
token.address,
TOKEN_NAME,
TOKEN_NAME,
);
const config = {
type: TokenType.collateralVault,
token: vault.address,
hook: hookAddress,
...baseConfig,
};
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
// Let's derive it's onchain token type
const { deployedTokenRoute } = evmERC20WarpModule.serialize();
const tokenType: TokenType =
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute);
expect(tokenType).to.equal(TokenType.collateralVault);
// Validate onchain token values
const collateralVault = HypERC20CollateralVaultDeposit__factory.connect(
deployedTokenRoute,
signer,
);
await validateCoreValues(collateralVault);
expect(await collateralVault.vault()).to.equal(vault.address);
expect(await collateralVault.wrappedToken()).to.equal(token.address);
});
it('should create with a a synthetic config', async () => {
const config = {
type: TokenType.synthetic,
token: token.address,
hook: hookAddress,
name: TOKEN_NAME,
symbol: TOKEN_NAME,
decimals: TOKEN_DECIMALS,
totalSupply: TOKEN_SUPPLY,
...baseConfig,
};
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
// Let's derive it's onchain token type
const { deployedTokenRoute } = evmERC20WarpModule.serialize();
const tokenType: TokenType =
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute);
expect(tokenType).to.equal(TokenType.synthetic);
// Validate onchain token values
const synthetic = HypERC20__factory.connect(deployedTokenRoute, signer);
await validateCoreValues(synthetic);
expect(await synthetic.name()).to.equal(TOKEN_NAME);
expect(await synthetic.symbol()).to.equal(TOKEN_NAME);
expect(await synthetic.decimals()).to.equal(TOKEN_DECIMALS);
expect(await synthetic.totalSupply()).to.equal(TOKEN_SUPPLY);
});
it('should create with a a native config', async () => {
const config = {
type: TokenType.native,
hook: hookAddress,
...baseConfig,
} as TokenRouterConfig;
// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config,
multiProvider,
});
// Let's derive it's onchain token type
const { deployedTokenRoute } = evmERC20WarpModule.serialize();
const tokenType: TokenType =
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute);
expect(tokenType).to.equal(TokenType.native);
// Validate onchain token values
const native = HypNative__factory.connect(deployedTokenRoute, signer);
await validateCoreValues(native);
});
});

@ -0,0 +1,93 @@
import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.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';
export class EvmERC20WarpModule extends HyperlaneModule<
ProtocolType.Ethereum,
TokenRouterConfig,
{
deployedTokenRoute: Address;
}
> {
protected logger = rootLogger.child({
module: 'EvmERC20WarpModule',
});
reader: EvmERC20WarpRouteReader;
constructor(
protected readonly multiProvider: MultiProvider,
args: HyperlaneModuleParams<
TokenRouterConfig,
{
deployedTokenRoute: Address;
}
>,
) {
super(args);
this.reader = new EvmERC20WarpRouteReader(multiProvider, args.chain);
}
/**
* Retrieves the token router configuration for the specified address.
*
* @param address - The address to derive the token router configuration from.
* @returns A promise that resolves to the token router configuration.
*/
public async read(): Promise<TokenRouterConfig> {
return this.reader.deriveWarpRouteConfig(
this.args.addresses.deployedTokenRoute,
);
}
/**
* 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,
): Promise<AnnotatedEV5Transaction[]> {
throw Error('Not implemented');
}
/**
* Deploys the Warp Route.
*
* @param chain - The chain to deploy the module on.
* @param config - The configuration for the token router.
* @param multiProvider - The multi-provider instance to use.
* @returns A new instance of the EvmERC20WarpHyperlaneModule.
*/
public static async create(params: {
chain: ChainNameOrId;
config: TokenRouterConfig;
multiProvider: MultiProvider;
}): Promise<EvmERC20WarpModule> {
const { chain, config, multiProvider } = params;
const chainName = multiProvider.getChainName(chain);
const deployer = new HypERC20Deployer(multiProvider);
const deployedContracts = await deployer.deployContracts(chainName, config);
return new EvmERC20WarpModule(multiProvider, {
addresses: {
deployedTokenRoute: deployedContracts[config.type].address,
},
chain,
config,
});
}
}

@ -0,0 +1,237 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
import { expect } from 'chai';
import hre from 'hardhat';
import {
ERC20Test,
ERC20Test__factory,
ERC4626,
ERC4626Test__factory,
Mailbox,
Mailbox__factory,
} from '@hyperlane-xyz/core';
import {
HyperlaneContractsMap,
RouterConfig,
TestChainName,
TokenRouterConfig,
} from '@hyperlane-xyz/sdk';
import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainMap } from '../types.js';
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { TokenType } from './config.js';
import { HypERC20Deployer } from './deploy.js';
describe('ERC20WarpRouterReader', async () => {
const TOKEN_NAME = 'fake';
const TOKEN_SUPPLY = '100000000000000000000';
const TOKEN_DECIMALS = 18;
const GAS = 65_000;
const chain = TestChainName.test4;
let ismFactory: HyperlaneIsmFactory;
let factories: HyperlaneContractsMap<ProxyFactoryFactories>;
let erc20Factory: ERC20Test__factory;
let token: ERC20Test;
let signer: SignerWithAddress;
let deployer: HypERC20Deployer;
let multiProvider: MultiProvider;
let coreApp: TestCoreApp;
let routerConfigMap: ChainMap<RouterConfig>;
let baseConfig: RouterConfig;
let mailbox: Mailbox;
let evmERC20WarpRouteReader: EvmERC20WarpRouteReader;
let vault: ERC4626;
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
factories = await ismFactoryDeployer.deploy(
multiProvider.mapKnownChains(() => ({})),
);
ismFactory = new HyperlaneIsmFactory(factories, multiProvider);
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp();
routerConfigMap = coreApp.getRouterConfig(signer.address);
erc20Factory = new ERC20Test__factory(signer);
token = await erc20Factory.deploy(
TOKEN_NAME,
TOKEN_NAME,
TOKEN_SUPPLY,
TOKEN_DECIMALS,
);
baseConfig = routerConfigMap[chain];
mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer);
evmERC20WarpRouteReader = new EvmERC20WarpRouteReader(multiProvider, chain);
deployer = new HypERC20Deployer(multiProvider);
const vaultFactory = new ERC4626Test__factory(signer);
vault = await vaultFactory.deploy(token.address, TOKEN_NAME, TOKEN_NAME);
});
it('should derive a token type from contract', async () => {
const typesToDerive = [
TokenType.collateral,
TokenType.collateralVault,
TokenType.synthetic,
TokenType.native,
] as const;
await Promise.all(
typesToDerive.map(async (type) => {
// Create config
const config = {
[chain]: {
type,
token:
type === TokenType.collateralVault
? vault.address
: token.address,
hook: await mailbox.defaultHook(),
name: TOKEN_NAME,
symbol: TOKEN_NAME,
decimals: TOKEN_DECIMALS,
totalSupply: TOKEN_SUPPLY,
gas: GAS,
...baseConfig,
},
};
// Deploy warp route with config
const warpRoute = await deployer.deploy(config);
const derivedTokenType = await evmERC20WarpRouteReader.deriveTokenType(
warpRoute[chain][type].address,
);
expect(derivedTokenType).to.equal(type);
}),
);
});
it('should derive collateral config correctly', async () => {
// Create config
const config = {
[chain]: {
type: TokenType.collateral,
token: token.address,
hook: await mailbox.defaultHook(),
interchainsecurityModule: await mailbox.defaultIsm(),
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].collateral.address,
);
for (const [key, value] of Object.entries(derivedConfig)) {
const deployedValue = (config[chain] as any)[key];
if (deployedValue && typeof value === 'string')
expect(deployedValue).to.equal(value);
}
// Check hook because they're potentially objects
expect(derivedConfig.hook).to.deep.equal(
await evmERC20WarpRouteReader.evmHookReader.deriveHookConfig(
config[chain].hook as string,
),
);
// Check ism. should return undefined
expect(derivedConfig.interchainSecurityModule).to.be.undefined;
// Check if token values matches
if (derivedConfig.type === TokenType.collateral) {
expect(derivedConfig.name).to.equal(TOKEN_NAME);
expect(derivedConfig.symbol).to.equal(TOKEN_NAME);
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS);
}
});
it('should derive synthetic config correctly', async () => {
// Create config
const config = {
[chain]: {
type: TokenType.synthetic,
token: token.address,
hook: await mailbox.defaultHook(),
name: TOKEN_NAME,
symbol: TOKEN_NAME,
decimals: TOKEN_DECIMALS,
totalSupply: TOKEN_SUPPLY,
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].synthetic.address,
);
for (const [key, value] of Object.entries(derivedConfig)) {
const deployedValue = (config[chain] as any)[key];
if (deployedValue && typeof value === 'string')
expect(deployedValue).to.equal(value);
}
// Check if token values matches
if (derivedConfig.type === TokenType.collateral) {
expect(derivedConfig.name).to.equal(TOKEN_NAME);
expect(derivedConfig.symbol).to.equal(TOKEN_NAME);
}
});
it('should derive native config correctly', async () => {
// Create config
const config = {
[chain]: {
type: TokenType.native,
hook: await mailbox.defaultHook(),
...baseConfig,
},
} as ChainMap<TokenRouterConfig>;
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].native.address,
);
for (const [key, value] of Object.entries(derivedConfig)) {
const deployedValue = (config[chain] as any)[key];
if (deployedValue && typeof value === 'string')
expect(deployedValue).to.equal(value);
}
// Check if token values matches
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS);
});
it('should return undefined if ism is not set onchain', async () => {
// Create config
const config = {
[chain]: {
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 each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].collateral.address,
);
expect(derivedConfig.interchainSecurityModule).to.be.undefined;
});
});

@ -1,33 +1,37 @@
import { ethers, providers } from 'ethers';
import { BigNumber, constants, providers } from 'ethers';
import {
ERC20__factory,
HypERC20CollateralVaultDeposit__factory,
HypERC20Collateral__factory,
MailboxClient__factory,
HypERC20__factory,
} from '@hyperlane-xyz/core';
import { Address, eqAddress } from '@hyperlane-xyz/utils';
import {
MailboxClientConfig,
TokenRouterConfig,
TokenType,
} from '@hyperlane-xyz/sdk';
import { Address, eqAddress, rootLogger } from '@hyperlane-xyz/utils';
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 { MailboxClientConfig } from '../router/types.js';
import { ChainName } from '../types.js';
import { ChainNameOrId } from '../types.js';
import { TokenType } from './config.js';
import { TokenRouterConfig } from './schemas.js';
import { CollateralExtensions } from './config.js';
import { TokenMetadata } from './types.js';
const { AddressZero } = ethers.constants;
export class EvmERC20WarpRouteReader {
protected readonly logger = rootLogger.child({
module: 'EvmERC20WarpRouteReader',
});
provider: providers.Provider;
evmHookReader: EvmHookReader;
evmIsmReader: EvmIsmReader;
constructor(
protected readonly multiProvider: MultiProvider,
protected readonly chain: ChainName,
protected readonly chain: ChainNameOrId,
protected readonly concurrency: number = DEFAULT_CONTRACT_READ_CONCURRENCY,
) {
this.provider = this.multiProvider.getProvider(chain);
@ -38,50 +42,91 @@ export class EvmERC20WarpRouteReader {
/**
* Derives the configuration for a Hyperlane ERC20 router contract at the given address.
*
* @param address - The address of the Hyperlane ERC20 router contract.
* @param warpRouteAddress - The address of the Hyperlane ERC20 router contract.
* @returns The configuration for the Hyperlane ERC20 router.
*
*/
async deriveWarpRouteConfig(
address: Address,
type = TokenType.collateral,
warpRouteAddress: Address,
): Promise<TokenRouterConfig> {
const mailboxClientConfig = await this.fetchMailboxClientConfig(address);
let token: Address;
switch (type) {
case TokenType.collateral:
token = await HypERC20Collateral__factory.connect(
address,
this.provider,
).wrappedToken();
break;
case TokenType.synthetic:
token = address;
break;
default:
throw new Error(`Invalid token type: ${type}`);
}
const fetchedTokenMetadata = await this.fetchTokenMetadata(token);
// Derive the config type
const type = await this.deriveTokenType(warpRouteAddress);
const fetchedBaseMetadata = await this.fetchMailboxClientConfig(
warpRouteAddress,
);
const fetchedTokenMetadata = await this.fetchTokenMetadata(
type,
warpRouteAddress,
);
return {
type,
token: TokenType.collateral === type ? token : undefined,
...mailboxClientConfig,
...fetchedBaseMetadata,
...fetchedTokenMetadata,
type,
} as TokenRouterConfig;
}
/**
* Derives the token type for a given Warp Route address using specific methods
*
* @param warpRouteAddress - The Warp Route address to derive the token type for.
* @returns The derived token type, which can be one of: collateralVault, collateral, native, or synthetic.
*/
async deriveTokenType(warpRouteAddress: Address): Promise<TokenType> {
const contractTypes: Partial<
Record<TokenType, { factory: any; method: string }>
> = {
collateralVault: {
factory: HypERC20CollateralVaultDeposit__factory,
method: 'vault',
},
collateral: {
factory: HypERC20Collateral__factory,
method: 'wrappedToken',
},
synthetic: {
factory: HypERC20__factory,
method: 'decimals',
},
};
// First, try checking token specific methods
for (const [type, { factory, method }] of Object.entries(contractTypes)) {
try {
const warpRoute = factory.connect(warpRouteAddress, this.provider);
await warpRoute[method]();
return type as TokenType;
} catch (e) {
continue;
}
}
// Finally check native
// Using estimateGas to send 1 wei. Success implies that the Warp Route has a receive() function
try {
await this.multiProvider.estimateGas(this.chain, {
to: warpRouteAddress,
from: await this.multiProvider.getSignerAddress(this.chain),
value: BigNumber.from(1),
});
return TokenType.native;
} catch (e) {
throw Error(
`Error accessing token specific method, implying this is not a supported token.`,
);
}
}
/**
* 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.
* @returns The base metadata for the Warp Route contract, including the mailbox, owner, hook, and ism.
*/
async fetchMailboxClientConfig(
routerAddress: Address,
): Promise<MailboxClientConfig> {
const warpRoute = MailboxClient__factory.connect(
const warpRoute = HypERC20Collateral__factory.connect(
routerAddress,
this.provider,
);
@ -92,10 +137,10 @@ export class EvmERC20WarpRouteReader {
warpRoute.interchainSecurityModule(),
]);
const derivedIsm = eqAddress(ism, AddressZero)
const derivedIsm = eqAddress(ism, constants.AddressZero)
? undefined
: await this.evmIsmReader.deriveIsmConfig(ism);
const derivedHook = eqAddress(hook, AddressZero)
const derivedHook = eqAddress(hook, constants.AddressZero)
? undefined
: await this.evmHookReader.deriveHookConfig(hook);
@ -112,16 +157,50 @@ export class EvmERC20WarpRouteReader {
*
* @param tokenAddress - The address of the token.
* @returns A partial ERC20 metadata object containing the token name, symbol, total supply, and decimals.
* Throws if unsupported token type
*/
async fetchTokenMetadata(tokenAddress: Address): Promise<TokenMetadata> {
const erc20 = ERC20__factory.connect(tokenAddress, this.provider);
const [name, symbol, totalSupply, decimals] = await Promise.all([
async fetchTokenMetadata(
type: TokenType,
tokenAddress: Address,
): Promise<TokenMetadata & { token?: string }> {
if (CollateralExtensions.includes(type)) {
const erc20 = HypERC20Collateral__factory.connect(
tokenAddress,
this.provider,
);
const token = await erc20.wrappedToken();
const { name, symbol, decimals, totalSupply } =
await this.fetchERC20Metadata(token);
return { name, symbol, decimals, totalSupply, token };
} else if (type === TokenType.synthetic) {
return this.fetchERC20Metadata(tokenAddress);
} else if (type === TokenType.native) {
const chainMetadata = this.multiProvider.getChainMetadata(this.chain);
if (chainMetadata.nativeToken) {
const { name, symbol, decimals } = chainMetadata.nativeToken;
return { name, symbol, decimals, totalSupply: 0 };
} else {
throw new Error(
`Warp route config specifies native token but chain metadata for ${this.chain} does not provide native token details`,
);
}
} else {
throw new Error(
`Unsupported token type ${type} when fetching token metadata`,
);
}
}
async fetchERC20Metadata(tokenAddress: Address): Promise<TokenMetadata> {
const erc20 = HypERC20__factory.connect(tokenAddress, this.provider);
const [name, symbol, decimals, totalSupply] = await Promise.all([
erc20.name(),
erc20.symbol(),
erc20.totalSupply(),
erc20.decimals(),
erc20.totalSupply(),
]);
return { name, symbol, totalSupply: totalSupply.toString(), decimals };
return { name, symbol, decimals, totalSupply: totalSupply.toString() };
}
}

@ -13,6 +13,11 @@ export enum TokenType {
nativeScaled = 'nativeScaled',
}
export const CollateralExtensions = [
TokenType.collateral,
TokenType.collateralVault,
];
export const gasOverhead = (tokenType: TokenType) => {
switch (tokenType) {
case TokenType.fastSynthetic:

@ -91,12 +91,9 @@ describe('TokenDeployer', async () => {
routerAddress = warpRoute[chain][type].address;
});
it(`should derive TokenRouterConfig from ${type} correctly`, async () => {
const derivedConfig = await reader.deriveWarpRouteConfig(
routerAddress,
type,
);
expect(derivedConfig).to.include(config[chain]);
it(`should derive TokenRouterConfig correctly`, async () => {
const derivedConfig = await reader.deriveWarpRouteConfig(routerAddress);
expect(derivedConfig.type).to.equal(config[chain].type);
});
});
}

Loading…
Cancel
Save