diff --git a/.changeset/five-baboons-smoke.md b/.changeset/five-baboons-smoke.md new file mode 100644 index 0000000000..3cebc87ac8 --- /dev/null +++ b/.changeset/five-baboons-smoke.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Add EvmWarpModule with create() diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index d16926aba5..d3d90971fa 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -58,7 +58,7 @@ export function readIsmConfig(filePath: string) { return parsedConfig; } -const ISM_TYPE_DESCRIPTIONS: Record = { +const ISM_TYPE_DESCRIPTIONS: Record = { [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', diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index 8cbd26fd25..c0edaafced 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -27,6 +27,7 @@ export enum OnchainHookType { } export enum HookType { + CUSTOM = 'custom', MERKLE_TREE = 'merkleTreeHook', INTERCHAIN_GAS_PAYMASTER = 'interchainGasPaymaster', AGGREGATION = 'aggregationHook', diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts index 180f493712..87cf1044a3 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts @@ -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( diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts index b1dc1fe641..631a7a5f54 100644 --- a/typescript/sdk/src/ism/types.ts +++ b/typescript/sdk/src/ism/types.ts @@ -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; export type DeployedIsmType = { + [IsmType.CUSTOM]: IInterchainSecurityModule; [IsmType.ROUTING]: IRoutingIsm; [IsmType.FALLBACK_ROUTING]: IRoutingIsm; [IsmType.AGGREGATION]: IAggregationIsm; diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts new file mode 100644 index 0000000000..bf64cfa17a --- /dev/null +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -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; + let erc20Factory: ERC20Test__factory; + let token: ERC20Test; + let signer: SignerWithAddress; + let multiProvider: MultiProvider; + let coreApp: TestCoreApp; + let routerConfigMap: ChainMap; + 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); + }); +}); diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts new file mode 100644 index 0000000000..a41101e791 --- /dev/null +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -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 { + 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 { + 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 { + 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, + }); + } +} diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts new file mode 100644 index 0000000000..dc791a2d6f --- /dev/null +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts @@ -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; + let erc20Factory: ERC20Test__factory; + let token: ERC20Test; + let signer: SignerWithAddress; + let deployer: HypERC20Deployer; + let multiProvider: MultiProvider; + let coreApp: TestCoreApp; + let routerConfigMap: ChainMap; + 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; + // 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; + }); +}); diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 3055ce4bf3..fc7a5326c3 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -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 { - 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 { + const contractTypes: Partial< + Record + > = { + 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 { - 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 { - const erc20 = ERC20__factory.connect(tokenAddress, this.provider); - const [name, symbol, totalSupply, decimals] = await Promise.all([ + async fetchTokenMetadata( + type: TokenType, + tokenAddress: Address, + ): Promise { + 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 { + 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() }; } } diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index a68b2a5a9c..f12d1e77af 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -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: diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index 71ac682874..772813943c 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -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); }); }); }