UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

812 lines 37.9 kB
import { expect } from 'chai'; import hre from 'hardhat'; import sinon from 'sinon'; import { zeroAddress } from 'viem'; import { ERC20Test__factory, ERC4626Test__factory, FiatTokenTest__factory, HypERC20__factory, ISafe__factory, Mailbox__factory, MockCircleMessageTransmitter__factory, MockCircleTokenMessenger__factory, MockEverclearAdapter__factory, MockWETH__factory, PackageVersioned__factory, ProxyAdmin__factory, TokenRouter__factory, XERC20LockboxTest__factory, XERC20Test__factory, } from '@hyperlane-xyz/core'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; import { ContractVerifier, ExplorerLicenseType, TestChainName, TokenFeeType, normalizeConfig, proxyAdmin, proxyImplementation, test3, } from '@hyperlane-xyz/sdk'; import { addressToBytes32, assert, randomInt } from '@hyperlane-xyz/utils'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { VerifyContractTypes } from '../deploy/verify/types.js'; import { BPS, HALF_AMOUNT, MAX_FEE, } from '../fee/EvmTokenFeeReader.hardhat-test.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { EvmERC20WarpRouteReader, TOKEN_FEE_CONTRACT_VERSION, } from './EvmERC20WarpRouteReader.js'; import { TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; import { ContractVerificationStatus, OwnerStatus, derivedIsmAddress, } from './types.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; let factories; let erc20Factory; let token; let wethMockFactory; let weth; let signer; let deployer; let contractVerifier; let multiProvider; let coreApp; let routerConfigMap; let baseConfig; let mailbox; let evmERC20WarpRouteReader; let vault; let collateralFiatToken; let everclearBridgeAdapterMockFactory; let everclearBridgeAdapterMock; let mockCircleTokenMessenger; let mockCircleMessageTransmitter; 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); wethMockFactory = new MockWETH__factory(signer); weth = await wethMockFactory.deploy(); baseConfig = routerConfigMap[chain]; mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); deployer = new HypERC20Deployer(multiProvider); const vaultFactory = new ERC4626Test__factory(signer); vault = await vaultFactory.deploy(token.address, TOKEN_NAME, TOKEN_NAME); const fiatCollateralFactory = new FiatTokenTest__factory(signer); collateralFiatToken = await fiatCollateralFactory.deploy(TOKEN_NAME, TOKEN_NAME, TOKEN_SUPPLY, TOKEN_DECIMALS); everclearBridgeAdapterMockFactory = new MockEverclearAdapter__factory(signer); everclearBridgeAdapterMock = await everclearBridgeAdapterMockFactory.deploy(); mockCircleTokenMessenger = await new MockCircleTokenMessenger__factory(signer).deploy(token.address); mockCircleMessageTransmitter = await new MockCircleMessageTransmitter__factory(signer).deploy(token.address); }); beforeEach(async () => { // Reset the MultiProvider and create a new deployer for each test multiProvider = MultiProvider.createTestMultiProvider({ signer }); contractVerifier = new ContractVerifier(multiProvider, {}, coreBuildArtifact, ExplorerLicenseType.MIT); evmERC20WarpRouteReader = new EvmERC20WarpRouteReader(multiProvider, chain, 1, contractVerifier); deployer = new HypERC20Deployer(multiProvider); }); it('should derive a token type from contract', async () => { const typesToDerive = [ TokenType.collateral, TokenType.collateralVault, TokenType.synthetic, TokenType.native, ]; 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, 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][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)); // Check ism expect(derivedIsmAddress(derivedConfig)).to.be.equal(await mailbox.defaultIsm()); // 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 xerc20 config correctly', async () => { // Create token const xerc20Token = await new XERC20Test__factory(signer).deploy(TOKEN_NAME, TOKEN_NAME, TOKEN_SUPPLY, TOKEN_DECIMALS); // Create config const config = { [chain]: { type: TokenType.XERC20, token: xerc20Token.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].xERC20.address); for (const [key, value] of Object.entries(derivedConfig)) { const deployedValue = config[chain][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)); // Check ism expect(derivedIsmAddress(derivedConfig)).to.be.equal(await mailbox.defaultIsm()); // Check if token values matches if (derivedConfig.type === TokenType.XERC20) { expect(derivedConfig.name).to.equal(TOKEN_NAME); expect(derivedConfig.symbol).to.equal(TOKEN_NAME); expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); expect(derivedConfig.token).to.equal(xerc20Token.address); } }); it('should derive xerc20lockbox config correctly', async () => { // Create token const xerc20Lockbox = await new XERC20LockboxTest__factory(signer).deploy(TOKEN_NAME, TOKEN_NAME, TOKEN_SUPPLY, TOKEN_DECIMALS); // Create config const config = { [chain]: { type: TokenType.XERC20Lockbox, token: xerc20Lockbox.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].xERC20Lockbox.address); for (const [key, value] of Object.entries(derivedConfig)) { const deployedValue = config[chain][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)); // Check ism expect(derivedIsmAddress(derivedConfig)).to.be.equal(await mailbox.defaultIsm()); // Check if token values matches if (derivedConfig.type === TokenType.XERC20) { expect(derivedConfig.name).to.equal(TOKEN_NAME); expect(derivedConfig.symbol).to.equal(TOKEN_NAME); expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); expect(derivedConfig.token).to.equal(xerc20Lockbox.address); } }); it('should derive synthetic rebase config correctly', async () => { // Create config const config = { [chain]: { type: TokenType.syntheticRebase, collateralChainName: TestChainName.test4, hook: await mailbox.defaultHook(), name: TOKEN_NAME, symbol: TOKEN_NAME, decimals: TOKEN_DECIMALS, ...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].syntheticRebase.address); for (const [key, value] of Object.entries(derivedConfig)) { const deployedValue = config[chain][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 synthetic config correctly', async () => { // Create config const config = { [chain]: { type: TokenType.synthetic, hook: await mailbox.defaultHook(), name: TOKEN_NAME, symbol: TOKEN_NAME, decimals: TOKEN_DECIMALS, initialSupply: 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][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, }, }; // 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][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 derive collateral vault config correctly', async () => { // Create config const config = { [chain]: { type: TokenType.collateralVault, token: vault.address, ...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].collateralVault.address); assert(derivedConfig.type === TokenType.collateralVault, 'Must be collateralVault'); expect(derivedConfig.type).to.equal(config[chain].type); expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); expect(derivedConfig.owner).to.equal(config[chain].owner); expect(derivedConfig.token).to.equal(vault.address); }); it('should derive rebase collateral vault config correctly', async () => { // Create config const config = { [chain]: { type: TokenType.collateralVaultRebase, token: vault.address, ...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].collateralVaultRebase.address); assert(derivedConfig.type === TokenType.collateralVaultRebase, 'Must be collateralVaultRebase'); expect(derivedConfig.type).to.equal(config[chain].type); expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); expect(derivedConfig.owner).to.equal(config[chain].owner); expect(derivedConfig.token).to.equal(vault.address); }); // FiatTokenTest it('should derive collateral fiat token type correctly', async () => { // Create config const config = { [chain]: { type: TokenType.collateralFiat, token: collateralFiatToken.address, ...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].collateralFiat.address); assert(derivedConfig.type === TokenType.collateralFiat, `Must be ${TokenType.collateralFiat}`); expect(derivedConfig.type).to.equal(config[chain].type); expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); expect(derivedConfig.owner).to.equal(config[chain].owner); expect(derivedConfig.token).to.equal(collateralFiatToken.address); }); const getEverclearTokenBridgeConfig = () => { return { [TokenType.ethEverclear]: { type: TokenType.ethEverclear, wethAddress: weth.address, everclearBridgeAddress: everclearBridgeAdapterMock.address, everclearFeeParams: { [chain]: { deadline: Date.now(), fee: randomInt(10000000), signature: '0x', }, }, outputAssets: {}, ...baseConfig, }, [TokenType.collateralEverclear]: { type: TokenType.collateralEverclear, token: token.address, everclearBridgeAddress: everclearBridgeAdapterMock.address, everclearFeeParams: { [chain]: { deadline: Date.now(), fee: randomInt(10000000), signature: '0x', }, }, outputAssets: {}, ...baseConfig, }, }; }; for (const tokenType of [ TokenType.ethEverclear, TokenType.collateralEverclear, ]) { it(`should derive ${tokenType} token correctly`, async () => { // Create config const config = { [chain]: getEverclearTokenBridgeConfig()[tokenType], }; // Deploy with config const warpRoute = await deployer.deploy(config); // Derive config and check if each value matches const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(warpRoute[chain][tokenType].address); assert(derivedConfig.type === tokenType, `Must be ${tokenType}`); expect(derivedConfig.type).to.equal(config[chain].type); expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); expect(derivedConfig.owner).to.equal(config[chain].owner); expect(derivedConfig.everclearBridgeAddress).to.equal(everclearBridgeAdapterMock.address); if (derivedConfig.type === TokenType.collateralEverclear) { expect(derivedConfig.token).to.equal(token.address); } if (derivedConfig.type === TokenType.ethEverclear) { expect(derivedConfig.wethAddress).to.equal(weth.address); } }); } for (const cctpVersion of ['V1', 'V2']) { it(`should derive CCTP ${cctpVersion} token correctly`, async () => { const rawVersion = cctpVersion === 'V2' ? 1 : 0; await mockCircleMessageTransmitter.setVersion(rawVersion); await mockCircleTokenMessenger.setVersion(rawVersion); const tokenType = TokenType.collateralCctp; const cctpConfig = { type: tokenType, token: token.address, cctpVersion: cctpVersion, messageTransmitter: mockCircleMessageTransmitter.address, tokenMessenger: mockCircleTokenMessenger.address, urls: ['https://fake-cctp-url.com'], }; if (cctpVersion === 'V2') { cctpConfig.maxFeeBps = 1; cctpConfig.minFinalityThreshold = 1000; } // Create config const config = { [TestChainName.test4]: { ...cctpConfig, ...baseConfig, }, [TestChainName.test3]: { ...cctpConfig, ...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][tokenType].address); // delete undefined member delete config[TestChainName.test4].ownerOverrides; // check that derived is a superset of specified config expect(derivedConfig).to.deep.include(config[TestChainName.test4]); }); } it('should return 0x0 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.equal(zeroAddress); }); 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].address).to.be.equal(addressToBytes32(warpRoute[otherChain].collateral.address)); }); it('should return the contractVerificationStatus virtual config', async () => { const otherChain = TestChainName.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); // Stub isLocalRpc to bypass local rpc check const isLocalRpcStub = sinon .stub(multiProvider, 'isLocalRpc') .returns(false); // Stub getContractVerificationStatus const getContractVerificationStatus = sinon .stub(contractVerifier, 'getContractVerificationStatus') .resolves(ContractVerificationStatus.Verified); // Derive config and check if the owner is active const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteVirtualConfig(chain, warpRoute[chain].collateral.address); expect(derivedConfig.contractVerificationStatus).to.deep.equal({ [VerifyContractTypes.Proxy]: ContractVerificationStatus.Verified, [VerifyContractTypes.Implementation]: ContractVerificationStatus.Verified, [VerifyContractTypes.ProxyAdmin]: ContractVerificationStatus.Verified, }); // Restore stub getContractVerificationStatus.restore(); isLocalRpcStub.restore(); }); it('should return the ownerStatus virtual config for the proxy, implementation, and proxy admin, if they are different', async () => { const provider = multiProvider.getProvider(chain); const otherChain = TestChainName.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); // Stub isLocalRpc to bypass local rpc check const isLocalRpcStub = sinon .stub(multiProvider, 'isLocalRpc') .returns(false); // Derive config and transfer the proxy, implementation, and proxyAdmin over const warpRouteAddress = warpRoute[chain].collateral.address; const proxyAdminAddress = await proxyAdmin(provider, warpRouteAddress); await new ProxyAdmin__factory() .connect(signer) .attach(proxyAdminAddress) .transferOwnership(warpRouteAddress); const implementation = await proxyImplementation(provider, warpRouteAddress); await new HypERC20__factory() .connect(signer) .attach(implementation) .transferOwnership(mailbox.address); const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteVirtualConfig(chain, warpRouteAddress); expect(derivedConfig.ownerStatus).to.deep.equal({ [signer.address]: OwnerStatus.Active, [warpRouteAddress]: OwnerStatus.Active, [mailbox.address]: OwnerStatus.Active, }); // Restore stub isLocalRpcStub.restore(); }); it('should return a Gnosis Safe ownerStatus', async () => { const config = { [chain]: { type: TokenType.collateral, token: token.address, hook: await mailbox.defaultHook(), ...baseConfig, }, }; // Deploy with config const warpRoute = await deployer.deploy(config); const warpRouteAddress = warpRoute[chain].collateral.address; // Stub isLocalRpc to bypass local rpc check const isLocalRpcStub = sinon .stub(multiProvider, 'isLocalRpc') .returns(false); const mockOwnerManager = { getThreshold: sinon.stub().resolves(randomInt(1e4)), nonce: sinon.stub().resolves(randomInt(1e4)), }; const connectStub = sinon .stub(ISafe__factory, 'connect') .returns(mockOwnerManager); // Derive config and check if the owner is active const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteVirtualConfig(chain, warpRouteAddress); expect(derivedConfig.ownerStatus).to.deep.equal({ [signer.address]: OwnerStatus.GnosisSafe, }); // Restore stub connectStub.restore(); isLocalRpcStub.restore(); }); it('should derive token fee config correctly', async () => { const config = { [chain]: { ...baseConfig, type: TokenType.collateral, token: token.address, hook: await mailbox.defaultHook(), tokenFee: { type: TokenFeeType.LinearFee, owner: mailbox.address, token: token.address, bps: BPS, }, }, }; // 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(normalizeConfig(derivedConfig.tokenFee)).to.deep.equal(normalizeConfig({ ...config[chain].tokenFee, maxFee: MAX_FEE, halfAmount: HALF_AMOUNT, })); }); it('should return undefined fee token config if it is not set onchain', async () => { const config = { [chain]: { ...baseConfig, type: TokenType.collateral, token: token.address, hook: await mailbox.defaultHook(), }, }; // 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.tokenFee).to.be.undefined; }); it(`should return undefined fee token config if the package version is below ${TOKEN_FEE_CONTRACT_VERSION}`, async () => { const config = { [chain]: { ...baseConfig, type: TokenType.collateral, token: token.address, hook: await mailbox.defaultHook(), tokenFee: { type: TokenFeeType.LinearFee, owner: mailbox.address, token: token.address, bps: BPS, }, }, }; // Deploy with config const warpRoute = await deployer.deploy(config); const mockPackageVersioned = { PACKAGE_VERSION: sinon.stub().resolves('8.0.1'), }; const fetchPackageVersionStub = sinon .stub(PackageVersioned__factory, 'connect') .returns(mockPackageVersioned); // Derive config and check if each value matches const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(warpRoute[chain].collateral.address); // Even though we deployed a token fee, it should be undefined because the package version is below the required version. // This should never happen, but serves as a clear test expect(derivedConfig.tokenFee).to.be.undefined; fetchPackageVersionStub.restore(); }); describe('Backward compatibility for token type detection', () => { // Test table for token type detection const tokenTypeTestCases = [ { version: '9.0.0', tokenType: TokenType.native, description: 'legacy native token using estimateGas fallback', isLegacy: true, }, { version: '9.0.0', tokenType: TokenType.synthetic, description: 'legacy synthetic token using decimals() fallback', isLegacy: true, }, { version: TOKEN_FEE_CONTRACT_VERSION, tokenType: TokenType.native, description: 'modern native token using token() method', isLegacy: false, }, { version: TOKEN_FEE_CONTRACT_VERSION, tokenType: TokenType.synthetic, description: 'modern synthetic token using token() method', isLegacy: false, }, ]; for (const testCase of tokenTypeTestCases) { it(`should detect ${testCase.description} (v${testCase.version})`, async () => { const config = { [chain]: { type: testCase.tokenType, hook: await mailbox.defaultHook(), ...(testCase.tokenType === TokenType.synthetic ? { name: TOKEN_NAME, symbol: TOKEN_NAME, decimals: TOKEN_DECIMALS, initialSupply: TOKEN_SUPPLY, } : {}), ...baseConfig, }, }; const warpRoute = await deployer.deploy(config); const warpAddress = warpRoute[chain][testCase.tokenType].address; // Stub package version for legacy contracts let fetchPackageVersionStub; if (testCase.isLegacy) { const mockPackageVersioned = { PACKAGE_VERSION: sinon.stub().resolves(testCase.version), }; fetchPackageVersionStub = sinon .stub(PackageVersioned__factory, 'connect') .returns(mockPackageVersioned); } const derivedTokenType = await evmERC20WarpRouteReader.deriveTokenType(warpAddress); expect(derivedTokenType).to.equal(testCase.tokenType); // Cleanup if (fetchPackageVersionStub) { fetchPackageVersionStub.restore(); } }); } // Test table for full config derivation const fullConfigTestCases = [ { version: '9.0.0', tokenType: TokenType.native, description: 'legacy native contract', }, { version: '9.0.0', tokenType: TokenType.synthetic, description: 'legacy synthetic contract', }, ]; for (const testCase of fullConfigTestCases) { it(`should derive warp route config for ${testCase.description} (v${testCase.version})`, async () => { const config = { [chain]: { type: testCase.tokenType, hook: await mailbox.defaultHook(), ...(testCase.tokenType === TokenType.synthetic ? { name: TOKEN_NAME, symbol: TOKEN_NAME, decimals: TOKEN_DECIMALS, initialSupply: TOKEN_SUPPLY, } : {}), ...baseConfig, }, }; const warpRoute = await deployer.deploy(config); const warpAddress = warpRoute[chain][testCase.tokenType].address; // Stub package version to simulate legacy contract const mockPackageVersioned = { PACKAGE_VERSION: sinon.stub().resolves(testCase.version), }; const fetchPackageVersionStub = sinon .stub(PackageVersioned__factory, 'connect') .returns(mockPackageVersioned); const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(warpAddress); expect(derivedConfig.type).to.equal(testCase.tokenType); expect(derivedConfig.contractVersion).to.equal(testCase.version); if (testCase.tokenType === TokenType.native) { expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); } else if (testCase.tokenType === TokenType.synthetic) { expect(derivedConfig.name).to.equal(TOKEN_NAME); expect(derivedConfig.symbol).to.equal(TOKEN_NAME); expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); } fetchPackageVersionStub.restore(); }); } it('should fail when modern version contract claims v10.0.0+ but is missing token() method', async () => { const config = { [chain]: { type: TokenType.native, hook: await mailbox.defaultHook(), ...baseConfig, }, }; const warpRoute = await deployer.deploy(config); const warpAddress = warpRoute[chain].native.address; // Stub package version to claim it's modern (10.0.0+) const mockPackageVersioned = { PACKAGE_VERSION: sinon.stub().resolves(TOKEN_FEE_CONTRACT_VERSION), }; const fetchPackageVersionStub = sinon .stub(PackageVersioned__factory, 'connect') .returns(mockPackageVersioned); // Stub token() to throw error (simulating missing method) const mockTokenRouter = { token: sinon.stub().rejects(new Error('token() method not found')), }; const tokenRouterStub = sinon .stub(TokenRouter__factory, 'connect') .returns(mockTokenRouter); await expect(evmERC20WarpRouteReader.deriveTokenType(warpAddress)).to.be.rejectedWith(`Error deriving token type for token at address "${warpAddress}"`); // Cleanup fetchPackageVersionStub.restore(); tokenRouterStub.restore(); }); }); }); //# sourceMappingURL=EvmERC20WarpRouteReader.hardhat-test.js.map