UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

1,090 lines 84.9 kB
import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ethers } from 'ethers'; import hre from 'hardhat'; import sinon from 'sinon'; import { UINT_256_MAX } from 'starknet'; import { CONTRACTS_PACKAGE_VERSION, CrossCollateralRoutingFee__factory, CrossCollateralRouter__factory, ERC20Test__factory, ERC4626Test__factory, GasRouter__factory, HypERC20__factory, HypERC4626Collateral__factory, HypNative__factory, MailboxClient__factory, Mailbox__factory, MockEverclearAdapter__factory, MovableCollateralRouter__factory, TokenBridgeCctpV2__factory, } from '@hyperlane-xyz/core'; import { EvmIsmModule, HookType, IsmType, TestChainName, TokenFeeType, proxyAdmin, proxyImplementation, serializeContracts, } from '@hyperlane-xyz/sdk'; import { addressToBytes32, assert, deepCopy, eqAddress, normalizeAddressEvm, objMap, randomInt, } from '@hyperlane-xyz/utils'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { randomAddress } from '../test/testUtils.js'; import { normalizeConfig } from '../utils/ism.js'; import { EvmTokenFeeModule } from '../fee/EvmTokenFeeModule.js'; import { DEFAULT_ROUTER_KEY } from '../fee/types.js'; import { EvmWarpModule } from './EvmWarpModule.js'; import { TokenType, isMovableCollateralTokenType, } from './config.js'; import { HypTokenRouterConfigSchema, derivedHookAddress, isEverclearTokenBridgeConfig, isMovableCollateralTokenConfig, } from './types.js'; chai.use(chaiAsPromised); const { expect } = chai; const randomRemoteRouters = (n) => { const routers = {}; for (let domain = 0; domain < n; domain++) { routers[domain] = { address: randomAddress(), }; } return routers; }; describe('EvmWarpModule', async () => { const TOKEN_NAME = 'fake'; const TOKEN_SUPPLY = '100000000000000000000'; const TOKEN_DECIMALS = 18; const chain = TestChainName.test4; const domainId = 31337; let mailbox; let ismAddress; let ismFactory; let factories; let ismFactoryAddresses; let erc20Factory; let vaultFactory; let vault; let token; let feeToken; let everclearBridgeAdapterMockFactory; let everclearBridgeAdapterMock; let signer; let multiProvider; let coreApp; let routerConfigMap; let baseConfig; async function validateCoreValues(deployedToken) { expect(await deployedToken.mailbox()).to.equal(mailbox.address); expect(await deployedToken.owner()).to.equal(signer.address); } async function sendTxs(txs) { for (const tx of txs) { await multiProvider.sendTransaction(chain, tx); } } before(async () => { [signer] = await hre.ethers.getSigners(); multiProvider = MultiProvider.createTestMultiProvider({ signer }); const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); factories = await ismFactoryDeployer.deploy(multiProvider.mapKnownChains(() => ({}))); ismFactoryAddresses = serializeContracts(factories[chain]); 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); feeToken = await erc20Factory.deploy(TOKEN_NAME, TOKEN_NAME, TOKEN_SUPPLY, TOKEN_DECIMALS); vaultFactory = new ERC4626Test__factory(signer); vault = await vaultFactory.deploy(token.address, TOKEN_NAME, TOKEN_NAME); baseConfig = routerConfigMap[chain]; mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); ismAddress = await mailbox.defaultIsm(); everclearBridgeAdapterMockFactory = new MockEverclearAdapter__factory(signer); everclearBridgeAdapterMock = await everclearBridgeAdapterMockFactory.deploy(); }); const movableCollateralTypes = Object.values(TokenType).filter((t) => isMovableCollateralTokenType(t) && // CrossCollateralRouter contract too large for hardhat; covered by forge tests t !== TokenType.crossCollateral); const everclearTokenBridgeTypes = [ TokenType.ethEverclear, TokenType.collateralEverclear, ]; const assertAllowedRebalancers = async (evmERC20WarpModule, expectedRebalancers) => { const currentConfig = await evmERC20WarpModule.read(); if (isMovableCollateralTokenConfig(currentConfig)) { const currentRebalancers = Array.from(currentConfig.allowedRebalancers ?? []); expect(currentRebalancers.length).to.equal(expectedRebalancers.length); currentRebalancers.forEach((rebalancer, idx) => expect(eqAddress(rebalancer, expectedRebalancers[idx])).to.be.true); } }; const getMovableTokenConfig = (allowedRebalancers = []) => { return { [TokenType.collateral]: { ...baseConfig, type: TokenType.collateral, token: token.address, allowedRebalancers, }, [TokenType.native]: { ...baseConfig, type: TokenType.native, allowedRebalancers, }, [TokenType.nativeScaled]: { ...baseConfig, type: TokenType.nativeScaled, allowedRebalancers, }, [TokenType.crossCollateral]: { ...baseConfig, type: TokenType.crossCollateral, token: token.address, allowedRebalancers, }, }; }; const getEverclearTokenBridgeTokenConfig = () => { const chainId = multiProvider.getChainId(chain); const everclearFeeParams = { [chainId]: { deadline: Date.now(), fee: randomInt(1000), signature: '0x', }, }; // Need to "enroll" otherwise the fee won't be set const remoteRouters = { [chainId]: { address: randomAddress(), }, }; return { [TokenType.collateralEverclear]: { type: TokenType.collateralEverclear, token: token.address, ...baseConfig, everclearBridgeAddress: everclearBridgeAdapterMock.address, everclearFeeParams, outputAssets: {}, remoteRouters, }, [TokenType.ethEverclear]: { type: TokenType.ethEverclear, wethAddress: token.address, ...baseConfig, everclearBridgeAddress: everclearBridgeAdapterMock.address, everclearFeeParams, outputAssets: {}, remoteRouters, }, }; }; it('should create with a collateral config', async () => { const config = { ...baseConfig, type: TokenType.collateral, token: token.address, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type const { deployedTokenRoute } = evmERC20WarpModule.serialize(); const tokenType = await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); expect(tokenType).to.equal(TokenType.collateral); }); it('should create with a collateral vault config', async () => { const config = { type: TokenType.collateralVault, token: vault.address, ...baseConfig, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type const { deployedTokenRoute } = evmERC20WarpModule.serialize(); const tokenType = await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); expect(tokenType).to.equal(TokenType.collateralVault); // Validate onchain token values const collateralVaultContract = HypERC4626Collateral__factory.connect(deployedTokenRoute, signer); await validateCoreValues(collateralVaultContract); expect(await collateralVaultContract.vault()).to.equal(vault.address); expect(await collateralVaultContract.wrappedToken()).to.equal(token.address); }); it('should create with a synthetic config', async () => { const config = { ...baseConfig, type: TokenType.synthetic, name: TOKEN_NAME, symbol: TOKEN_NAME, decimals: TOKEN_DECIMALS, initialSupply: TOKEN_SUPPLY, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type const { deployedTokenRoute } = evmERC20WarpModule.serialize(); const tokenType = await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); expect(tokenType).to.equal(TokenType.synthetic); // Validate onchain token values const syntheticContract = HypERC20__factory.connect(deployedTokenRoute, signer); await validateCoreValues(syntheticContract); expect(await syntheticContract.name()).to.equal(TOKEN_NAME); expect(await syntheticContract.symbol()).to.equal(TOKEN_NAME); expect(await syntheticContract.decimals()).to.equal(TOKEN_DECIMALS); expect(await syntheticContract.totalSupply()).to.equal(TOKEN_SUPPLY); }); it('should create with a native config', async () => { const config = { type: TokenType.native, ...baseConfig, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); // Let's derive it's onchain token type const { deployedTokenRoute } = evmERC20WarpModule.serialize(); const tokenType = await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); expect(tokenType).to.equal(TokenType.native); // Validate onchain token values const nativeContract = HypNative__factory.connect(deployedTokenRoute, signer); await validateCoreValues(nativeContract); }); it('should create with remote routers', async () => { const numOfRouters = Math.floor(Math.random() * 10); const config = { ...baseConfig, type: TokenType.native, remoteRouters: randomRemoteRouters(numOfRouters), }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const { remoteRouters } = await evmERC20WarpModule.read(); expect(Object.keys(remoteRouters).length).to.equal(numOfRouters); }); for (const tokenType of movableCollateralTypes) { it(`should deploy the token with rebalancers when the token is of type "${tokenType}"`, async () => { const rebalancers = new Set([randomAddress(), randomAddress()]); const expectedRebalancers = Array.from(rebalancers); const config = deepCopy(getMovableTokenConfig(expectedRebalancers)[tokenType]); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); await assertAllowedRebalancers(evmERC20WarpModule, expectedRebalancers); }); } for (const tokenType of everclearTokenBridgeTypes) { it(`should create ${tokenType} token`, async () => { const config = getEverclearTokenBridgeTokenConfig()[tokenType]; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const currentConfig = await evmERC20WarpModule.read(); assert(isEverclearTokenBridgeConfig(currentConfig), `Expected token of type ${tokenType}`); expect(currentConfig.everclearBridgeAddress).to.deep.equal(config.everclearBridgeAddress); expect(currentConfig.everclearFeeParams).to.deep.equal(config.everclearFeeParams); }); it(`should deploy with multiple output assets and fee setting when the token is of type ${tokenType}`, async () => { const baseConfig = getEverclearTokenBridgeTokenConfig()[tokenType]; const domainId1 = randomInt(100, 10); const domainId2 = randomInt(1000, 100); const updatedConfig = { ...baseConfig, remoteRouters: { [domainId1]: { address: randomAddress(), }, [domainId2]: { address: randomAddress(), }, }, everclearFeeParams: { [domainId1]: { signature: '0x10', deadline: Date.now(), fee: randomInt(100), }, [domainId2]: { signature: '0x10', deadline: Date.now(), fee: randomInt(100), }, }, outputAssets: { [domainId1]: addressToBytes32(randomAddress()), [domainId2]: addressToBytes32(randomAddress()), }, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: updatedConfig, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const currentConfig = await evmERC20WarpModule.read(); assert(isEverclearTokenBridgeConfig(currentConfig), `Expected token of type ${tokenType}`); expect(currentConfig.everclearBridgeAddress).to.deep.equal(updatedConfig.everclearBridgeAddress); expect(currentConfig.everclearFeeParams).to.deep.equal(updatedConfig.everclearFeeParams); expect(currentConfig.outputAssets).to.deep.equal(updatedConfig.outputAssets); }); } describe(EvmWarpModule.prototype.update.name, async () => { const owner = randomAddress(); const ismConfigToUpdate = [ { type: IsmType.TRUSTED_RELAYER, relayer: owner, }, { type: IsmType.FALLBACK_ROUTING, owner: owner, domains: {}, }, { type: IsmType.PAUSABLE, owner: owner, paused: false, }, ethers.constants.AddressZero, ]; const hookConfigToUpdate = [ { type: HookType.PROTOCOL_FEE, beneficiary: owner, owner: owner, maxProtocolFee: '1337', protocolFee: '1337', }, { type: HookType.INTERCHAIN_GAS_PAYMASTER, owner: owner, beneficiary: owner, oracleKey: owner, overhead: {}, oracleConfig: {}, }, { type: HookType.MERKLE_TREE, }, ]; for (const interchainSecurityModule of ismConfigToUpdate) { it(`should deploy and set a new Ism (${typeof interchainSecurityModule === 'string' ? interchainSecurityModule : interchainSecurityModule.type})`, async () => { const config = { ...baseConfig, type: TokenType.native, interchainSecurityModule: ismAddress, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); const expectedConfig = { ...actualConfig, interchainSecurityModule, }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); const updatedConfig = normalizeConfig((await evmERC20WarpModule.read()).interchainSecurityModule); expect(updatedConfig).to.deep.equal(interchainSecurityModule); }); } it('should not deploy and set a new Ism if the config is the same', async () => { const config = { ...baseConfig, type: TokenType.native, interchainSecurityModule: ismAddress, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); const owner = randomAddress(); const interchainSecurityModule = { type: IsmType.PAUSABLE, owner, paused: false, }; const expectedConfig = { ...actualConfig, interchainSecurityModule, }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); const updatedConfig = normalizeConfig((await evmERC20WarpModule.read()).interchainSecurityModule); expect(updatedConfig).to.deep.equal(interchainSecurityModule); // Deploy with the same config const txs = await evmERC20WarpModule.update(expectedConfig); expect(txs.length).to.equal(0); }); it('should update and set a new Hook based on config', async () => { const config = { ...baseConfig, type: TokenType.native, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); for (const hook of hookConfigToUpdate) { const expectedConfig = { ...actualConfig, hook, }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); const updatedConfig = await evmERC20WarpModule.read(); expect(normalizeConfig(updatedConfig.hook)).to.deep.equal(hook); } }); it('should set new deployed hook mailbox to WarpConfig.owner', async () => { const config = { ...baseConfig, type: TokenType.native, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); const expectedConfig = { ...actualConfig, hook: hookConfigToUpdate.find((c) => c.type === HookType.MERKLE_TREE), }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); const updatedConfig = await evmERC20WarpModule.read(); const hook = MailboxClient__factory.connect(derivedHookAddress(updatedConfig), multiProvider.getProvider(chain)); expect(await hook.mailbox()).to.equal(expectedConfig.mailbox); }); it("should set Proxied Hook's proxyAdmins to WarpConfig.proxyAdmin", async () => { const config = { ...baseConfig, type: TokenType.native, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); const expectedConfig = { ...actualConfig, hook: hookConfigToUpdate.find((c) => c.type === HookType.INTERCHAIN_GAS_PAYMASTER), }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); const updatedConfig = await evmERC20WarpModule.read(); expect(await proxyAdmin(multiProvider.getProvider(chain), derivedHookAddress(updatedConfig))).to.equal(expectedConfig.proxyAdmin?.address); }); it('should update a mutable Ism', async () => { const ismConfig = { type: IsmType.ROUTING, owner: signer.address, domains: { '1': ismAddress, }, }; const ism = await EvmIsmModule.create({ chain, multiProvider, config: ismConfig, proxyFactoryFactories: ismFactoryAddresses, mailbox: mailbox.address, }); const { deployedIsm } = ism.serialize(); // Deploy using WarpModule const config = { ...baseConfig, type: TokenType.native, interchainSecurityModule: deployedIsm, }; const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const actualConfig = await evmERC20WarpModule.read(); const expectedConfig = { ...actualConfig, interchainSecurityModule: { type: IsmType.ROUTING, owner: randomAddress(), domains: { test2: { type: IsmType.TEST_ISM }, }, }, }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); const updatedConfig = normalizeConfig((await evmERC20WarpModule.read()).interchainSecurityModule); expect(updatedConfig).to.deep.equal(expectedConfig.interchainSecurityModule); }); it('should enroll connected routers', async () => { const config = { ...baseConfig, type: TokenType.native, ismFactoryAddresses, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, interchainSecurityModule: ismAddress, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const numOfRouters = randomInt(10, 0); await sendTxs(await evmERC20WarpModule.update({ ...config, remoteRouters: randomRemoteRouters(numOfRouters), })); const updatedConfig = await evmERC20WarpModule.read(); expect(Object.keys(updatedConfig.remoteRouters).length).to.be.equal(numOfRouters); }); it('should unenroll connected routers', async () => { const config = { ...baseConfig, type: TokenType.native, ismFactoryAddresses, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, interchainSecurityModule: ismAddress, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const numOfRouters = randomInt(10, 0); await sendTxs(await evmERC20WarpModule.update({ ...config, remoteRouters: randomRemoteRouters(numOfRouters), })); // Read config & delete remoteRouters const existingConfig = await evmERC20WarpModule.read(); for (let i = 0; i < numOfRouters; i++) { delete existingConfig.remoteRouters?.[i.toString()]; // Also remove corresponding destinationGas entry to stay consistent if (existingConfig.destinationGas) { delete existingConfig.destinationGas[i.toString()]; } await sendTxs(await evmERC20WarpModule.update(existingConfig)); const updatedConfig = await evmERC20WarpModule.read(); expect(Object.keys(updatedConfig.remoteRouters).length).to.be.equal(numOfRouters - (i + 1)); } }); it('should replace an enrollment if they are new one different, if the config lengths are the same', async () => { const config = { ...baseConfig, type: TokenType.native, ismFactoryAddresses, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, interchainSecurityModule: ismAddress, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const remoteRouters = randomRemoteRouters(1); await sendTxs(await evmERC20WarpModule.update({ ...config, remoteRouters, })); let updatedConfig = await evmERC20WarpModule.read(); expect(Object.keys(updatedConfig.remoteRouters).length).to.be.equal(1); // Try to extend with the same remoteRouters let txs = await evmERC20WarpModule.update({ ...config, remoteRouters, }); expect(txs.length).to.equal(0); await sendTxs(txs); // Try to extend with the different remoteRouters, but same length const extendedRemoteRouter = { 3: { address: randomAddress(), }, }; txs = await evmERC20WarpModule.update({ ...config, remoteRouters: extendedRemoteRouter, }); expect(txs.length).to.equal(2); await sendTxs(txs); updatedConfig = await evmERC20WarpModule.read(); expect(Object.keys(updatedConfig.remoteRouters).length).to.be.equal(1); expect(updatedConfig.remoteRouters?.['3'].address.toLowerCase()).to.be.eq(addressToBytes32(extendedRemoteRouter['3'].address)); }); it('normalizes chain-name crossCollateralRouters keys for multicollateral enroll/unenroll txs', async () => { const destinationDomain = multiProvider.getDomainId(TestChainName.test2); const keepRouterAddress = '0x1111111111111111111111111111111111111111'; const keepRouter = addressToBytes32(keepRouterAddress); const addRouterAddress = '0x2222222222222222222222222222222222222222'; const addRouter = addressToBytes32(addRouterAddress); const removeRouterAddress = '0x3333333333333333333333333333333333333333'; const removeRouter = addressToBytes32(removeRouterAddress); const module = new EvmWarpModule(multiProvider, { chain, config: { ...baseConfig, type: TokenType.crossCollateral, token: token.address, }, addresses: { deployedTokenRoute: randomAddress(), }, }); const actualConfig = { ...baseConfig, type: TokenType.crossCollateral, token: token.address, crossCollateralRouters: { [destinationDomain]: [keepRouter, removeRouter], }, }; const expectedConfig = { ...baseConfig, type: TokenType.crossCollateral, token: token.address, crossCollateralRouters: { [TestChainName.test2]: [keepRouterAddress.toUpperCase(), addRouter], }, }; const enrollTxs = module.createEnrollCrossCollateralRoutersTxs(actualConfig, expectedConfig); expect(enrollTxs.length).to.equal(1); const [enrollDomains, enrollRouters] = CrossCollateralRouter__factory.createInterface().decodeFunctionData('enrollCrossCollateralRouters', enrollTxs[0].data); expect(enrollDomains.map(Number)).to.deep.equal([destinationDomain]); expect(enrollRouters[0].toLowerCase()).to.equal(addRouter.toLowerCase()); const unenrollTxs = module.createUnenrollCrossCollateralRoutersTxs(actualConfig, expectedConfig); expect(unenrollTxs.length).to.equal(1); const [unenrollDomains, unenrollRouters] = CrossCollateralRouter__factory.createInterface().decodeFunctionData('unenrollCrossCollateralRouters', unenrollTxs[0].data); expect(unenrollDomains.map(Number)).to.deep.equal([destinationDomain]); expect(unenrollRouters[0].toLowerCase()).to.equal(removeRouter.toLowerCase()); }); it('unenrolls all crossCollateralRouters when expected config omits crossCollateralRouters', async () => { const destinationDomain = multiProvider.getDomainId(TestChainName.test2); const routerOne = addressToBytes32('0x3333333333333333333333333333333333333333'); const routerTwo = addressToBytes32('0x4444444444444444444444444444444444444444'); const module = new EvmWarpModule(multiProvider, { chain, config: { ...baseConfig, type: TokenType.crossCollateral, token: token.address, }, addresses: { deployedTokenRoute: randomAddress(), }, }); const actualConfig = { ...baseConfig, type: TokenType.crossCollateral, token: token.address, crossCollateralRouters: { [destinationDomain]: [routerOne, routerTwo], }, }; const expectedConfig = { ...baseConfig, type: TokenType.crossCollateral, token: token.address, }; const unenrollTxs = module.createUnenrollCrossCollateralRoutersTxs(actualConfig, expectedConfig); expect(unenrollTxs.length).to.equal(1); const [unenrollDomains, unenrollRouters] = CrossCollateralRouter__factory.createInterface().decodeFunctionData('unenrollCrossCollateralRouters', unenrollTxs[0].data); expect(unenrollDomains.map(Number)).to.deep.equal([ destinationDomain, destinationDomain, ]); expect(unenrollRouters.map((router) => router.toLowerCase()).sort()).to.deep.equal([routerOne.toLowerCase(), routerTwo.toLowerCase()].sort()); }); it('includes MC crossCollateralRouters domains in destination gas txs', async () => { const destinationDomain = multiProvider.getDomainId(TestChainName.test2); const enrolledRouter = addressToBytes32('0x4444444444444444444444444444444444444444'); const module = new EvmWarpModule(multiProvider, { chain, config: { ...baseConfig, type: TokenType.crossCollateral, token: token.address, }, addresses: { deployedTokenRoute: randomAddress(), }, }); const actualConfig = { ...baseConfig, type: TokenType.crossCollateral, token: token.address, destinationGas: {}, crossCollateralRouters: { [destinationDomain]: [enrolledRouter], }, }; // Config has destinationGas for test2, but no remoteRouters — only crossCollateralRouters const expectedConfig = { ...baseConfig, type: TokenType.crossCollateral, token: token.address, crossCollateralRouters: { [TestChainName.test2]: [enrolledRouter], }, destinationGas: { [TestChainName.test2]: '200000', }, }; const gasTxs = module.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig); // Should produce a tx (not throw) even without remoteRouters expect(gasTxs.length).to.equal(1); // Should use standard setDestinationGas (MC overrides _setDestinationGas) const gasRouterIface = GasRouter__factory.createInterface(); const decoded = gasRouterIface.decodeFunctionData('setDestinationGas((uint32,uint256)[])', gasTxs[0].data); expect(decoded[0].length).to.equal(1); expect(decoded[0][0].domain).to.equal(destinationDomain); expect(decoded[0][0].gas.toString()).to.equal('200000'); }); it('throws when destinationGas set but no remoteRouters or crossCollateralRouters', async () => { const module = new EvmWarpModule(multiProvider, { chain, config: { ...baseConfig, type: TokenType.collateral, token: token.address, }, addresses: { deployedTokenRoute: randomAddress(), }, }); const actualConfig = { ...baseConfig, type: TokenType.collateral, token: token.address, destinationGas: {}, }; const expectedConfig = { ...baseConfig, type: TokenType.collateral, token: token.address, destinationGas: { [TestChainName.test2]: '200000', }, }; expect(() => module.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig)).to.throw(/remoteRouters and crossCollateralRouters are empty/); }); it('should update the owner only if they are different', async () => { const config = { ...baseConfig, type: TokenType.native, ismFactoryAddresses, }; const owner = signer.address.toLowerCase(); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, interchainSecurityModule: ismAddress, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const currentConfig = await evmERC20WarpModule.read(); expect(currentConfig.owner.toLowerCase()).to.equal(owner); const newOwner = randomAddress(); await sendTxs(await evmERC20WarpModule.update({ ...config, owner: newOwner, })); const latestConfig = normalizeConfig(await evmERC20WarpModule.read()); expect(latestConfig.owner).to.equal(newOwner); // No op if the same owner const txs = await evmERC20WarpModule.update({ ...config, owner: newOwner, }); expect(txs.length).to.equal(0); }); it('should update the ProxyAdmin owner only if they are different', async () => { const config = { ...baseConfig, type: TokenType.native, }; const owner = signer.address.toLowerCase(); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, interchainSecurityModule: ismAddress, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const currentConfig = await evmERC20WarpModule.read(); expect(currentConfig.proxyAdmin?.owner.toLowerCase()).to.equal(owner); const newOwner = randomAddress(); const updatedWarpCoreConfig = { ...config, proxyAdmin: { address: currentConfig.proxyAdmin.address, owner: newOwner, }, }; await sendTxs(await evmERC20WarpModule.update(updatedWarpCoreConfig)); const latestConfig = normalizeConfig(await evmERC20WarpModule.read()); expect(latestConfig.proxyAdmin?.owner).to.equal(newOwner); // Sanity check to be sure that the owner of the warp route token has not been updated if not changed expect(latestConfig.owner).to.equal(owner); // No op if the same owner const txs = await evmERC20WarpModule.update(updatedWarpCoreConfig); expect(txs.length).to.equal(0); }); it('should update the destination gas', async () => { const domain = 3; const config = { ...baseConfig, type: TokenType.native, remoteRouters: { [domain]: { address: randomAddress(), }, }, }; // Deploy using WarpModule const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); await sendTxs(await evmERC20WarpModule.update({ ...config, destinationGas: { [domain]: '5000', }, })); const updatedConfig = await evmERC20WarpModule.read(); expect(Object.keys(updatedConfig.destinationGas).length).to.be.equal(1); expect(updatedConfig.destinationGas[domain]).to.equal('5000'); }); for (const tokenType of movableCollateralTypes) { it(`should add a new rebalancer on the deployed token if it is of type "${tokenType}"`, async () => { const initialRebalancer = randomAddress(); const config = deepCopy(getMovableTokenConfig([initialRebalancer])[tokenType]); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const expectedRebalancers = [initialRebalancer, randomAddress()]; const txs = await evmERC20WarpModule.update({ ...config, allowedRebalancers: expectedRebalancers, }); expect(txs.length).to.equal(1); await sendTxs(txs); await assertAllowedRebalancers(evmERC20WarpModule, expectedRebalancers); }); it(`should remove a rebalancer on the deployed token if the token is of type "${tokenType}"`, async () => { const rebalancerToKeep = randomAddress(); const expectedRebalancers = [rebalancerToKeep]; const rebalancers = new Set([rebalancerToKeep, randomAddress()]); const config = deepCopy(getMovableTokenConfig(Array.from(rebalancers))[tokenType]); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const txs = await evmERC20WarpModule.update({ ...config, allowedRebalancers: expectedRebalancers, }); expect(txs.length).to.equal(1); await sendTxs(txs); await assertAllowedRebalancers(evmERC20WarpModule, expectedRebalancers); }); it(`should not generate rebalancer update transactions if the address is in a different casing when token is of type "${tokenType}"`, async () => { const rebalancerToKeep = randomAddress(); const config = deepCopy(getMovableTokenConfig([rebalancerToKeep.toLowerCase()])[tokenType]); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const txs = await evmERC20WarpModule.update({ ...config, allowedRebalancers: [rebalancerToKeep], }); expect(txs.length).to.equal(0); }); it(`should add the specified addresses as rebalancing bridges for tokens of type "${tokenType}"`, async () => { const movableTokenConfigs = getMovableTokenConfig(); const config = { ...movableTokenConfigs[tokenType], remoteRouters: { [domainId]: { address: randomAddress(), }, }, }; const allowedBridgeToAdd = normalizeAddressEvm(randomAddress()); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const txs = await evmERC20WarpModule.update(HypTokenRouterConfigSchema.parse({ ...config, allowedRebalancingBridges: { [domainId]: [ { bridge: allowedBridgeToAdd, approvedTokens: [feeToken.address], }, ], }, })); // 1 tx to allow the bridge and another to approve the token expect(txs.length).to.equal(2); await sendTxs(txs); const warpTokenInstance = MovableCollateralRouter__factory.connect(evmERC20WarpModule.serialize().deployedTokenRoute, signer); const check = await warpTokenInstance.callStatic.allowedBridges(domainId); expect(check[0]).to.eql(allowedBridgeToAdd); const allowance = await feeToken.callStatic.allowance(evmERC20WarpModule.serialize().deployedTokenRoute, allowedBridgeToAdd); expect(allowance.toBigInt() === UINT_256_MAX).to.be.true; }); it(`should remove rebalancing bridges for tokens of type "${tokenType}"`, async () => { const allowedBridgeToAdd = normalizeAddressEvm(randomAddress()); const config = HypTokenRouterConfigSchema.parse({ ...getMovableTokenConfig()[tokenType], remoteRouters: { [domainId]: { address: randomAddress(), }, }, allowedRebalancingBridges: { [domainId]: [ { bridge: allowedBridgeToAdd, approvedTokens: [feeToken.address], }, ], }, }); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const txs = await evmERC20WarpModule.update(HypTokenRouterConfigSchema.parse({ ...config, allowedRebalancingBridges: { [domainId]: [], }, })); // 1 tx to remove the bridge expect(txs.length).to.equal(1); await sendTxs(txs); const warpTokenInstance = MovableCollateralRouter__factory.connect(evmERC20WarpModule.serialize().deployedTokenRoute, signer); const allowedBridges = await warpTokenInstance.callStatic.allowedBridges(domainId); expect(allowedBridges).to.be.empty; }); it(`should not generate update transactions for the allowed rebalancing bridges if the address is in a different casing when token is of type "${tokenType}"`, async () => { const movableTokenConfigs = getMovableTokenConfig(); const allowedBridgeToAdd = normalizeAddressEvm(randomAddress()); const config = HypTokenRouterConfigSchema.parse({ ...movableTokenConfigs[tokenType], remoteRouters: { [domainId]: { address: randomAddress(), }, }, allowedRebalancingBridges: { [domainId]: [ { bridge: allowedBridgeToAdd, approvedTokens: [feeToken.address], }, ], }, }); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); const txs = await evmERC20WarpModule.update(HypTokenRouterConfigSchema.parse({ ...config, allowedRebalancingBridges: { [domainId]: [ { bridge: allowedBridgeToAdd.toLowerCase(), approvedTokens: [feeToken.address], }, ], }, })); expect(txs.length).to.equal(0); }); it(`should add and remove a bridge on the deployed token if it is of type "${tokenType}" and the router map uses chain names instead of domainIds`, async () => { const bridges = [randomAddress(), randomAddress()]; const remoteRouter = randomAddress(); const config = deepCopy(getMovableTokenConfig()[tokenType]); const evmERC20WarpModule = await EvmWarpModule.create({ chain, config: { ...config, remoteRouters: { [domainId]: { address: remoteRouter, }, }, }, multiProvider, proxyFactoryFactories: ismFactoryAddresses, }); let testCase = 0; for (const bridge of bridges) { const expectedNumOfTxs = testCase === 0 ? 1 : 2; const txs = await evmERC20WarpModule.update({ ...config, allowedRebalancingBridges: { [chain]: [{ bridge }], },