@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
1,061 lines • 64.3 kB
JavaScript
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