@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
371 lines • 19.2 kB
JavaScript
import { expect } from 'chai';
import { constants } from 'ethers';
import hre from 'hardhat';
import { ERC20Test__factory } from '@hyperlane-xyz/core';
import { assert } from '@hyperlane-xyz/utils';
import { TestChainName } from '../consts/testChains.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomInt } from '../test/testUtils.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmTokenFeeDeployer } from './EvmTokenFeeDeployer.js';
import { EvmTokenFeeReader } from './EvmTokenFeeReader.js';
import { DEFAULT_ROUTER_KEY, TokenFeeConfigSchema, TokenFeeType, } from './types.js';
import { ASSUMED_MAX_AMOUNT_FOR_ZERO_SUPPLY, convertToBps } from './utils.js';
// eslint-disable-next-line jest/no-export -- test fixtures shared across test files
export const MAX_FEE = 115792089237316195423570985008687907853269n;
// eslint-disable-next-line jest/no-export -- test fixtures shared across test files
export const HALF_AMOUNT = 578960446186580977117854925043439539266340n;
// eslint-disable-next-line jest/no-export -- test fixtures shared across test files
export const BPS = convertToBps(MAX_FEE, HALF_AMOUNT);
describe('EvmTokenFeeReader', () => {
let multiProvider;
let signer;
let token;
const TOKEN_TOTAL_SUPPLY = '100000000000000000000';
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const factory = new ERC20Test__factory(signer);
token = await factory.deploy('fake', 'FAKE', TOKEN_TOTAL_SUPPLY, 18);
await token.deployed();
});
describe('LinearFee', () => {
it('should read the token fee config', async () => {
const config = TokenFeeConfigSchema.parse({
type: TokenFeeType.LinearFee,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
token: token.address,
owner: signer.address,
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: config,
});
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const tokenFee = deployedContracts[TestChainName.test2][TokenFeeType.LinearFee];
const onchainConfig = await reader.deriveTokenFeeConfig({
address: tokenFee.address,
});
expect(normalizeConfig(onchainConfig)).to.deep.equal(normalizeConfig({
...config,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
}));
});
it('should convert maxFee and halfAmount to bps', async () => {
const maxFee = BigInt(randomInt(2, 100_000_000));
const halfAmount = maxFee * 5n;
const config = {
type: TokenFeeType.LinearFee,
owner: signer.address,
token: token.address,
maxFee,
halfAmount,
bps: convertToBps(maxFee, halfAmount),
};
const parsedConfig = TokenFeeConfigSchema.parse(config);
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
await deployer.deploy({
[TestChainName.test3]: parsedConfig,
});
const convertedBps = convertToBps(maxFee, halfAmount);
expect(convertedBps).to.equal(BPS);
});
it('should be able to convert bps to maxFee and halfAmount, and back', async () => {
const bps = randomInt(1, 10_000);
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const { maxFee: convertedMaxFee, halfAmount: convertedHalfAmount } = reader.convertFromBps(bps);
const convertedBps = convertToBps(convertedMaxFee, convertedHalfAmount);
expect(convertedBps).to.equal(bps);
});
it('should round-trip fractional bps values', async () => {
const fractionalValues = [0.5, 1.5, 2.5, 8.3333];
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
for (const bps of fractionalValues) {
const { maxFee, halfAmount } = reader.convertFromBps(bps);
const roundTripped = convertToBps(maxFee, halfAmount);
expect(roundTripped).to.equal(bps);
}
});
it('should use constant divisor for consistent fee derivation', async () => {
const bps = 8;
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const { maxFee, halfAmount } = reader.convertFromBps(bps);
expect(maxFee > 0n).to.be.true;
expect(halfAmount > 0n).to.be.true;
const expectedMaxFee = BigInt(constants.MaxUint256.toString()) /
ASSUMED_MAX_AMOUNT_FOR_ZERO_SUPPLY;
expect(maxFee).to.equal(expectedMaxFee);
const convertedBps = convertToBps(maxFee, halfAmount);
expect(convertedBps).to.equal(bps);
});
it('should reject bps = 0 to prevent division by zero', () => {
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
expect(() => reader.convertFromBps(0)).to.throw('bps must be > 0 to prevent division by zero');
});
});
describe('OffchainQuotedLinearFee', () => {
it('should read the offchain quoted linear fee config', async () => {
const config = TokenFeeConfigSchema.parse({
type: TokenFeeType.OffchainQuotedLinearFee,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
token: token.address,
owner: signer.address,
quoteSigners: [signer.address],
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: config,
});
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const contract = deployedContracts[TestChainName.test2][TokenFeeType.OffchainQuotedLinearFee];
const onchainConfig = await reader.deriveTokenFeeConfig({
address: contract.address,
});
expect(normalizeConfig(onchainConfig)).to.deep.equal(normalizeConfig(config));
});
it('should read multiple quote signers', async () => {
const [, otherSigner] = await hre.ethers.getSigners();
const config = TokenFeeConfigSchema.parse({
type: TokenFeeType.OffchainQuotedLinearFee,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
token: token.address,
owner: signer.address,
quoteSigners: [signer.address, otherSigner.address],
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: config,
});
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const contract = deployedContracts[TestChainName.test2][TokenFeeType.OffchainQuotedLinearFee];
const onchainConfig = await reader.deriveTokenFeeConfig({
address: contract.address,
});
expect(onchainConfig.type).to.equal(TokenFeeType.OffchainQuotedLinearFee);
if (onchainConfig.type === TokenFeeType.OffchainQuotedLinearFee) {
expect(onchainConfig.quoteSigners).to.have.lengthOf(2);
expect(onchainConfig.quoteSigners).to.include(signer.address);
expect(onchainConfig.quoteSigners).to.include(otherSigner.address);
}
});
});
describe('RoutingFee', () => {
it('should be able to derive a routing fee config and its sub fees', async () => {
const routingFeeConfig = {
type: TokenFeeType.RoutingFee,
owner: signer.address,
token: token.address,
feeContracts: {
[TestChainName.test2]: {
owner: signer.address,
token: token.address,
type: TokenFeeType.LinearFee,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
},
[TestChainName.test3]: {
owner: signer.address,
token: token.address,
type: TokenFeeType.LinearFee,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
},
},
};
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: routingFeeConfig,
});
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const destinations = [
multiProvider.getDomainId(TestChainName.test2),
multiProvider.getDomainId(TestChainName.test3),
];
const routingFee = await reader.deriveTokenFeeConfig({
address: deployedContracts[TestChainName.test2][TokenFeeType.RoutingFee]
.address,
routingDestinations: destinations,
});
expect(normalizeConfig(routingFee)).to.deep.equal(normalizeConfig(routingFeeConfig));
});
it('should derive routing fee config without routingDestinations', async () => {
const routingFeeConfig = TokenFeeConfigSchema.parse({
type: TokenFeeType.RoutingFee,
owner: signer.address,
token: token.address,
feeContracts: {
[TestChainName.test2]: {
owner: signer.address,
token: token.address,
type: TokenFeeType.LinearFee,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
},
},
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: routingFeeConfig,
});
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const routingFee = await reader.deriveTokenFeeConfig({
address: deployedContracts[TestChainName.test2][TokenFeeType.RoutingFee]
.address,
});
assert(routingFee.type === TokenFeeType.RoutingFee, `Must be ${TokenFeeType.RoutingFee}`);
expect(routingFee.type).to.equal(TokenFeeType.RoutingFee);
expect(routingFee.owner).to.equal(signer.address);
expect(routingFee.token).to.equal(token.address);
expect(Object.keys(routingFee.feeContracts)).to.have.length(0);
});
it('should derive cross collateral routing fee config from DEFAULT_ROUTER entries', async () => {
const linearFeeConfig = TokenFeeConfigSchema.parse({
type: TokenFeeType.LinearFee,
owner: signer.address,
token: token.address,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: linearFeeConfig,
});
const crossCollateralRoutingFeeFactory = await hre.ethers.getContractFactory('MockCrossCollateralRoutingFee', signer);
const crossCollateralRoutingFee = await crossCollateralRoutingFeeFactory.deploy(signer.address);
await crossCollateralRoutingFee.deployed();
const destination = multiProvider.getDomainId(TestChainName.test3);
const destination2 = multiProvider.getDomainId(TestChainName.test4);
const defaultRouter = await crossCollateralRoutingFee.DEFAULT_ROUTER();
await crossCollateralRoutingFee.setCrossCollateralRouterFeeContracts([destination, destination2], [defaultRouter, defaultRouter], [
deployedContracts[TestChainName.test2][TokenFeeType.LinearFee]
.address,
deployedContracts[TestChainName.test2][TokenFeeType.LinearFee]
.address,
]);
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const routingFee = await reader.deriveTokenFeeConfig({
address: crossCollateralRoutingFee.address,
crossCollateralRouters: {
[destination]: [],
[destination2]: [],
},
});
expect(routingFee.type).to.equal(TokenFeeType.CrossCollateralRoutingFee);
expect(routingFee.owner).to.equal(signer.address);
expect(Object.keys(routingFee.feeContracts)).to.have.length(2);
expect(normalizeConfig(routingFee)).to.deep.equal(normalizeConfig({
type: TokenFeeType.CrossCollateralRoutingFee,
owner: signer.address,
feeContracts: {
[TestChainName.test3]: {
[DEFAULT_ROUTER_KEY]: linearFeeConfig,
},
[TestChainName.test4]: {
[DEFAULT_ROUTER_KEY]: linearFeeConfig,
},
},
}));
});
it('should derive cross collateral routing fee config using enrolled router mapping', async () => {
const linearFeeConfig = TokenFeeConfigSchema.parse({
type: TokenFeeType.LinearFee,
owner: signer.address,
token: token.address,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: linearFeeConfig,
});
const crossCollateralRoutingFeeFactory = await hre.ethers.getContractFactory('MockCrossCollateralRoutingFee', signer);
const crossCollateralRoutingFee = await crossCollateralRoutingFeeFactory.deploy(signer.address);
await crossCollateralRoutingFee.deployed();
const destination = multiProvider.getDomainId(TestChainName.test3);
const routerBytes32 = hre.ethers.utils.hexZeroPad(signer.address, 32);
await crossCollateralRoutingFee.setCrossCollateralRouterFeeContracts([destination], [routerBytes32], [
deployedContracts[TestChainName.test2][TokenFeeType.LinearFee]
.address,
]);
expect(await crossCollateralRoutingFee.feeContracts(destination, routerBytes32)).to.equal(deployedContracts[TestChainName.test2][TokenFeeType.LinearFee].address);
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const routingFee = await reader.deriveTokenFeeConfig({
address: crossCollateralRoutingFee.address,
routingDestinations: [destination],
crossCollateralRouters: {
[destination]: [routerBytes32],
},
});
expect(Object.keys(routingFee.feeContracts)).to.have.length(1);
expect(normalizeConfig(routingFee)).to.deep.equal(normalizeConfig({
type: TokenFeeType.CrossCollateralRoutingFee,
owner: signer.address,
feeContracts: {
[TestChainName.test3]: {
[routerBytes32]: linearFeeConfig,
},
},
}));
});
it('should derive CCRF DEFAULT_ROUTER entries for routing destinations outside cross collateral routers', async () => {
const linearFeeConfig = TokenFeeConfigSchema.parse({
type: TokenFeeType.LinearFee,
owner: signer.address,
token: token.address,
maxFee: MAX_FEE,
halfAmount: HALF_AMOUNT,
bps: BPS,
});
const deployer = new EvmTokenFeeDeployer(multiProvider, TestChainName.test2);
const deployedContracts = await deployer.deploy({
[TestChainName.test2]: linearFeeConfig,
});
const crossCollateralRoutingFeeFactory = await hre.ethers.getContractFactory('MockCrossCollateralRoutingFee', signer);
const crossCollateralRoutingFee = await crossCollateralRoutingFeeFactory.deploy(signer.address);
await crossCollateralRoutingFee.deployed();
const destination = multiProvider.getDomainId(TestChainName.test3);
const destination2 = multiProvider.getDomainId(TestChainName.test4);
const defaultRouter = await crossCollateralRoutingFee.DEFAULT_ROUTER();
await crossCollateralRoutingFee.setCrossCollateralRouterFeeContracts([destination, destination2], [defaultRouter, defaultRouter], [
deployedContracts[TestChainName.test2][TokenFeeType.LinearFee]
.address,
deployedContracts[TestChainName.test2][TokenFeeType.LinearFee]
.address,
]);
const reader = new EvmTokenFeeReader(multiProvider, TestChainName.test2);
const routingFee = await reader.deriveTokenFeeConfig({
address: crossCollateralRoutingFee.address,
routingDestinations: [destination, destination2],
crossCollateralRouters: {
[destination]: [],
},
});
expect(normalizeConfig(routingFee)).to.deep.equal(normalizeConfig({
type: TokenFeeType.CrossCollateralRoutingFee,
owner: signer.address,
feeContracts: {
[TestChainName.test3]: {
[DEFAULT_ROUTER_KEY]: linearFeeConfig,
},
[TestChainName.test4]: {
[DEFAULT_ROUTER_KEY]: linearFeeConfig,
},
},
}));
});
});
});
//# sourceMappingURL=EvmTokenFeeReader.hardhat-test.js.map