@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
414 lines • 20.7 kB
JavaScript
import assert from 'assert';
import { expect } from 'chai';
import hre from 'hardhat';
import { eqAddress } from '@hyperlane-xyz/utils';
import { TestChainName, testChains } from '../consts/testChains.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomAddress, randomIsmConfig, randomMultisigIsmConfig, } from '../test/testUtils.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmIsmModule } from './EvmIsmModule.js';
import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js';
import { IsmType, } from './types.js';
describe('EvmIsmModule', async () => {
let multiProvider;
let exampleRoutingConfig;
let mailboxAddress;
let fundingAccount;
const chain = TestChainName.test4;
let factoryAddresses;
let factoryContracts;
before(async () => {
const [signer, funder] = await hre.ethers.getSigners();
fundingAccount = funder;
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const contractsMap = await new HyperlaneProxyFactoryDeployer(multiProvider).deploy(multiProvider.mapKnownChains(() => ({})));
// get addresses of factories for the chain
factoryContracts = contractsMap[chain];
factoryAddresses = Object.keys(factoryContracts).reduce((acc, key) => {
acc[key] =
contractsMap[chain][key].address;
return acc;
}, {});
// legacy HyperlaneIsmFactory is required to do a core deploy
const legacyIsmFactory = new HyperlaneIsmFactory(contractsMap, multiProvider);
// mailbox
mailboxAddress = (await new TestCoreDeployer(multiProvider, legacyIsmFactory).deployApp()).getContracts(chain).mailbox.address;
});
beforeEach(async () => {
// Reset the MultiProvider for each test
const [signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
// example routing config
exampleRoutingConfig = {
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: Object.fromEntries(testChains
.filter((c) => c !== TestChainName.test4)
.map((c) => [c, randomMultisigIsmConfig(3, 5)])),
};
});
// Helper method for create a new multiprovider with an impersonated account
async function impersonateAccount(account) {
await hre.ethers.provider.send('hardhat_impersonateAccount', [account]);
await fundingAccount.sendTransaction({
to: account,
value: hre.ethers.utils.parseEther('1.0'),
});
return MultiProvider.createTestMultiProvider({
signer: hre.ethers.provider.getSigner(account),
});
}
// Helper method to expect exactly N updates to be applied
async function expectTxsAndUpdate(ism, config, n) {
const txs = await ism.update(config);
expect(txs.length).to.equal(n);
for (const tx of txs) {
await multiProvider.sendTransaction(chain, tx);
}
}
// ism module and config for testing
let testIsm;
let testConfig;
// expect that the ISM matches the config after all tests
afterEach(async () => {
const derivedConfiig = await testIsm.read();
const normalizedDerivedConfig = normalizeConfig(derivedConfiig);
const normalizedConfig = normalizeConfig(testConfig);
assert.deepStrictEqual(normalizedDerivedConfig, normalizedConfig);
});
// create a new ISM and verify that it matches the config
async function createIsm(config) {
const ism = await EvmIsmModule.create({
chain,
config,
proxyFactoryFactories: factoryAddresses,
mailbox: mailboxAddress,
multiProvider,
});
testIsm = ism;
testConfig = config;
return { ism, initialIsmAddress: ism.serialize().deployedIsm };
}
describe('create', async () => {
it('deploys a simple ism', async () => {
const config = randomMultisigIsmConfig(3, 5);
await createIsm(config);
});
it('deploys a trusted relayer ism', async () => {
const relayer = randomAddress();
const config = {
type: IsmType.TRUSTED_RELAYER,
relayer,
};
await createIsm(config);
});
for (const type of [IsmType.ROUTING, IsmType.FALLBACK_ROUTING]) {
it(`deploys ${type} routingIsm with correct routes`, async () => {
exampleRoutingConfig.type = type;
await createIsm(exampleRoutingConfig);
});
}
it(`deploys ${IsmType.AMOUNT_ROUTING}`, async () => {
await createIsm({
type: IsmType.AMOUNT_ROUTING,
lowerIsm: randomMultisigIsmConfig(3, 5),
upperIsm: randomMultisigIsmConfig(3, 5),
threshold: 2,
});
});
for (let i = 0; i < 16; i++) {
it(`deploys a random ism config #${i}`, async () => {
const config = randomIsmConfig();
await createIsm(config);
});
}
});
describe('update', async () => {
for (const type of [IsmType.ROUTING, IsmType.FALLBACK_ROUTING]) {
beforeEach(() => {
exampleRoutingConfig.type = type;
});
it(`should skip deployment with warning if no chain metadata configured ${type}`, async () => {
// create a new ISM
const { ism } = await createIsm(exampleRoutingConfig);
// create an updated config with a domain the multiprovider doesn't have
const updatedRoutingConfig = {
...exampleRoutingConfig,
domains: {
...exampleRoutingConfig.domains,
test5: randomMultisigIsmConfig(3, 5),
},
};
// expect 0 txs, as adding test5 domain is no-op
await expectTxsAndUpdate(ism, updatedRoutingConfig, 0);
});
it(`update route in an existing ${type}`, async () => {
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// changing the type of a domain should enroll the domain
exampleRoutingConfig.domains[TestChainName.test2].type = IsmType.MESSAGE_ID_MULTISIG;
// expect 1 tx to enroll test2 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// check that the ISM address is the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`deletes route in an existing ${type}`, async () => {
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// deleting the domain should unenroll the domain
delete exampleRoutingConfig.domains[TestChainName.test3];
// expect 1 tx to unenroll test3 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`deletes route in an existing ${type} even if not in multiprovider`, async () => {
// create a new ISM
const { ism } = await createIsm(exampleRoutingConfig);
// keep track of the domains before deleting
const numDomainsBefore = Object.keys((await ism.read()).domains).length;
// deleting the domain and removing from multiprovider should unenroll the domain
delete exampleRoutingConfig.domains[TestChainName.test3];
multiProvider = multiProvider.intersect(
// remove test3 from multiprovider
testChains.filter((c) => c !== TestChainName.test3)).result;
// expect 1 tx to unenroll test3 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// domains should have decreased by 1
const numDomainsAfter = Object.keys((await ism.read()).domains).length;
expect(numDomainsBefore - 1).to.equal(numDomainsAfter);
});
it(`updates owner in an existing ${type}`, async () => {
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// change the config owner
exampleRoutingConfig.owner = randomAddress();
// expect 1 tx to transfer ownership
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`no changes to an existing ${type} means no redeployment or updates`, async () => {
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// expect 0 updates
await expectTxsAndUpdate(ism, exampleRoutingConfig, 0);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`reordering validators in an existing ${type} should not trigger a redeployment`, async () => {
// create a new ISM
const routerConfig = {
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
],
threshold: 2,
},
},
};
const { ism, initialIsmAddress } = await createIsm(routerConfig);
const updatedRouterConfig = {
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
],
threshold: 2,
},
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
],
threshold: 2,
},
},
};
// expect 0 updates
await expectTxsAndUpdate(ism, updatedRouterConfig, 0);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`update owner in an existing ${type} not owned by deployer`, async () => {
// ISM owner is not the deployer
exampleRoutingConfig.owner = randomAddress();
const originalOwner = exampleRoutingConfig.owner;
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// update the config owner and impersonate the original owner
exampleRoutingConfig.owner = randomAddress();
multiProvider = await impersonateAccount(originalOwner);
// expect 1 tx to transfer ownership
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be unchanged
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`update validators in an existing ${type}`, async () => {
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// update the validators for a domain
exampleRoutingConfig.domains[TestChainName.test2] = {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [randomAddress(), randomAddress()],
threshold: 2,
};
// expect 1 tx to update validator set for test2 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`update threshold in an existing ${type}`, async () => {
// create a new ISM
const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig);
// update the threshold for a domain
exampleRoutingConfig.domains[TestChainName.test2].threshold = 2;
// expect 1 tx to update threshold for test2 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`update threshold in an existing ${type} with Module creating using constructor`, async () => {
// create an initial ISM
const { initialIsmAddress } = await createIsm(exampleRoutingConfig);
// update the threshold for a domain
exampleRoutingConfig.domains[TestChainName.test2].threshold = 2;
// create a new IsmModule using it's constructor. Set it's deployedIsm address to the initialIsmAddr
const ism = new EvmIsmModule(multiProvider, {
chain,
config: exampleRoutingConfig,
addresses: {
...factoryAddresses,
mailbox: mailboxAddress,
deployedIsm: initialIsmAddress,
},
});
// expect 1 tx to update threshold for test2 domain
await expectTxsAndUpdate(ism, exampleRoutingConfig, 1);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
}
it(`reordering modules in an existing staticAggregationIsm should not trigger a redeployment`, async () => {
// create a new ISM
const config = {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
{
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
],
threshold: 2,
},
},
},
],
threshold: 2,
};
const { ism, initialIsmAddress } = await createIsm(config);
const updatedConfig = {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
],
threshold: 2,
},
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
},
},
{
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
],
threshold: 2,
};
// expect 0 updates
await expectTxsAndUpdate(ism, updatedConfig, 0);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
});
});
//# sourceMappingURL=EvmIsmModule.hardhat-test.js.map