@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
1,090 lines • 84.9 kB
JavaScript
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 }],
},