UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

371 lines 19.2 kB
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