UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

1,061 lines 64.3 kB
import { expect } from 'chai'; import fs from 'fs'; import sinon from 'sinon'; import { parse as yamlParse } from 'yaml'; import { test1, test2, testCosmosChain, testScale1, testScale2, testSealevelChain, testVSXERC20, testXERC20, testXERC20Lockbox, } from '../consts/testChains.js'; import { MultiProtocolProvider } from '../providers/MultiProtocolProvider.js'; import { ProviderType } from '../providers/ProviderType.js'; import { Token } from '../token/Token.js'; import { TokenAmount } from '../token/TokenAmount.js'; import { TokenStandard } from '../token/TokenStandard.js'; import { encodeAbiParameters, zeroAddress } from 'viem'; import { TokenPullMode } from '../quoted-calls/types.js'; import { WarpCore } from './WarpCore.js'; import { WarpTxCategory } from './types.js'; const MOCK_LOCAL_QUOTE = { gasUnits: 2000n, gasPrice: 100, fee: 200000n }; const MOCK_INTERCHAIN_QUOTE = { amount: 20000n }; const TRANSFER_AMOUNT = BigInt('1000000000000000000'); // 1 units @ 18 decimals const MEDIUM_TRANSFER_AMOUNT = BigInt('15000000000000000000'); // 15 units @ 18 deicmals const BIG_TRANSFER_AMOUNT = BigInt('100000000000000000000'); // 100 units @ 18 decimals const MOCK_BALANCE = BigInt('10000000000000000000'); // 10 units @ 18 decimals const MEDIUM_MOCK_BALANCE = BigInt('50000000000000000000'); // 50 units at @ 18 decimals const MOCK_ADDRESS = '0x0000000000000000000000000000000000000001'; describe('WarpCore', () => { const multiProvider = MultiProtocolProvider.createTestMultiProtocolProvider(); let warpCore; let evmHypNative; let evmHypNativeScale1; let evmHypNativeScale2; let evmHypSynthetic; let evmHypXERC20; let evmHypVSXERC20; let evmHypXERC20Lockbox; let evmHypCollateralFiat; let sealevelHypSynthetic; let cwHypCollateral; let cw20; let cosmosIbc; before(() => { const exampleConfig = yamlParse(fs.readFileSync('./src/warp/test-warp-core-config.yaml', 'utf-8')); warpCore = WarpCore.FromConfig(multiProvider, exampleConfig); [ evmHypNative, evmHypSynthetic, evmHypXERC20, evmHypVSXERC20, evmHypXERC20Lockbox, evmHypNativeScale1, evmHypNativeScale2, evmHypCollateralFiat, sealevelHypSynthetic, cwHypCollateral, cw20, cosmosIbc, ] = warpCore.tokens; }); beforeEach(() => { sinon .stub(multiProvider, 'estimateTransactionFee') .returns(Promise.resolve(MOCK_LOCAL_QUOTE)); }); afterEach(() => { sinon.restore(); }); it('Constructs', () => { const fromArgs = new WarpCore(multiProvider, [ Token.FromChainMetadataNativeToken(test1), ]); const exampleConfig = yamlParse(fs.readFileSync('./src/warp/test-warp-core-config.yaml', 'utf-8')); const fromConfig = WarpCore.FromConfig(multiProvider, exampleConfig); expect(fromArgs).to.be.instanceOf(WarpCore); expect(fromConfig).to.be.instanceOf(WarpCore); expect(fromConfig.tokens.length).to.equal(exampleConfig.tokens.length); }); it('Finds tokens', () => { expect(warpCore.findToken(test1.name, evmHypNative.addressOrDenom)).to.be.instanceOf(Token); expect(warpCore.findToken(testSealevelChain.name, sealevelHypSynthetic.addressOrDenom)).to.be.instanceOf(Token); expect(warpCore.findToken(testCosmosChain.name, cw20.addressOrDenom)).to.be.instanceOf(Token); expect(warpCore.findToken(test1.name, sealevelHypSynthetic.addressOrDenom)) .to.be.null; }); it('Gets transfer gas quote', async () => { const stubs = warpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ quoteTransferRemoteGas: () => Promise.resolve({ igpQuote: MOCK_INTERCHAIN_QUOTE, tokenFeeQuote: MOCK_INTERCHAIN_QUOTE, }), isApproveRequired: () => Promise.resolve(false), populateTransferRemoteTx: () => Promise.resolve({}), isRevokeApprovalRequired: () => Promise.resolve(false), })); const testQuote = async (token, destination, standard, interchainQuote = { igpQuote: MOCK_INTERCHAIN_QUOTE, tokenFeeQuote: MOCK_INTERCHAIN_QUOTE, }) => { const tokenAmount = new TokenAmount(0, token); const result = await warpCore.estimateTransferRemoteFees({ originTokenAmount: tokenAmount, destination, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, }); expect(result.localQuote.token.standard, `token local standard check for ${token.chainName} to ${destination}`).equals(standard); expect(result.localQuote.amount, `token local amount check for ${token.chainName} to ${destination}`).to.equal(MOCK_LOCAL_QUOTE.fee); expect(result.interchainQuote.token.standard, `token interchain standard check for ${token.chainName} to ${destination}`).equals(standard); expect(result.interchainQuote.amount, `token interchain amount check for ${token.chainName} to ${destination}`).to.equal(interchainQuote.igpQuote.amount); expect(result.tokenFeeQuote?.amount, `token fee amount check for ${token.chainName} to ${destination}`).to.equal(interchainQuote.tokenFeeQuote?.amount); }; await testQuote(evmHypNative, test1.name, TokenStandard.EvmNative); await testQuote(evmHypNative, testCosmosChain.name, TokenStandard.EvmNative); await testQuote(evmHypNative, testSealevelChain.name, TokenStandard.EvmNative); await testQuote(evmHypSynthetic, test2.name, TokenStandard.EvmNative); await testQuote(sealevelHypSynthetic, test2.name, TokenStandard.SealevelNative); await testQuote(cosmosIbc, test1.name, TokenStandard.CosmosNative); // Note, this route uses an igp quote const config await testQuote(cwHypCollateral, test2.name, TokenStandard.CosmosNative, { igpQuote: { amount: 1n, addressOrDenom: 'atom' }, }); stubs.forEach((s) => s.restore()); }); it('Checks for destination collateral', async () => { const stubs = warpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(MOCK_BALANCE), getBridgedSupply: () => Promise.resolve(MOCK_BALANCE), isRevokeApprovalRequired: () => Promise.resolve(false), })); const testCollateral = async (token, destination, expectedBigResult) => { const smallResult = await warpCore.isDestinationCollateralSufficient({ originTokenAmount: token.amount(TRANSFER_AMOUNT), destination, }); expect(smallResult, `small collateral check for ${token.chainName} to ${destination}`).to.be.true; const bigResult = await warpCore.isDestinationCollateralSufficient({ originTokenAmount: token.amount(BIG_TRANSFER_AMOUNT), destination, }); expect(bigResult, `big collateral check for ${token.chainName} to ${destination}`).to.equal(expectedBigResult); }; await testCollateral(evmHypNative, test2.name, true); await testCollateral(evmHypNative, testCosmosChain.name, false); await testCollateral(evmHypNative, testSealevelChain.name, true); await testCollateral(cwHypCollateral, test1.name, false); await testCollateral(evmHypXERC20, testVSXERC20.name, true); await testCollateral(evmHypVSXERC20, testXERC20.name, true); await testCollateral(evmHypXERC20Lockbox, testXERC20.name, true); await testCollateral(evmHypNative, testXERC20Lockbox.name, false); stubs.forEach((s) => s.restore()); }); it('Checks for destination collateral with scaling factors', async () => { const stubs = warpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(10n), getBridgedSupply: () => Promise.resolve(10n), isRevokeApprovalRequired: () => Promise.resolve(false), })); const testCollateral = async (token, destination, amount, expectedResult) => { const result = await warpCore.isDestinationCollateralSufficient({ originTokenAmount: token.amount(amount), destination, }); expect(result, `collateral check for ${token.chainName} to ${destination}`).to.equal(expectedResult); }; const originalScale1 = evmHypNativeScale1.scale; const originalScale2 = evmHypNativeScale2.scale; try { await testCollateral(evmHypNativeScale1, testScale2.name, 10n, false); await testCollateral(evmHypNativeScale1, testScale2.name, 1n, true); await testCollateral(evmHypNativeScale2, testScale1.name, 10n, true); await testCollateral(evmHypNativeScale2, testScale1.name, 100n, true); await testCollateral(evmHypNativeScale2, testScale1.name, 101n, false); evmHypNativeScale1.scale = { numerator: 100n, denominator: 10n }; evmHypNativeScale2.scale = { numerator: 10n, denominator: 10n }; await testCollateral(evmHypNativeScale1, testScale2.name, 10n, false); await testCollateral(evmHypNativeScale1, testScale2.name, 1n, true); await testCollateral(evmHypNativeScale2, testScale1.name, 10n, true); await testCollateral(evmHypNativeScale2, testScale1.name, 100n, true); await testCollateral(evmHypNativeScale2, testScale1.name, 101n, false); } finally { evmHypNativeScale1.scale = originalScale1; evmHypNativeScale2.scale = originalScale2; stubs.forEach((s) => s.restore()); } }); it('Preserves decimals fallback for mixed-decimal routes missing scale', async () => { const originToken = new Token({ chainName: test1.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 18, symbol: 'TEST', name: 'Test Token', connections: [], }); const destinationToken = new Token({ chainName: test2.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 6, symbol: 'TEST', name: 'Test Token', connections: [], }); originToken.connections.push({ token: destinationToken }); const mixedDecimalWarpCore = new WarpCore(multiProvider, [ originToken, destinationToken, ]); const originStub = sinon.stub(originToken, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); const destinationStub = sinon .stub(destinationToken, 'getHypAdapter') .returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); try { expect(await mixedDecimalWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(1000000000000000000n), destination: test2.name, })).to.be.true; expect(await mixedDecimalWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(1000000000000000001n), destination: test2.name, })).to.be.false; } finally { originStub.restore(); destinationStub.restore(); } }); it('Uses message-space collateral checks when one side has rational scale and the other uses implicit identity', async () => { const originToken = new Token({ chainName: test1.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 18, scale: { numerator: 2, denominator: 1_000_000_000_000 }, symbol: 'TEST', name: 'Test Token', connections: [], }); const destinationToken = new Token({ chainName: test2.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 6, symbol: 'TEST', name: 'Test Token', connections: [], }); originToken.connections.push({ token: destinationToken }); const scaledWarpCore = new WarpCore(multiProvider, [ originToken, destinationToken, ]); const originStub = sinon.stub(originToken, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); const destinationStub = sinon .stub(destinationToken, 'getHypAdapter') .returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); try { expect(await scaledWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(500000000000000000n), destination: test2.name, })).to.be.true; expect(await scaledWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(750000000000000000n), destination: test2.name, })).to.be.false; } finally { originStub.restore(); destinationStub.restore(); } }); it('Uses message-space collateral checks when destination has scale and origin uses implicit identity', async () => { // Reverse of the previous test: origin is the "simple" 6-dec side, // destination is 18-dec with a rational scale. const originToken = new Token({ chainName: test1.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 6, symbol: 'TEST', name: 'Test Token', connections: [], }); const destinationToken = new Token({ chainName: test2.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 18, scale: { numerator: 2, denominator: 1_000_000_000_000 }, symbol: 'TEST', name: 'Test Token', connections: [], }); originToken.connections.push({ token: destinationToken }); const scaledWarpCore = new WarpCore(multiProvider, [ originToken, destinationToken, ]); // dest has 1_000_000 local units; scale {2, 1e12} → message = 1_000_000 * 2 / 1e12 = 0 // So even a small origin amount should be insufficient. const originStub = sinon.stub(originToken, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); const destinationStub = sinon .stub(destinationToken, 'getHypAdapter') .returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); try { // origin 1_000_000 (6-dec, identity scale) → message = 1_000_000 // dest 1_000_000 (18-dec, scale 2/1e12) → message = 0 (floor) expect(await scaledWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(1n), destination: test2.name, })).to.be.false; // Give dest enough balance: 500_000_000_000_000_000 * 2 / 1e12 = 1_000_000 destinationStub.restore(); const destinationStub2 = sinon .stub(destinationToken, 'getHypAdapter') .returns({ getBalance: () => Promise.resolve(500000000000000000n), getBridgedSupply: () => Promise.resolve(500000000000000000n), }); expect(await scaledWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(1000000n), destination: test2.name, })).to.be.true; destinationStub2.restore(); } finally { originStub.restore(); } }); it('Uses message-space collateral checks when both tokens have non-trivial scale and different decimals', async () => { // Production-realistic: origin 18-dec with scale-down {1, 1e12}, // dest 6-dec with scale-up {2, 1}. // origin message amount = localAmount * 1 / 1e12 // dest message amount = localAmount * 2 / 1 const originToken = new Token({ chainName: test1.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 18, scale: { numerator: 1, denominator: 1_000_000_000_000 }, symbol: 'TEST', name: 'Test Token', connections: [], }); const destinationToken = new Token({ chainName: test2.name, standard: TokenStandard.EvmHypCollateral, addressOrDenom: zeroAddress, decimals: 6, scale: { numerator: 2, denominator: 1 }, symbol: 'TEST', name: 'Test Token', connections: [], }); originToken.connections.push({ token: destinationToken }); const scaledWarpCore = new WarpCore(multiProvider, [ originToken, destinationToken, ]); const originStub = sinon.stub(originToken, 'getHypAdapter').returns({ getBalance: () => Promise.resolve(1000000n), getBridgedSupply: () => Promise.resolve(1000000n), }); // dest balance 500_000: message = 500_000 * 2 = 1_000_000 const destinationStub = sinon .stub(destinationToken, 'getHypAdapter') .returns({ getBalance: () => Promise.resolve(500000n), getBridgedSupply: () => Promise.resolve(500000n), }); try { // origin 1_000_000_000_000_000_000 (1 token in 18-dec) // → message = 1e18 * 1 / 1e12 = 1_000_000 // dest message = 500_000 * 2 = 1_000_000 → sufficient expect(await scaledWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(1000000000000000000n), destination: test2.name, })).to.be.true; // origin 2e18 → message = 2_000_000 // dest message = 1_000_000 → insufficient expect(await scaledWarpCore.isDestinationCollateralSufficient({ originTokenAmount: originToken.amount(2000000000000000000n), destination: test2.name, })).to.be.false; } finally { originStub.restore(); destinationStub.restore(); } }); it('Validates transfers', async () => { const balanceStubs = warpCore.tokens.map((t) => sinon.stub(t, 'getBalance').resolves({ amount: MOCK_BALANCE })); const minimumTransferAmount = 10n; const quoteStubs = warpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ quoteTransferRemoteGas: () => Promise.resolve({ igpQuote: MOCK_INTERCHAIN_QUOTE }), isApproveRequired: () => Promise.resolve(false), populateTransferRemoteTx: () => Promise.resolve({}), getMinimumTransferAmount: () => Promise.resolve(minimumTransferAmount), getBalance: () => Promise.resolve(MOCK_BALANCE), getBridgedSupply: () => Promise.resolve(MOCK_BALANCE), getMintLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), getMintMaxLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), isRevokeApprovalRequired: () => Promise.resolve(false), })); const validResult = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(validResult).to.be.null; const invalidChain = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: 'fakechain', recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.keys(invalidChain || {})[0]).to.equal('destination'); const invalidRecipient = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: testCosmosChain.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.keys(invalidRecipient || {})[0]).to.equal('recipient'); const invalidAmount = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(-10), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.keys(invalidAmount || {})[0]).to.equal('amount'); const insufficientAmount = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(minimumTransferAmount - 1n), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.keys(insufficientAmount || {})[0]).to.equal('amount'); const insufficientBalance = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(BIG_TRANSFER_AMOUNT), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.keys(insufficientBalance || {})[0]).to.equal('amount'); const validXERC20TokenResult = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: testXERC20.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(validXERC20TokenResult).to.be.null; const invalidRateLimit = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(BIG_TRANSFER_AMOUNT), destination: testXERC20.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.values(invalidRateLimit || {})[0]).to.equal('Rate limit exceeded on destination'); const invalidXERC20LockboxTokenRateLimit = await warpCore.validateTransfer({ originTokenAmount: evmHypXERC20.amount(BIG_TRANSFER_AMOUNT), destination: testXERC20Lockbox.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.values(invalidXERC20LockboxTokenRateLimit || {})[0]).to.equal('Rate limit exceeded on destination'); const invalidCollateralFiatTokenRateLimit = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(BIG_TRANSFER_AMOUNT), destination: evmHypCollateralFiat.chainName, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.values(invalidCollateralFiatTokenRateLimit || {})[0]).to.equal('Rate limit exceeded on destination'); const invalidCollateralXERC20LockboxToken = await warpCore.validateTransfer({ originTokenAmount: evmHypXERC20.amount(MEDIUM_TRANSFER_AMOUNT), destination: testXERC20Lockbox.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.values(invalidCollateralXERC20LockboxToken || {})[0]).to.equal('Insufficient collateral on destination'); balanceStubs.forEach((s) => { s.restore(); }); quoteStubs.forEach((s) => { s.restore(); }); }); it('Validates destination token routing', async () => { const balanceStubs = warpCore.tokens.map((t) => sinon.stub(t, 'getBalance').resolves({ amount: MOCK_BALANCE })); const minimumTransferAmount = 10n; const quoteStubs = warpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ quoteTransferRemoteGas: () => Promise.resolve({ igpQuote: MOCK_INTERCHAIN_QUOTE }), isApproveRequired: () => Promise.resolve(false), populateTransferRemoteTx: () => Promise.resolve({}), getMinimumTransferAmount: () => Promise.resolve(minimumTransferAmount), getBalance: () => Promise.resolve(MOCK_BALANCE), getBridgedSupply: () => Promise.resolve(MOCK_BALANCE), getMintLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), getMintMaxLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), isRevokeApprovalRequired: () => Promise.resolve(false), })); const invalidDestinationToken = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, destinationToken: evmHypCollateralFiat, }); expect(Object.values(invalidDestinationToken || {})[0]).to.equal(`Destination token chain mismatch for ${test2.name}`); const validDestinationToken = await warpCore.validateTransfer({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, destinationToken: evmHypSynthetic, }); expect(validDestinationToken).to.be.null; balanceStubs.forEach((s) => { s.restore(); }); quoteStubs.forEach((s) => { s.restore(); }); }); it('Requires explicit destination token for ambiguous routes', async () => { const ambiguousConfig = yamlParse(fs.readFileSync('./src/warp/test-warp-core-config.yaml', 'utf-8')); const extraTest2Address = '0x9876543210987654321098765432109876543219'; const test1Token = ambiguousConfig.tokens.find((token) => token.chainName === test1.name && token.addressOrDenom === evmHypNative.addressOrDenom); test1Token.connections.push({ token: `ethereum|${test2.name}|${extraTest2Address}`, }); ambiguousConfig.tokens.push({ chainName: test2.name, standard: TokenStandard.EvmHypSynthetic, decimals: 18, symbol: 'ETH2', name: 'Ether 2', addressOrDenom: extraTest2Address, connections: [ { token: `ethereum|${test1.name}|${evmHypNative.addressOrDenom}`, }, ], }); const ambiguousWarpCore = WarpCore.FromConfig(multiProvider, ambiguousConfig); const ambiguousOrigin = ambiguousWarpCore.findToken(test1.name, evmHypNative.addressOrDenom); const extraDestination = ambiguousWarpCore.findToken(test2.name, extraTest2Address); expect(ambiguousOrigin).to.not.be.null; expect(extraDestination).to.not.be.null; const balanceStubs = ambiguousWarpCore.tokens.map((t) => sinon.stub(t, 'getBalance').resolves({ amount: MOCK_BALANCE })); const minimumTransferAmount = 10n; const quoteStubs = ambiguousWarpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ quoteTransferRemoteGas: () => Promise.resolve({ igpQuote: MOCK_INTERCHAIN_QUOTE }), isApproveRequired: () => Promise.resolve(false), populateTransferRemoteTx: () => Promise.resolve({}), getMinimumTransferAmount: () => Promise.resolve(minimumTransferAmount), getBalance: () => Promise.resolve(MOCK_BALANCE), getBridgedSupply: () => Promise.resolve(MOCK_BALANCE), getMintLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), getMintMaxLimit: () => Promise.resolve(MEDIUM_MOCK_BALANCE), isRevokeApprovalRequired: () => Promise.resolve(false), })); const ambiguousValidation = await ambiguousWarpCore.validateTransfer({ originTokenAmount: ambiguousOrigin.amount(TRANSFER_AMOUNT), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, }); expect(Object.values(ambiguousValidation || {})[0]).to.equal(`Ambiguous route to ${test2.name}; specify destination token`); const explicitValidation = await ambiguousWarpCore.validateTransfer({ originTokenAmount: ambiguousOrigin.amount(TRANSFER_AMOUNT), destination: test2.name, recipient: MOCK_ADDRESS, sender: MOCK_ADDRESS, destinationToken: extraDestination, }); expect(explicitValidation).to.be.null; balanceStubs.forEach((s) => s.restore()); quoteStubs.forEach((s) => s.restore()); }); it('Includes token fee in CrossCollateralRouter approval debit', async () => { const tokenFeeAmount = 123n; const originalCollateralAddress = evmHypNative.collateralAddressOrDenom; evmHypNative.collateralAddressOrDenom = evmHypNative.addressOrDenom; const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const destinationMultiStub = sinon .stub(evmHypSynthetic, 'isCrossCollateralToken') .returns(true); const quoteTransferRemoteToGas = sinon.stub().resolves({ igpQuote: { amount: 1n }, tokenFeeQuote: { addressOrDenom: evmHypNative.addressOrDenom, amount: tokenFeeAmount, }, }); const isApproveRequired = sinon.stub().resolves(true); const populateApproveTx = sinon.stub().resolves({}); const populateTransferRemoteToTx = sinon.stub().resolves({}); const adapterStub = sinon.stub(evmHypNative, 'getHypAdapter').returns({ quoteTransferRemoteToGas, isApproveRequired, populateApproveTx, populateTransferRemoteToTx, isRevokeApprovalRequired: () => Promise.resolve(false), }); try { const result = await warpCore.getTransferRemoteTxs({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: evmHypSynthetic, }); expect(result.length).to.equal(2); sinon.assert.calledWithExactly(isApproveRequired, MOCK_ADDRESS, evmHypNative.addressOrDenom, TRANSFER_AMOUNT + tokenFeeAmount); sinon.assert.calledWithMatch(populateApproveTx, { weiAmountOrId: TRANSFER_AMOUNT + tokenFeeAmount, recipient: evmHypNative.addressOrDenom, }); } finally { adapterStub.restore(); originMultiStub.restore(); destinationMultiStub.restore(); evmHypNative.collateralAddressOrDenom = originalCollateralAddress; } }); it('Rejects CrossCollateralRouter transfer tx generation when IGP fee denom is non-native', async () => { const originalCollateralAddress = evmHypNative.collateralAddressOrDenom; evmHypNative.collateralAddressOrDenom = evmHypNative.addressOrDenom; const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const destinationMultiStub = sinon .stub(evmHypSynthetic, 'isCrossCollateralToken') .returns(true); const adapterStub = sinon.stub(evmHypNative, 'getHypAdapter').returns({ quoteTransferRemoteToGas: sinon.stub().resolves({ igpQuote: { amount: 1n, addressOrDenom: evmHypNative.addressOrDenom, }, tokenFeeQuote: { addressOrDenom: evmHypNative.addressOrDenom, amount: 0n, }, }), isApproveRequired: sinon.stub().resolves(false), isRevokeApprovalRequired: sinon.stub().resolves(false), populateTransferRemoteToTx: sinon.stub().resolves({}), }); try { let thrown; try { await warpCore.getTransferRemoteTxs({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: evmHypSynthetic, }); } catch (error) { thrown = error; } expect(thrown).to.not.equal(undefined); expect(thrown.message).to.contain('CrossCollateralRouter transferRemoteTo requires native IGP fee'); } finally { adapterStub.restore(); originMultiStub.restore(); destinationMultiStub.restore(); evmHypNative.collateralAddressOrDenom = originalCollateralAddress; } }); it('Checks destination collateral for CrossCollateralRouter route using explicit destination token', async () => { const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const destinationMultiStub = sinon .stub(cwHypCollateral, 'isCrossCollateralToken') .returns(true); const destinationAdapterStub = sinon .stub(cwHypCollateral, 'getAdapter') .returns({ getBalance: sinon.stub().resolves(10n), }); try { const smallResult = await warpCore.isDestinationCollateralSufficient({ originTokenAmount: evmHypNative.amount(9n), destination: cwHypCollateral.chainName, destinationToken: cwHypCollateral, }); expect(smallResult).to.equal(true); const bigResult = await warpCore.isDestinationCollateralSufficient({ originTokenAmount: evmHypNative.amount(11n), destination: cwHypCollateral.chainName, destinationToken: cwHypCollateral, }); expect(bigResult).to.equal(false); } finally { destinationAdapterStub.restore(); originMultiStub.restore(); destinationMultiStub.restore(); } }); it('Adds revoke before approval for CrossCollateralRouter when allowance must be reset', async () => { const tokenFeeAmount = 123n; const originalCollateralAddress = evmHypNative.collateralAddressOrDenom; evmHypNative.collateralAddressOrDenom = evmHypNative.addressOrDenom; const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const destinationMultiStub = sinon .stub(evmHypSynthetic, 'isCrossCollateralToken') .returns(true); const quoteTransferRemoteToGas = sinon.stub().resolves({ igpQuote: { amount: 1n }, tokenFeeQuote: { addressOrDenom: evmHypNative.addressOrDenom, amount: tokenFeeAmount, }, }); const isApproveRequired = sinon.stub().resolves(true); const isRevokeApprovalRequired = sinon.stub().resolves(true); const populateApproveTx = sinon.stub().resolves({}); const populateTransferRemoteToTx = sinon.stub().resolves({}); const adapterStub = sinon.stub(evmHypNative, 'getHypAdapter').returns({ quoteTransferRemoteToGas, isApproveRequired, isRevokeApprovalRequired, populateApproveTx, populateTransferRemoteToTx, }); try { const result = await warpCore.getTransferRemoteTxs({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: evmHypSynthetic, }); expect(result.length).to.equal(3); expect(result[0].category).to.equal(WarpTxCategory.Revoke); expect(result[1].category).to.equal(WarpTxCategory.Approval); expect(result[2].category).to.equal(WarpTxCategory.Transfer); sinon.assert.calledWithMatch(populateApproveTx.firstCall, { weiAmountOrId: 0, }); sinon.assert.calledWithMatch(populateApproveTx.secondCall, { weiAmountOrId: TRANSFER_AMOUNT + tokenFeeAmount, }); } finally { adapterStub.restore(); originMultiStub.restore(); destinationMultiStub.restore(); evmHypNative.collateralAddressOrDenom = originalCollateralAddress; } }); it('Uses destination router-aware quote for CrossCollateralRouter fees', async () => { const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const destinationMultiStub = sinon .stub(evmHypSynthetic, 'isCrossCollateralToken') .returns(true); const quoteTransferRemoteToGas = sinon.stub().resolves({ igpQuote: { amount: 42n }, tokenFeeQuote: { addressOrDenom: evmHypNative.addressOrDenom, amount: 11n, }, }); const adapterStub = sinon.stub(evmHypNative, 'getHypAdapter').returns({ quoteTransferRemoteToGas, populateTransferRemoteToTx: sinon.stub(), }); try { const quote = await warpCore.getInterchainTransferFee({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: evmHypSynthetic, }); expect(quote.igpQuote.amount).to.equal(42n); expect(quote.tokenFeeQuote?.amount).to.equal(11n); sinon.assert.calledWithMatch(quoteTransferRemoteToGas, { destination: test2.domainId, recipient: MOCK_ADDRESS, amount: TRANSFER_AMOUNT, targetRouter: evmHypSynthetic.addressOrDenom, }); } finally { adapterStub.restore(); originMultiStub.restore(); destinationMultiStub.restore(); } }); it('uses quoted interchain fee token for CrossCollateralRouter estimateTransferRemoteFees', async () => { const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const destinationMultiStub = sinon .stub(evmHypSynthetic, 'isCrossCollateralToken') .returns(true); const quoteTransferRemoteToGas = sinon.stub().resolves({ igpQuote: { amount: 42n, addressOrDenom: evmHypNative.addressOrDenom, }, }); const adapterStub = sinon.stub(evmHypNative, 'getHypAdapter').returns({ quoteTransferRemoteToGas, populateTransferRemoteToTx: sinon.stub(), }); const localFeeAmountStub = sinon .stub(warpCore, 'getLocalTransferFeeAmount') .resolves(evmHypNative.amount(7n)); try { const quote = await warpCore.estimateTransferRemoteFees({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: evmHypSynthetic, }); expect(quote.interchainQuote.amount).to.equal(42n); expect(quote.interchainQuote.token.addressOrDenom).to.equal(evmHypNative.addressOrDenom); expect(quote.localQuote.amount).to.equal(7n); } finally { localFeeAmountStub.restore(); adapterStub.restore(); destinationMultiStub.restore(); originMultiStub.restore(); } }); it('Rejects non-connected destination token for CrossCollateralRouter fee quote', async () => { const originMultiStub = sinon .stub(evmHypNative, 'isCrossCollateralToken') .returns(true); const quoteTransferRemoteToGas = sinon.stub().resolves({ igpQuote: { amount: 42n }, }); const adapterStub = sinon.stub(evmHypNative, 'getHypAdapter').returns({ quoteTransferRemoteToGas, }); const invalidDestinationToken = new Token({ ...evmHypSynthetic, addressOrDenom: '0x9999999999999999999999999999999999999999', }); const invalidDestinationMultiStub = sinon .stub(invalidDestinationToken, 'isCrossCollateralToken') .returns(true); try { let error; try { await warpCore.getInterchainTransferFee({ originTokenAmount: evmHypNative.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: invalidDestinationToken, }); } catch (e) { error = e; } expect(error).to.exist; expect(error.message).to.contain('is not connected'); expect(quoteTransferRemoteToGas.called).to.equal(false); } finally { invalidDestinationMultiStub.restore(); adapterStub.restore(); originMultiStub.restore(); } }); it('Treats Sealevel cross-collateral to EVM cross-collateral as transferRemoteTo route', async () => { const sealevelCrossCollateral = new Token({ chainName: testSealevelChain.name, standard: TokenStandard.SealevelHypCrossCollateral, addressOrDenom: '4UMNyNWW75zo69hxoJaRX5iXNUa5FdRPZZa9vDVCiESg', collateralAddressOrDenom: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6, symbol: 'USDC', name: 'USDC', }); const evmCrossCollateral = new Token({ chainName: test2.name, standard: TokenStandard.EvmHypCrossCollateralRouter, addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147', collateralAddressOrDenom: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6, symbol: 'USDC', name: 'USDC', }); sealevelCrossCollateral.addConnection({ token: evmCrossCollateral }); evmCrossCollateral.addConnection({ token: sealevelCrossCollateral }); const crossCollateralWarpCore = new WarpCore(multiProvider, [ sealevelCrossCollateral, evmCrossCollateral, ]); const quoteTransferRemoteToGas = sinon.stub().resolves({ igpQuote: { amount: 1n }, tokenFeeQuote: { amount: 0n }, }); const populateTransferRemoteToTx = sinon.stub().resolves({}); const populateTransferRemoteTx = sinon.stub().resolves({}); const originAdapterStub = sinon .stub(sealevelCrossCollateral, 'getHypAdapter') .returns({ quoteTransferRemoteToGas, populateTransferRemoteToTx, populateTransferRemoteTx, isApproveRequired: sinon.stub().resolves(false), isRevokeApprovalRequired: sinon.stub().resolves(false), }); try { expect(crossCollateralWarpCore.isCrossCollateralTransfer(sealevelCrossCollateral, evmCrossCollateral)).to.equal(true); const result = await crossCollateralWarpCore.getTransferRemoteTxs({ originTokenAmount: sealevelCrossCollateral.amount(TRANSFER_AMOUNT), destination: test2.name, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, destinationToken: evmCrossCollateral, }); expect(result).to.have.length(1); expect(result[0].category).to.equal(WarpTxCategory.Transfer); sinon.assert.calledOnce(populateTransferRemoteToTx); sinon.assert.notCalled(populateTransferRemoteTx); } finally { originAdapterStub.restore(); } }); it('Converts destination minimum transfer amount into origin decimals correctly', async () => { const destinationAdapterStub = sinon .stub(sealevelHypSynthetic, 'getAdapter') .returns({ getMinimumTransferAmount: () => Promise.resolve(1000000000n), }); try { const belowMinimum = await warpCore.validateAmount(evmHypNative.amount(500000000000000000n), testSealevelChain.name, MOCK_ADDRESS, sealevelHypSynthetic); expect(belowMinimum?.amount).to.contain('Minimum transfer amount'); const atMinimum = await warpCore.validateAmount(evmHypNative.amount(1000000000000000000n), testSealevelChain.name, MOCK_ADDRESS, sealevelHypSynthetic); expect(atMinimum).to.be.null; } finally { destinationAdapterStub.restore(); } }); it('Gets transfer remote txs', async () => { const coreStub = sinon .stub(warpCore, 'isApproveRequired') .returns(Promise.resolve(false)); const adapterStubs = warpCore.tokens.map((t) => sinon.stub(t, 'getHypAdapter').returns({ quoteTransferRemoteGas: () => Promise.resolve({ igpQuote: MOCK_INTERCHAIN_QUOTE }), populateTransferRemoteTx: () => Promise.resolve({}), isRevokeApprovalRequired: () => Promise.resolve(false), })); const testGetTxs = async (token, destination, providerType = ProviderType.EthersV5, expectExtraSigners = false) => { const result = await warpCore.getTransferRemoteTxs({ originTokenAmount: token.amount(TRANSFER_AMOUNT), destination, sender: MOCK_ADDRESS, recipient: MOCK_ADDRESS, }); expect(result.length).to.equal(1); if (expectExtraSigners) { const tx = result[0]; expect(tx.category).to.equal(WarpTxCategory.Transfer); expect(tx.type).to.equal(providerType); expect(tx.transaction).to.eql({}); if (tx.type === ProviderType.SolanaWeb3) { expect(tx.extraSigners).to.be.an('array').with.lengthOf(1); } } else { expect(result[0], `transfer tx for ${token.chainName} to ${destination}`).to.eql({ category: WarpTxCategory.Transfer, transaction: {}, type: providerType, }); } }; await testGetTxs(evmHypNative, test1.name); await testGetTxs(evmHypNative, testCosmosChain.name); await testGetTxs(evmHypNative, testSealevelChain.name); await testGetTxs(evmHypSynthetic, test2.name); await testGetTxs(sealevelHypSynthetic, test2.name, ProviderType.SolanaWeb3, true); await testGetTxs(cwHypCollateral, test1.name, ProviderType.CosmJsWasm); await testGetTxs(cosmosIbc, test1.name, ProviderType.CosmJs); coreStub.restore(); adapterStubs.forEach((s) => s.restore()); }); // ============ QuotedCalls tests ============ const MOCK_QUOTED_CALLS_ADDRESS = '0x0000000000000000000000000000000000AC0101'; const MOCK_CLIENT_SALT = '0x5555555555555555555555555555555555555555555555555555555555555555'; const MOCK_SUBMIT_QUOTE = { quoter: '0x000000000000000000000000000000000000aa01', quote: { context: '0xdeadbeef', data: '0xcafebabe', issuedAt: 1000, expiry: 1000, salt: MOCK_CLIENT_SALT, submitter: MOCK_QUOTED_CALLS_ADDRESS, }, signature: '0xaabb', }; // Encode a mock quoteExecute return value (Quote[][]) function encodeMockQuoteResult(results) { return encodeAbiParameters([ { type: 'tuple[][]', components: [ { name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, ], }, ], [results]); } it('Gets quoted transfer fee via quoteExecute', async () => { const syntheticAddr = evmHypSynthetic.addressOrDenom; const mockQuoteResult = encodeMockQuoteResult([ [], // SUBMIT_QUOTE [ // TRANSFER_REMOTE { token: zeroAddress, amount: 500n }, { token: syntheticAddr, amount: TRANSFER_AMOUNT + 10