@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
734 lines • 31.7 kB
JavaScript
import { expect } from 'chai';
import { constants } from 'ethers';
import { DEFAULT_ROUTER_KEY, TokenFeeType, } from '../fee/types.js';
import { HookType } from '../hook/types.js';
import { IsmType } from '../ism/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { test1, test2 } from '../consts/testChains.js';
import { TokenType } from './config.js';
import { filterWarpCoreConfigMapByChains, getChainsFromWarpCoreConfig, normalizeWarpDeployConfigForCheck, resolveTokenFeeAddress, transformConfigToCheck, warpCoreConfigMatchesChains, } from './configUtils.js';
import { TokenStandard } from './TokenStandard.js';
function buildMultiProvider() {
return new MultiProvider({
[test1.name]: test1,
[test2.name]: test2,
});
}
describe('configUtils', () => {
describe(transformConfigToCheck.name, () => {
const ADDRESS = '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359';
const testCases = [
{
msg: 'It should remove the address and ownerOverrides fields from the config',
input: {
ownerOverrides: {
owner: ADDRESS,
},
hook: {
type: HookType.AMOUNT_ROUTING,
address: ADDRESS,
},
interchainSecurityModule: {
type: IsmType.AGGREGATION,
address: ADDRESS,
modules: [
{
type: IsmType.AMOUNT_ROUTING,
address: ADDRESS,
},
{
type: IsmType.FALLBACK_ROUTING,
address: ADDRESS,
},
],
},
},
expected: {
hook: {
type: HookType.AMOUNT_ROUTING,
},
interchainSecurityModule: {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.AMOUNT_ROUTING,
},
{
type: IsmType.FALLBACK_ROUTING,
},
],
},
scale: { numerator: 1n, denominator: 1n },
},
},
{
msg: 'It should not remove the address property from the remoteRouters object',
input: {
interchainSecurityModule: {
address: ADDRESS,
type: 'NULL',
},
remoteRouters: {
'1': {
address: ADDRESS,
},
},
},
expected: {
interchainSecurityModule: {
type: 'NULL',
},
remoteRouters: {
'1': {
address: ADDRESS,
},
},
scale: { numerator: 1n, denominator: 1n },
},
},
{
msg: 'It should preserve the proxyAdmin address property for explicit checks',
input: {
hook: {
address: ADDRESS,
type: HookType.MERKLE_TREE,
},
proxyAdmin: {
address: ADDRESS,
owner: ADDRESS,
},
},
expected: {
hook: {
type: HookType.MERKLE_TREE,
},
proxyAdmin: {
address: ADDRESS,
owner: ADDRESS,
},
scale: { numerator: 1n, denominator: 1n },
},
},
{
msg: 'It should sort out of order modules and validator arrays',
expected: {
bsc: {
decimals: 6,
interchainSecurityModule: {
type: 'defaultFallbackRoutingIsm',
owner: '0xe472f601aeeebeafbbd3a6fd9a788966011ad1df',
domains: {
milkyway: {
threshold: '1',
modules: [
{
threshold: 3,
type: 'merkleRootMultisigIsm',
validators: [
'0x55010624d5e239281d0850dc7915b78187e8bc0e',
'0x56fa9ac314ad49836ffb35918043d6b2dec304fb',
'0x9985e0c6df8e25b655b46a317af422f5e7756875',
'0x9ecf299947b030f9898faf328e5edbf77b13e974',
'0xb69c0d1aacd305edeca88b482b9dd9657f3a8b5c',
],
},
{
threshold: 3,
type: 'messageIdMultisigIsm',
validators: [
'0x55010624d5e239281d0850dc7915b78187e8bc0e',
'0x56fa9ac314ad49836ffb35918043d6b2dec304fb',
'0x9985e0c6df8e25b655b46a317af422f5e7756875',
'0x9ecf299947b030f9898faf328e5edbf77b13e974',
'0xb69c0d1aacd305edeca88b482b9dd9657f3a8b5c',
],
},
],
},
},
},
name: 'MilkyWay',
owner: '0xe472f601aeeebeafbbd3a6fd9a788966011ad1df',
symbol: 'MILK',
type: 'synthetic',
},
milkyway: {
foreignDeployment: '0x726f757465725f61707000000000000000000000000000010000000000000000',
owner: 'milk169dcaz397j75tjfpl6ykm23dfrv39dqd58lsag',
type: 'native',
},
scale: { numerator: 1n, denominator: 1n },
},
input: {
bsc: {
decimals: 6,
interchainSecurityModule: {
type: 'defaultFallbackRoutingIsm',
owner: '0xE472F601aeEeBEafbbd3a6FD9A788966011AD1Df',
domains: {
milkyway: {
threshold: '1',
modules: [
{
threshold: 3,
type: 'messageIdMultisigIsm',
validators: [
'0x9985e0c6df8e25b655b46a317af422f5e7756875',
'0x55010624d5e239281d0850dc7915b78187e8bc0e',
'0x9ecf299947b030f9898faf328e5edbf77b13e974',
'0x56fa9ac314ad49836ffb35918043d6b2dec304fb',
'0xb69c0d1aacd305edeca88b482b9dd9657f3a8b5c',
],
},
{
threshold: 3,
type: 'merkleRootMultisigIsm',
validators: [
'0x9985e0c6df8e25b655b46a317af422f5e7756875',
'0x55010624d5e239281d0850dc7915b78187e8bc0e',
'0x9ecf299947b030f9898faf328e5edbf77b13e974',
'0x56fa9ac314ad49836ffb35918043d6b2dec304fb',
'0xb69c0d1aacd305edeca88b482b9dd9657f3a8b5c',
],
},
],
},
},
},
name: 'MilkyWay',
owner: '0xE472F601aeEeBEafbbd3a6FD9A788966011AD1Df',
symbol: 'MILK',
type: 'synthetic',
},
milkyway: {
foreignDeployment: '0x726f757465725f61707000000000000000000000000000010000000000000000',
owner: 'milk169dcaz397j75tjfpl6ykm23dfrv39dqd58lsag',
type: 'native',
},
},
},
];
for (const { msg, input, expected } of testCases) {
it(msg, () => {
const transformedObj = transformConfigToCheck(input);
expect(transformedObj).to.eql(expected);
});
}
it('normalizes plain number scale to {numerator, denominator} bigint', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
scale: 1000000000000,
});
expect(transformedObj.scale).to.eql({
numerator: 1000000000000n,
denominator: 1n,
});
});
it('normalizes {number, number} scale to {bigint, bigint}', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1, denominator: 1000000000000 },
});
expect(transformedObj.scale).to.eql({
numerator: 1n,
denominator: 1000000000000n,
});
});
it('normalizes undefined scale to identity {1n, 1n}', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
});
expect(transformedObj.scale).to.eql({
numerator: 1n,
denominator: 1n,
});
});
it('normalizes LinearFee maxFee/halfAmount so equivalent bps configs compare equal', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
tokenFee: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
maxFee: 999n,
halfAmount: 123n,
},
});
expect(transformedObj).to.eql({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1n, denominator: 1n },
tokenFee: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
},
});
});
it('normalizes OffchainQuotedLinearFee maxFee/halfAmount so equivalent bps configs compare equal', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
tokenFee: {
type: TokenFeeType.OffchainQuotedLinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
maxFee: 999n,
halfAmount: 123n,
quoteSigners: [ADDRESS],
},
});
expect(transformedObj).to.eql({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1n, denominator: 1n },
tokenFee: {
type: TokenFeeType.OffchainQuotedLinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
quoteSigners: [ADDRESS],
},
});
});
it('normalizes RoutingFee maxFee/halfAmount recursively for feeContracts', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
tokenFee: {
type: TokenFeeType.RoutingFee,
owner: ADDRESS,
token: ADDRESS,
maxFee: 1n,
halfAmount: 2n,
feeContracts: {
ethereum: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
maxFee: 3n,
halfAmount: 4n,
},
},
},
});
expect(transformedObj).to.eql({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1n, denominator: 1n },
tokenFee: {
type: TokenFeeType.RoutingFee,
owner: ADDRESS,
token: ADDRESS,
feeContracts: {
ethereum: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
},
},
},
});
});
it('normalizes CCRF router-keyed fee contracts recursively', () => {
const ROUTER_KEY = '0x1111111111111111111111111111111111111111111111111111111111111111';
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
tokenFee: {
type: TokenFeeType.CrossCollateralRoutingFee,
owner: ADDRESS,
feeContracts: {
ethereum: {
[DEFAULT_ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 200n,
maxFee: 3n,
halfAmount: 4n,
},
[ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
maxFee: 5n,
halfAmount: 6n,
},
},
},
},
});
expect(transformedObj).to.eql({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1n, denominator: 1n },
tokenFee: {
type: TokenFeeType.CrossCollateralRoutingFee,
owner: ADDRESS,
feeContracts: {
ethereum: {
[DEFAULT_ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 200n,
},
[ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 300n,
},
},
},
},
});
});
it('keeps only populated CCRF router entries during normalization', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
tokenFee: {
type: TokenFeeType.CrossCollateralRoutingFee,
owner: ADDRESS,
feeContracts: {
ethereum: {
[DEFAULT_ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 200n,
},
},
},
},
});
expect(transformedObj).to.eql({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1n, denominator: 1n },
tokenFee: {
type: TokenFeeType.CrossCollateralRoutingFee,
owner: ADDRESS,
feeContracts: {
ethereum: {
[DEFAULT_ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 200n,
},
},
},
},
});
});
it('normalizes RoutingFee feeContracts when both destination and nested fee contracts are provided', () => {
const transformedObj = transformConfigToCheck({
type: TokenType.collateral,
token: ADDRESS,
tokenFee: {
type: TokenFeeType.RoutingFee,
owner: ADDRESS,
token: ADDRESS,
feeContracts: {
ethereum: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 100n,
},
},
},
});
expect(transformedObj).to.eql({
type: TokenType.collateral,
token: ADDRESS,
scale: { numerator: 1n, denominator: 1n },
tokenFee: {
type: TokenFeeType.RoutingFee,
owner: ADDRESS,
token: ADDRESS,
feeContracts: {
ethereum: {
type: TokenFeeType.LinearFee,
owner: ADDRESS,
token: ADDRESS,
bps: 100n,
},
},
},
});
});
});
describe(resolveTokenFeeAddress.name, () => {
const ROUTER_ADDRESS = '0x1234567890123456789012345678901234567890';
const OWNER_ADDRESS = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd';
const COLLATERAL_TOKEN = '0x9999999999999999999999999999999999999999';
const syntheticConfig = {
type: TokenType.synthetic,
};
const collateralConfig = {
type: TokenType.collateral,
token: COLLATERAL_TOKEN,
};
const nativeConfig = {
type: TokenType.native,
};
it('should resolve token to router address for synthetic tokens', () => {
const input = {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 100,
};
const result = resolveTokenFeeAddress(input, ROUTER_ADDRESS, syntheticConfig);
expect(result.token).to.equal(ROUTER_ADDRESS);
expect(result.owner).to.equal(OWNER_ADDRESS);
});
it('should resolve token to collateral address for collateral tokens', () => {
const input = {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 100,
};
const result = resolveTokenFeeAddress(input, ROUTER_ADDRESS, collateralConfig);
expect(result.token).to.equal(COLLATERAL_TOKEN);
});
it('should resolve token to AddressZero for native tokens', () => {
const input = {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 100,
};
const result = resolveTokenFeeAddress(input, ROUTER_ADDRESS, nativeConfig);
expect(result.token).to.equal(constants.AddressZero);
});
it('should resolve nested feeContracts tokens for RoutingFee', () => {
const input = {
type: TokenFeeType.RoutingFee,
owner: OWNER_ADDRESS,
feeContracts: {
ethereum: {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 100,
},
arbitrum: {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 50,
},
},
};
const result = resolveTokenFeeAddress(input, ROUTER_ADDRESS, syntheticConfig);
expect(result.token).to.equal(ROUTER_ADDRESS);
expect(result.type).to.equal(TokenFeeType.RoutingFee);
expect(result.feeContracts.ethereum.token).to.equal(ROUTER_ADDRESS);
expect(result.feeContracts.arbitrum.token).to.equal(ROUTER_ADDRESS);
});
it('should handle RoutingFee with empty feeContracts', () => {
const input = {
type: TokenFeeType.RoutingFee,
owner: OWNER_ADDRESS,
feeContracts: {},
};
const result = resolveTokenFeeAddress(input, ROUTER_ADDRESS, syntheticConfig);
expect(result.token).to.equal(ROUTER_ADDRESS);
expect(result.type).to.equal(TokenFeeType.RoutingFee);
});
it('should resolve token for nested cross collateral feeContracts', () => {
const ROUTER_KEY = '0x1111111111111111111111111111111111111111111111111111111111111111';
const input = {
type: TokenFeeType.CrossCollateralRoutingFee,
owner: OWNER_ADDRESS,
feeContracts: {
ethereum: {
[DEFAULT_ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 100n,
},
[ROUTER_KEY]: {
type: TokenFeeType.LinearFee,
owner: OWNER_ADDRESS,
bps: 200n,
},
},
},
};
const result = resolveTokenFeeAddress(input, ROUTER_ADDRESS, syntheticConfig);
expect(result.feeContracts.ethereum[DEFAULT_ROUTER_KEY]?.token).to.equal(ROUTER_ADDRESS);
expect(result.feeContracts.ethereum[ROUTER_KEY]?.token).to.equal(ROUTER_ADDRESS);
});
});
describe(normalizeWarpDeployConfigForCheck.name, () => {
const ADDRESS = '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359';
const OTHER_ADDRESS = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
it('normalizes OFT configs to sentinel router state for checks', () => {
const warpDeployConfig = {
[test1.name]: {
decimals: 6,
destinationGas: { [test2.name]: '12345' },
domainMappings: { [test2.name]: 30110 },
extraOptions: '0x',
hook: OTHER_ADDRESS,
interchainSecurityModule: OTHER_ADDRESS,
mailbox: ADDRESS,
name: 'USDT',
oft: OTHER_ADDRESS,
owner: ADDRESS,
remoteRouters: {
[test2.name]: {
address: OTHER_ADDRESS,
},
},
symbol: 'USDT',
token: ADDRESS,
type: TokenType.collateralOft,
},
};
const normalized = normalizeWarpDeployConfigForCheck({
multiProvider: buildMultiProvider(),
warpDeployConfig,
});
expect(normalized[test1.name]).to.deep.equal({
decimals: 6,
destinationGas: undefined,
domainMappings: { [test2.domainId]: 30110 },
extraOptions: undefined,
hook: constants.AddressZero,
interchainSecurityModule: constants.AddressZero,
mailbox: constants.AddressZero,
name: 'USDT',
oft: OTHER_ADDRESS,
owner: ADDRESS,
remoteRouters: {},
symbol: 'USDT',
token: ADDRESS,
type: TokenType.collateralOft,
});
});
it('preserves non-empty OFT extraOptions', () => {
const warpDeployConfig = {
[test1.name]: {
decimals: 6,
domainMappings: { [test2.name]: 30110 },
extraOptions: '0xdeadbeef',
hook: OTHER_ADDRESS,
interchainSecurityModule: OTHER_ADDRESS,
mailbox: ADDRESS,
name: 'USDT',
oft: OTHER_ADDRESS,
owner: ADDRESS,
symbol: 'USDT',
token: ADDRESS,
type: TokenType.collateralOft,
},
};
const normalized = normalizeWarpDeployConfigForCheck({
multiProvider: buildMultiProvider(),
warpDeployConfig,
});
expect(normalized[test1.name]).to.deep.include({
extraOptions: '0xdeadbeef',
});
});
it('leaves non-OFT configs unchanged', () => {
const warpDeployConfig = {
[test1.name]: {
decimals: 18,
mailbox: ADDRESS,
name: 'TOKEN',
owner: ADDRESS,
symbol: 'TKN',
type: TokenType.synthetic,
},
};
const normalized = normalizeWarpDeployConfigForCheck({
multiProvider: buildMultiProvider(),
warpDeployConfig,
});
expect(normalized).to.deep.equal(warpDeployConfig);
});
});
const buildWarpCoreConfig = (chainNames) => ({
tokens: chainNames.map((chainName, index) => ({
chainName,
standard: TokenStandard.EvmHypSynthetic,
decimals: 18,
symbol: `TKN${index + 1}`,
name: `Token ${index + 1}`,
addressOrDenom: `0x${(index + 1).toString(16).padStart(40, '0')}`,
})),
});
describe('getChainsFromWarpCoreConfig', () => {
it('should return chain names from tokens', () => {
const config = buildWarpCoreConfig(['ethereum', 'arbitrum', 'optimism']);
const result = getChainsFromWarpCoreConfig(config);
expect(result).to.deep.equal(['ethereum', 'arbitrum', 'optimism']);
});
it('should return empty array for empty tokens', () => {
const config = buildWarpCoreConfig([]);
const result = getChainsFromWarpCoreConfig(config);
expect(result).to.deep.equal([]);
});
});
describe('warpCoreConfigMatchesChains', () => {
const config = buildWarpCoreConfig(['ethereum', 'arbitrum', 'optimism']);
it('should return true when all chains are present', () => {
expect(warpCoreConfigMatchesChains(config, ['ethereum', 'arbitrum'])).to
.be.true;
});
it('should return true for single chain match', () => {
expect(warpCoreConfigMatchesChains(config, ['optimism'])).to.be.true;
});
it('should return false when a chain is missing', () => {
expect(warpCoreConfigMatchesChains(config, ['ethereum', 'polygon'])).to.be
.false;
});
it('should return true for empty chains array', () => {
expect(warpCoreConfigMatchesChains(config, [])).to.be.true;
});
});
describe('filterWarpCoreConfigMapByChains', () => {
const configMap = {
'ETH/ethereum-arbitrum': buildWarpCoreConfig(['ethereum', 'arbitrum']),
'ETH/ethereum-optimism': buildWarpCoreConfig(['ethereum', 'optimism']),
'USDC/arbitrum-optimism': buildWarpCoreConfig(['arbitrum', 'optimism']),
};
it('should filter to routes containing all specified chains', () => {
const result = filterWarpCoreConfigMapByChains(configMap, [
'ethereum',
'arbitrum',
]);
expect(Object.keys(result)).to.deep.equal(['ETH/ethereum-arbitrum']);
});
it('should return multiple routes when chains match multiple', () => {
const result = filterWarpCoreConfigMapByChains(configMap, ['ethereum']);
expect(Object.keys(result).sort()).to.deep.equal([
'ETH/ethereum-arbitrum',
'ETH/ethereum-optimism',
]);
});
it('should return empty object when no routes match', () => {
const result = filterWarpCoreConfigMapByChains(configMap, ['polygon']);
expect(Object.keys(result)).to.have.lengthOf(0);
});
it('should return all routes for empty chains array', () => {
const result = filterWarpCoreConfigMapByChains(configMap, []);
expect(Object.keys(result)).to.have.lengthOf(3);
});
});
});
//# sourceMappingURL=configUtils.test.js.map