UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

631 lines 30.9 kB
import { expect } from 'chai'; import { ethers } from 'ethers'; import hre from 'hardhat'; import { ERC20Test__factory, GasRouter__factory, LinearFee__factory, MailboxClient__factory, ProxyAdmin__factory, RateLimitedIsm__factory, RoutingFee__factory, StaticAggregationIsm__factory, TokenRouter__factory, TransparentUpgradeableProxy__factory, XERC20Test__factory, } from '@hyperlane-xyz/core'; import { ProtocolType, deepCopy, eqAddress, isZeroishAddress, objMap, } from '@hyperlane-xyz/utils'; import { TestChainName } from '../consts/testChains.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { TokenFeeType } from '../fee/types.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { IsmType, } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { EvmWarpRouteReader } from './EvmWarpRouteReader.js'; import { TokenType } from './config.js'; import { checkWarpRouteDeployConfig } from './warpCheck.js'; import { HypERC20Deployer } from './deploy.js'; import { isDepositAddressTokenConfig, } from './types.js'; const chain = TestChainName.test1; function addOverridesToConfig(config, ownerOverrides) { return Object.fromEntries(Object.entries(config).map(([chain, config]) => { return [ chain, { ...config, ownerOverrides, }, ]; })); } describe('TokenDeployer', async () => { let signer; let deployer; let multiProvider; let coreApp; let config; let token; let xerc20; let erc20; let admin; const totalSupply = '100000'; before(async () => { [signer] = await hre.ethers.getSigners(); multiProvider = MultiProvider.createTestMultiProvider({ signer }); for (const chainName of multiProvider.getKnownChainNames()) { multiProvider.metadata[chainName] = { ...multiProvider.metadata[chainName], blockExplorers: [], }; } const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); const factories = await ismFactoryDeployer.deploy(multiProvider.mapKnownChains(() => ({}))); const ismFactory = new HyperlaneIsmFactory(factories, multiProvider); coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); const routerConfigMap = coreApp.getRouterConfig(signer.address); const token = { type: TokenType.synthetic, name: chain, symbol: `u${chain}`, decimals: 18, }; config = objMap(routerConfigMap, (chain, c) => ({ ...token, ...c, })); }); beforeEach(async () => { const { name, decimals, symbol } = config[chain]; const implementation = await new XERC20Test__factory(signer).deploy(name, symbol, totalSupply, decimals); admin = await new ProxyAdmin__factory(signer).deploy(); const proxy = await new TransparentUpgradeableProxy__factory(signer).deploy(implementation.address, admin.address, XERC20Test__factory.createInterface().encodeFunctionData('initialize')); token = proxy.address; xerc20 = XERC20Test__factory.connect(token, signer); erc20 = await new ERC20Test__factory(signer).deploy(name, symbol, totalSupply, decimals); deployer = new HypERC20Deployer(multiProvider); }); it('deploys', async () => { await deployer.deploy(config); }); it('deploys a deposit-address bridge and derives its config', async () => { const depositAddress = ethers.Wallet.createRandom().address; const recipient = ethers.utils.hexZeroPad(ethers.Wallet.createRandom().address, 32); const depositConfig = { [chain]: { ...config[chain], type: TokenType.collateralDepositAddress, token: erc20.address, destinationConfigs: { [TestChainName.test2]: { [recipient]: { depositAddress, feeBps: '1000', }, }, }, }, }; const contracts = await deployer.deploy(depositConfig); const routerAddress = contracts[chain][TokenType.collateralDepositAddress].address; const reader = new EvmWarpRouteReader(multiProvider, chain); const derivedConfig = await reader.deriveWarpRouteConfig(routerAddress); expect(derivedConfig.type).to.equal(TokenType.collateralDepositAddress); if (!isDepositAddressTokenConfig(derivedConfig)) { throw new Error('Expected deposit-address token config'); } expect(derivedConfig.token).to.equal(erc20.address); expect(derivedConfig.mailbox).to.equal(ethers.constants.AddressZero); expect(derivedConfig.hook).to.equal(ethers.constants.AddressZero); expect(derivedConfig.interchainSecurityModule).to.equal(ethers.constants.AddressZero); expect(derivedConfig.remoteRouters).to.deep.equal({}); expect(derivedConfig.destinationConfigs).to.deep.equal({ [multiProvider.getDomainId(TestChainName.test2).toString()]: { [recipient.toLowerCase()]: { depositAddress, feeBps: '1000', }, }, }); }); it('deploys mixed deposit-address and router configs', async () => { const depositAddress = ethers.Wallet.createRandom().address; const recipient = ethers.utils.hexZeroPad(ethers.Wallet.createRandom().address, 32); const mixedConfig = { [TestChainName.test1]: { ...config[TestChainName.test1], type: TokenType.collateralDepositAddress, token: erc20.address, destinationConfigs: { [TestChainName.test2]: { [recipient]: { depositAddress, feeBps: '1000', }, }, }, }, [TestChainName.test2]: { ...config[TestChainName.test2], type: TokenType.synthetic, }, }; const contracts = await deployer.deploy(mixedConfig); expect(contracts[TestChainName.test1][TokenType.collateralDepositAddress] .address).to.not.equal(ethers.constants.AddressZero); expect(contracts[TestChainName.test2][TokenType.synthetic].address).to.not.equal(ethers.constants.AddressZero); const reader = new EvmWarpRouteReader(multiProvider, TestChainName.test1); const derivedConfig = await reader.deriveWarpRouteConfig(contracts[TestChainName.test1][TokenType.collateralDepositAddress] .address); if (!isDepositAddressTokenConfig(derivedConfig)) { throw new Error('Expected deposit-address token config'); } expect(derivedConfig.destinationConfigs).to.deep.equal({ [multiProvider.getDomainId(TestChainName.test2).toString()]: { [recipient.toLowerCase()]: { depositAddress, feeBps: '1000', }, }, }); }); for (const type of [ TokenType.collateral, TokenType.synthetic, TokenType.XERC20, ]) { const token = () => { switch (type) { case TokenType.XERC20: return xerc20.address; case TokenType.collateral: return erc20.address; default: return undefined; } }; describe('checkWarpRouteDeployConfig', async () => { let contractsMap; const getRouterAddress = (currentChain) => contractsMap[currentChain][config[currentChain].type].address; const getWarpCoreConfig = () => ({ tokens: Object.keys(config).map((currentChain) => ({ addressOrDenom: getRouterAddress(currentChain), chainName: currentChain, })), }); beforeEach(async () => { // @ts-expect-error - Test assigns varying token types to config config[chain] = { ...config[chain], type, token: token(), }; contractsMap = await deployer.deploy(config); }); it(`should have no violations on clean deploy of ${type}`, async () => { const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: getWarpCoreConfig(), warpDeployConfig: config, }); expect(result.isValid).to.equal(true); expect(result.violations).to.deep.equal([]); }); it('should ignore warp core config chains unknown to the multiprovider', async () => { const unknownChain = 'deprecated-chain'; const unknownWarpCoreConfig = { tokens: [ ...getWarpCoreConfig().tokens, { addressOrDenom: ethers.Wallet.createRandom().address, chainName: unknownChain, }, ], }; const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: unknownWarpCoreConfig, warpDeployConfig: config, }); expect(result.isValid).to.equal(true); expect(result.violations).to.deep.equal([]); }); it('should ignore non-EVM route members when expanding EVM configs', async () => { const cosmosChain = 'testcosmos'; if (!multiProvider.tryGetChainMetadata(cosmosChain)) { multiProvider.addChain({ chainId: 'testcosmos-1', domainId: 919191, name: cosmosChain, protocol: ProtocolType.Cosmos, rpcUrls: [{ http: 'https://cosmos.example.com' }], bech32Prefix: 'cosmos', slip44: 118, restUrls: [], grpcUrls: [], }); } const mixedWarpDeployConfig = deepCopy(config); mixedWarpDeployConfig[cosmosChain] = { type: TokenType.synthetic, mailbox: '0x0000000000000000000000000000000000000000000000000000000000000001', owner: signer.address, gas: 12345, name: 'Test Cosmos', symbol: 'TCOSM', decimals: 18, }; const cosmosRouterAddress = '0x0000000000000000000000000000000000000000000000000000000000000002'; const cosmosGas = 12345; const mixedWarpCoreConfig = { tokens: [ ...getWarpCoreConfig().tokens, { addressOrDenom: cosmosRouterAddress, chainName: cosmosChain, }, ], }; const cosmosDomain = multiProvider.getDomainId(cosmosChain); for (const currentChain of Object.keys(config)) { const tokenRouter = TokenRouter__factory.connect(getRouterAddress(currentChain), signer); await tokenRouter.enrollRemoteRouter(cosmosDomain, cosmosRouterAddress); const gasRouter = GasRouter__factory.connect(getRouterAddress(currentChain), signer); await gasRouter['setDestinationGas(uint32,uint256)'](cosmosDomain, cosmosGas); } const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: mixedWarpCoreConfig, warpDeployConfig: mixedWarpDeployConfig, }); expect(result.isValid).to.equal(true); expect(result.violations).to.deep.equal([]); }); it('should include non-EVM route members in scale validation', async () => { const cosmosChain = 'testcosmos'; if (!multiProvider.tryGetChainMetadata(cosmosChain)) { multiProvider.addChain({ chainId: 'testcosmos-1', domainId: 919191, name: cosmosChain, protocol: ProtocolType.Cosmos, rpcUrls: [{ http: 'https://cosmos.example.com' }], bech32Prefix: 'cosmos', slip44: 118, restUrls: [], grpcUrls: [], }); } const mixedWarpDeployConfig = deepCopy(config); mixedWarpDeployConfig[cosmosChain] = { type: TokenType.synthetic, mailbox: '0x0000000000000000000000000000000000000000000000000000000000000001', owner: signer.address, gas: 12345, name: 'Test Cosmos', symbol: 'TCOSM', decimals: 6, }; const mixedWarpCoreConfig = { tokens: [ ...getWarpCoreConfig().tokens, { addressOrDenom: '0x0000000000000000000000000000000000000000000000000000000000000002', chainName: cosmosChain, }, ], }; const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: mixedWarpCoreConfig, warpDeployConfig: mixedWarpDeployConfig, }); expect(result.isValid).to.equal(false); expect(result.scaleViolations).to.deep.equal([ { actual: 'invalid-or-missing', chain: 'route', expected: 'consistent-with-decimals', name: 'scale', type: 'ScaleMismatch', }, ]); }); it('should fail fast for pure non-EVM route subsets', async () => { const cosmosChain = 'testcosmos'; if (!multiProvider.tryGetChainMetadata(cosmosChain)) { multiProvider.addChain({ chainId: 'testcosmos-1', domainId: 919191, name: cosmosChain, protocol: ProtocolType.Cosmos, rpcUrls: [{ http: 'https://cosmos.example.com' }], bech32Prefix: 'cosmos', slip44: 118, restUrls: [], grpcUrls: [], }); } const cosmosOnlyWarpDeployConfig = { [cosmosChain]: { type: TokenType.synthetic, mailbox: '0x0000000000000000000000000000000000000000000000000000000000000001', owner: signer.address, gas: 12345, name: 'Test Cosmos', symbol: 'TCOSM', decimals: 18, }, }; const cosmosOnlyWarpCoreConfig = { tokens: [ { addressOrDenom: '0x0000000000000000000000000000000000000000000000000000000000000002', chainName: cosmosChain, }, ], }; try { await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: cosmosOnlyWarpCoreConfig, warpDeployConfig: cosmosOnlyWarpDeployConfig, }); expect.fail('Expected pure non-EVM route subset to reject'); } catch (error) { expect(error.message).to.contain('Warp route check requires at least one EVM chain in the selected route config'); } }); it('should ignore collateral owner changes when ownerOverrides is unset', async () => { if (type !== TokenType.XERC20) { return; } await xerc20.transferOwnership(ethers.Wallet.createRandom().address); await admin.transferOwnership(ethers.Wallet.createRandom().address); const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: getWarpCoreConfig(), warpDeployConfig: config, }); expect(result.isValid).to.equal(true); expect(result.violations.some((violation) => [ 'ownerOverrides.collateralToken', 'ownerOverrides.collateralProxyAdmin', ].includes(violation.name))).to.equal(false); }); it('should skip collateralToken override checks for non-Ownable collateral tokens', async () => { if (type !== TokenType.collateral) { return; } const overrideConfig = addOverridesToConfig(config, { collateralToken: ethers.Wallet.createRandom().address, }); const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: getWarpCoreConfig(), warpDeployConfig: overrideConfig, }); expect(result.violations.some((violation) => violation.chain === chain && violation.name === 'ownerOverrides.collateralToken')).to.equal(false); }); it('should flag explicit proxyAdmin address mismatches', async () => { const explicitProxyAdminConfig = deepCopy(config); explicitProxyAdminConfig[chain].proxyAdmin = { address: ethers.Wallet.createRandom().address, owner: explicitProxyAdminConfig[chain].owner, }; const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: getWarpCoreConfig(), warpDeployConfig: explicitProxyAdminConfig, }); expect(result.isValid).to.equal(false); expect(result.violations.some((violation) => violation.chain === chain && violation.name === 'proxyAdmin.address')).to.equal(true); }); it('should flag collateral ownership override mismatches', async () => { if (type !== TokenType.XERC20) { return; } const overrideConfig = addOverridesToConfig(config, { collateralProxyAdmin: await admin.owner(), collateralToken: await xerc20.owner(), }); await xerc20.transferOwnership(ethers.Wallet.createRandom().address); await admin.transferOwnership(ethers.Wallet.createRandom().address); const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: getWarpCoreConfig(), warpDeployConfig: overrideConfig, }); expect(result.isValid).to.equal(false); expect(result.violations.some((violation) => violation.chain === chain && violation.name === 'ownerOverrides.collateralToken')).to.equal(true); expect(result.violations.some((violation) => violation.chain === chain && violation.name === 'ownerOverrides.collateralProxyAdmin')).to.equal(true); }); it('should respect ownerOverrides.proxyAdmin over proxyAdmin.owner', async () => { const overrideConfig = addOverridesToConfig(config, { proxyAdmin: await contractsMap[chain].proxyAdmin.owner(), }); await contractsMap[chain].proxyAdmin.transferOwnership(ethers.Wallet.createRandom().address); const result = await checkWarpRouteDeployConfig({ multiProvider, warpCoreConfig: getWarpCoreConfig(), warpDeployConfig: overrideConfig, }); expect(result.isValid).to.equal(false); expect(result.violations.some((violation) => violation.chain === chain && violation.name === 'proxyAdmin.owner')).to.equal(true); }); }); describe('ERC20WarpRouterReader', async () => { let reader; let routerAddress; before(() => { reader = new EvmWarpRouteReader(multiProvider, TestChainName.test1); }); beforeEach(async () => { // @ts-expect-error - Test assigns varying token types to config config[chain] = { ...config[chain], type, token: token(), }; const warpRoute = await deployer.deploy(config); routerAddress = warpRoute[chain][type].address; }); it(`should derive HypTokenRouterConfig correctly`, async () => { const derivedConfig = await reader.deriveWarpRouteConfig(routerAddress); expect(derivedConfig.type).to.equal(config[chain].type); }); }); } describe('RateLimitedIsm with non-deployer warp owner', () => { let ismDeployer; let ismFactory; before(async () => { const pfd = new HyperlaneProxyFactoryDeployer(multiProvider); const factories = await pfd.deploy(multiProvider.mapKnownChains(() => ({}))); ismFactory = new HyperlaneIsmFactory(factories, multiProvider); }); beforeEach(() => { ismDeployer = new HypERC20Deployer(multiProvider, ismFactory); }); it('wires RateLimitedIsm and transfers all ownership when warp owner differs from deployer', async () => { const warpOwner = ethers.Wallet.createRandom().address; const warpConfig = { [chain]: { ...config[chain], type: TokenType.synthetic, owner: warpOwner, }, }; const rateLimitedIsms = { [chain]: { type: IsmType.RATE_LIMITED, maxCapacity: '86400', owner: warpOwner, }, }; const contracts = await ismDeployer.deploy(warpConfig, rateLimitedIsms); const routerAddress = contracts[chain].synthetic.address; // Token ISM should be the deployed RateLimitedIsm const tokenClient = MailboxClient__factory.connect(routerAddress, multiProvider.getProvider(chain)); const ismAddress = await tokenClient.interchainSecurityModule(); expect(isZeroishAddress(ismAddress)).to.be.false; // RateLimitedIsm owner should be warpOwner, not the deployer const rateLimitedIsm = RateLimitedIsm__factory.connect(ismAddress, multiProvider.getProvider(chain)); expect((await rateLimitedIsm.owner()).toLowerCase()).to.equal(warpOwner.toLowerCase()); // Token ownership also transferred to warpOwner const router = GasRouter__factory.connect(routerAddress, multiProvider.getProvider(chain)); expect((await router.owner()).toLowerCase()).to.equal(warpOwner.toLowerCase()); }); it('deploys RateLimitedIsm nested inside staticAggregationIsm and wires it correctly', async () => { const warpOwner = ethers.Wallet.createRandom().address; const warpConfig = { [chain]: { ...config[chain], type: TokenType.synthetic, owner: warpOwner, // interchainSecurityModule not set — passed via rateLimitedIsms }, }; const nestedIsmConfig = { type: IsmType.AGGREGATION, threshold: 2, modules: [ { type: IsmType.PAUSABLE, owner: warpOwner, paused: false, }, { type: IsmType.RATE_LIMITED, maxCapacity: '1000000000000000000', owner: warpOwner, }, ], }; const contracts = await ismDeployer.deploy(warpConfig, { [chain]: nestedIsmConfig, }); const routerAddress = contracts[chain].synthetic.address; // Token ISM must be set (not zero) const tokenClient = MailboxClient__factory.connect(routerAddress, multiProvider.getProvider(chain)); const ismAddress = await tokenClient.interchainSecurityModule(); expect(isZeroishAddress(ismAddress)).to.be.false; // ISM must be a staticAggregationIsm with threshold 2 const aggregationIsm = StaticAggregationIsm__factory.connect(ismAddress, multiProvider.getProvider(chain)); const [modules, threshold] = await aggregationIsm.modulesAndThreshold(ethers.constants.AddressZero); expect(threshold).to.equal(2); expect(modules).to.have.length(2); // Find the RateLimitedIsm module by calling recipient() on each let rateLimitedIsmAddress; for (const moduleAddress of modules) { const candidate = RateLimitedIsm__factory.connect(moduleAddress, multiProvider.getProvider(chain)); try { const recipient = await candidate.recipient(); // recipient() succeeds only on RateLimitedIsm expect(recipient.toLowerCase()).to.equal(routerAddress.toLowerCase()); rateLimitedIsmAddress = moduleAddress; break; } catch { // Not a RateLimitedIsm — continue } } expect(rateLimitedIsmAddress).to.not.be.undefined; // RateLimitedIsm owner must be warpOwner const rateLimitedIsm = RateLimitedIsm__factory.connect(rateLimitedIsmAddress, multiProvider.getProvider(chain)); expect((await rateLimitedIsm.owner()).toLowerCase()).to.equal(warpOwner.toLowerCase()); // Token ownership transferred to warpOwner const router = GasRouter__factory.connect(routerAddress, multiProvider.getProvider(chain)); expect((await router.owner()).toLowerCase()).to.equal(warpOwner.toLowerCase()); }); }); describe('TokenFee with optional token for synthetic', () => { it('should deploy LinearFee without token and resolve to router address', async () => { const syntheticConfig = { [chain]: { ...config[chain], type: TokenType.synthetic, tokenFee: { type: TokenFeeType.LinearFee, owner: signer.address, bps: 100, maxFee: 1000000000n, halfAmount: 500000000n, }, }, }; const warpRoute = await deployer.deploy(syntheticConfig); const routerAddress = warpRoute[chain].synthetic.address; const router = TokenRouter__factory.connect(routerAddress, multiProvider.getProvider(chain)); const feeRecipient = await router.feeRecipient(); expect(isZeroishAddress(feeRecipient)).to.be.false; const linearFee = LinearFee__factory.connect(feeRecipient, multiProvider.getProvider(chain)); const feeToken = await linearFee.token(); expect(eqAddress(feeToken, routerAddress)).to.be.true; }); it('should deploy RoutingFee without token and resolve to router address', async () => { const syntheticConfig = { [chain]: { ...config[chain], type: TokenType.synthetic, tokenFee: { type: TokenFeeType.RoutingFee, owner: signer.address, feeContracts: { [TestChainName.test2]: { type: TokenFeeType.LinearFee, owner: signer.address, bps: 100, maxFee: 1000000000n, halfAmount: 500000000n, }, }, }, }, }; const warpRoute = await deployer.deploy(syntheticConfig); const routerAddress = warpRoute[chain].synthetic.address; const router = TokenRouter__factory.connect(routerAddress, multiProvider.getProvider(chain)); const feeRecipient = await router.feeRecipient(); expect(isZeroishAddress(feeRecipient)).to.be.false; const routingFee = RoutingFee__factory.connect(feeRecipient, multiProvider.getProvider(chain)); const feeToken = await routingFee.token(); expect(eqAddress(feeToken, routerAddress)).to.be.true; }); }); }); //# sourceMappingURL=deploy.hardhat-test.js.map