@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
240 lines • 12.6 kB
JavaScript
import { expect } from 'chai';
import { constants, ethers } from 'ethers';
import hre from 'hardhat';
import { ERC20Test__factory, IERC20__factory, MinimalInterchainAccountRouter__factory, TestRecipient__factory, } from '@hyperlane-xyz/core';
import { objMap } from '@hyperlane-xyz/utils';
import { TestChainName } from '../../consts/testChains.js';
import { TestCoreDeployer } from '../../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../../deploy/HyperlaneProxyFactoryDeployer.js';
import { IcaRouterType, } from '../../ica/types.js';
import { HyperlaneIsmFactory } from '../../ism/HyperlaneIsmFactory.js';
import { IsmType } from '../../ism/types.js';
import { MultiProvider } from '../../providers/MultiProvider.js';
import { InterchainAccount } from './InterchainAccount.js';
import { InterchainAccountChecker } from './InterchainAccountChecker.js';
import { InterchainAccountDeployer } from './InterchainAccountDeployer.js';
describe('InterchainAccounts', async () => {
const localChain = TestChainName.test1;
const remoteChain = TestChainName.test2;
let signer;
let contracts;
let local;
let remote;
let multiProvider;
let coreApp;
let app;
let config;
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
const ismFactory = new HyperlaneIsmFactory(await ismFactoryDeployer.deploy(multiProvider.mapKnownChains(() => ({}))), multiProvider);
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp();
config = objMap(coreApp.getRouterConfig(signer.address), (_, config) => ({
...config,
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
owner: signer.address,
urls: ['some-url'],
},
}));
});
beforeEach(async () => {
contracts = await new InterchainAccountDeployer(multiProvider).deploy(config);
local = contracts[localChain].interchainAccountRouter;
remote = contracts[remoteChain].interchainAccountRouter;
app = new InterchainAccount(contracts, multiProvider);
});
it('checks', async () => {
const checker = new InterchainAccountChecker(multiProvider, app, config);
await checker.check();
expect(checker.violations.length).to.eql(0);
});
it('forwards calls from interchain account', async () => {
const recipientF = new TestRecipient__factory(signer);
const recipient = await recipientF.deploy();
const fooMessage = 'Test';
const data = recipient.interface.encodeFunctionData('fooBar', [
1,
fooMessage,
]);
const icaAddress = await remote['getLocalInterchainAccount(uint32,address,address,address)'](multiProvider.getDomainId(localChain), signer.address, local.address, constants.AddressZero);
const call = {
to: recipient.address,
data,
value: '0',
};
const quote = await local['quoteGasPayment(uint32)'](multiProvider.getDomainId(remoteChain));
const balanceBefore = await signer.getBalance();
const config = {
origin: localChain,
owner: signer.address,
localRouter: local.address,
};
await app.callRemote({
chain: localChain,
destination: remoteChain,
innerCalls: [call],
config,
});
const balanceAfter = await signer.getBalance();
await coreApp.processMessages();
expect(balanceAfter).to.lte(balanceBefore.sub(quote));
expect(await recipient.lastCallMessage()).to.eql(fooMessage);
expect(await recipient.lastCaller()).to.eql(icaAddress);
});
describe('MinimalInterchainAccountRouter', async () => {
it('deploys minimal router when routerType is MINIMAL', async () => {
const minimalConfig = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
routerType: IcaRouterType.MINIMAL,
}));
const minimalContracts = await new InterchainAccountDeployer(multiProvider).deploy(minimalConfig);
const router = minimalContracts[localChain].interchainAccountRouter;
expect(router.address).to.not.equal(constants.AddressZero);
// Verify it's actually a MinimalInterchainAccountRouter by checking
// implementation() exists (shared with full router) but CCIP_READ_ISM does not
const minimalInstance = MinimalInterchainAccountRouter__factory.connect(router.address, signer);
const impl = await minimalInstance.implementation();
expect(impl).to.not.equal(constants.AddressZero);
});
it('rejects minimal config with commitmentIsm set', async () => {
const badConfig = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
routerType: IcaRouterType.MINIMAL,
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
owner: signer.address,
urls: ['some-url'],
},
}));
await expect(new InterchainAccountDeployer(multiProvider).deploy(badConfig)).to.be.rejectedWith('commitmentIsm must not be set for minimal ICA router deployments');
});
it('rejects regular config without commitmentIsm', async () => {
const badConfig = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
routerType: IcaRouterType.REGULAR,
}));
await expect(new InterchainAccountDeployer(multiProvider).deploy(badConfig)).to.be.rejectedWith('commitmentIsm is required for regular ICA router deployments');
});
it('forwards calls from interchain account via minimal router', async () => {
const minimalConfig = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
routerType: IcaRouterType.MINIMAL,
}));
const minimalContracts = await new InterchainAccountDeployer(multiProvider).deploy(minimalConfig);
const minLocal = minimalContracts[localChain].interchainAccountRouter;
const minRemote = minimalContracts[remoteChain].interchainAccountRouter;
const minApp = new InterchainAccount(minimalContracts, multiProvider);
const recipientF = new TestRecipient__factory(signer);
const recipient = await recipientF.deploy();
const fooMessage = 'TestMinimal';
const data = recipient.interface.encodeFunctionData('fooBar', [
1,
fooMessage,
]);
const icaAddress = await minRemote['getLocalInterchainAccount(uint32,address,address,address)'](multiProvider.getDomainId(localChain), signer.address, minLocal.address, constants.AddressZero);
const call = {
to: recipient.address,
data,
value: '0',
};
const accountConfig = {
origin: localChain,
owner: signer.address,
localRouter: minLocal.address,
};
await minApp.callRemote({
chain: localChain,
destination: remoteChain,
innerCalls: [call],
config: accountConfig,
});
await coreApp.processMessages();
expect(await recipient.lastCallMessage()).to.eql(fooMessage);
expect(await recipient.lastCaller()).to.eql(icaAddress);
});
});
describe('feeTokenApprovals', async () => {
let feeToken;
let feeToken2;
let erc20Factory;
const mockHookAddress = '0x1234567890123456789012345678901234567890';
const mockHookAddress2 = '0xabcdef0123456789abcdef0123456789abcdef01';
before(async () => {
erc20Factory = new ERC20Test__factory(signer);
feeToken = await erc20Factory.deploy('FeeToken', 'FEE', '1000000', 18);
feeToken2 = await erc20Factory.deploy('FeeToken2', 'FEE2', '1000000', 18);
});
it('should approve fee tokens for hooks during deployment', async () => {
const feeTokenApprovals = [
{ feeToken: feeToken.address, hook: mockHookAddress },
];
const configWithApprovals = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
owner: signer.address,
urls: ['some-url'],
},
feeTokenApprovals,
}));
const contractsWithApprovals = await new InterchainAccountDeployer(multiProvider).deploy(configWithApprovals);
const localRouter = contractsWithApprovals[localChain].interchainAccountRouter;
const provider = multiProvider.getProvider(localChain);
const token = IERC20__factory.connect(feeToken.address, provider);
const allowance = await token.allowance(localRouter.address, mockHookAddress);
expect(allowance.toBigInt()).to.equal(ethers.constants.MaxUint256.toBigInt());
});
it('should approve multiple fee tokens during deployment', async () => {
const feeTokenApprovals = [
{ feeToken: feeToken.address, hook: mockHookAddress },
{ feeToken: feeToken2.address, hook: mockHookAddress2 },
];
const configWithApprovals = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
owner: signer.address,
urls: ['some-url'],
},
feeTokenApprovals,
}));
const contractsWithApprovals = await new InterchainAccountDeployer(multiProvider).deploy(configWithApprovals);
const localRouter = contractsWithApprovals[localChain].interchainAccountRouter;
const provider = multiProvider.getProvider(localChain);
const token1 = IERC20__factory.connect(feeToken.address, provider);
const token2 = IERC20__factory.connect(feeToken2.address, provider);
const allowance1 = await token1.allowance(localRouter.address, mockHookAddress);
const allowance2 = await token2.allowance(localRouter.address, mockHookAddress2);
expect(allowance1.toBigInt()).to.equal(ethers.constants.MaxUint256.toBigInt());
expect(allowance2.toBigInt()).to.equal(ethers.constants.MaxUint256.toBigInt());
});
it('should not fail when feeTokenApprovals is empty', async () => {
const configWithEmptyApprovals = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
owner: signer.address,
urls: ['some-url'],
},
feeTokenApprovals: [],
}));
const contractsWithEmptyApprovals = await new InterchainAccountDeployer(multiProvider).deploy(configWithEmptyApprovals);
expect(contractsWithEmptyApprovals[localChain].interchainAccountRouter.address).to.not.equal(constants.AddressZero);
});
it('should not fail when feeTokenApprovals is undefined', async () => {
const configWithoutApprovals = objMap(coreApp.getRouterConfig(signer.address), (_, baseConfig) => ({
...baseConfig,
commitmentIsm: {
type: IsmType.OFFCHAIN_LOOKUP,
owner: signer.address,
urls: ['some-url'],
},
}));
const contractsWithoutApprovals = await new InterchainAccountDeployer(multiProvider).deploy(configWithoutApprovals);
expect(contractsWithoutApprovals[localChain].interchainAccountRouter.address).to.not.equal(constants.AddressZero);
});
});
});
//# sourceMappingURL=accounts.hardhat-test.js.map