Cross-Chain Swap
This example demonstrates how to perform cross-chain token swaps using IXFI Protocol, including token transfers between different blockchain networks.
Overview
Cross-chain swaps enable users to exchange tokens across different blockchain networks in a single transaction. IXFI Protocol facilitates this through Axelar's cross-chain infrastructure.
Basic Cross-Chain Swap
Frontend Implementation
// examples/cross-chain-swap/frontend.js
import { ethers } from 'ethers';
import { IXFIGateway, CrossChainAggregator } from '@ixfi/sdk';
class CrossChainSwapExample {
constructor(providerUrl, privateKey) {
this.provider = new ethers.providers.JsonRpcProvider(providerUrl);
this.signer = new ethers.Wallet(privateKey, this.provider);
// Initialize IXFI components
this.gateway = new IXFIGateway({
provider: this.provider,
signer: this.signer
});
this.aggregator = new CrossChainAggregator({
provider: this.provider,
signer: this.signer
});
}
/**
* Execute a cross-chain swap from Ethereum to Polygon
* Example: Swap USDC on Ethereum to USDT on Polygon
*/
async executeCrossChainSwap() {
const swapParams = {
// Source chain swap (Ethereum)
sourceChain: 'ethereum',
sourceToken: '0xA0b86a33E6441e1a02c4e4670dd96EA0f25A632', // USDC on Ethereum
sourceAmount: ethers.utils.parseUnits('1000', 6), // 1000 USDC
// Destination chain swap (Polygon)
destinationChain: 'polygon',
destinationToken: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', // USDT on Polygon
destinationReceiver: this.signer.address,
minDestinationAmount: ethers.utils.parseUnits('950', 6), // 950 USDT (5% slippage)
// Swap configuration
routerType: 0, // Uniswap V2
deadline: Math.floor(Date.now() / 1000) + 1800, // 30 minutes
gasPayment: ethers.utils.parseEther('0.01') // Gas for destination chain
};
try {
console.log('Initiating cross-chain swap...');
// Step 1: Approve source token
await this.approveToken(swapParams.sourceToken, swapParams.sourceAmount);
// Step 2: Execute cross-chain swap
const tx = await this.aggregator.crossChainSwap(swapParams);
console.log('Transaction submitted:', tx.hash);
// Step 3: Wait for confirmation
const receipt = await tx.wait();
console.log('Source chain transaction confirmed:', receipt.transactionHash);
// Step 4: Monitor destination chain completion
const destinationTx = await this.monitorDestinationChain(
swapParams.destinationChain,
receipt.transactionHash
);
console.log('Cross-chain swap completed!');
return {
sourceTransaction: receipt.transactionHash,
destinationTransaction: destinationTx
};
} catch (error) {
console.error('Cross-chain swap failed:', error);
throw error;
}
}
async approveToken(tokenAddress, amount) {
const tokenContract = new ethers.Contract(
tokenAddress,
['function approve(address spender, uint256 amount) returns (bool)'],
this.signer
);
const approveTx = await tokenContract.approve(this.aggregator.address, amount);
await approveTx.wait();
console.log('Token approval confirmed');
}
async monitorDestinationChain(destinationChain, sourceTxHash) {
// Monitor Axelar for cross-chain message execution
const axelarApi = `https://api.axelar.dev/cross-chain/tx/${sourceTxHash}`;
for (let i = 0; i < 60; i++) { // Poll for 10 minutes
try {
const response = await fetch(axelarApi);
const data = await response.json();
if (data.status === 'executed') {
return data.destinationTransactionHash;
}
console.log(`Waiting for destination chain execution... (${i + 1}/60)`);
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
} catch (error) {
console.error('Error monitoring destination chain:', error);
}
}
throw new Error('Destination chain execution timeout');
}
/**
* Get quote for cross-chain swap
*/
async getCrossChainQuote(params) {
try {
const quote = await this.aggregator.getCrossChainQuote({
sourceChain: params.sourceChain,
sourceToken: params.sourceToken,
sourceAmount: params.sourceAmount,
destinationChain: params.destinationChain,
destinationToken: params.destinationToken,
routerType: params.routerType || 0
});
return {
estimatedOutput: quote.estimatedOutput,
bridgeFee: quote.bridgeFee,
gasFee: quote.gasFee,
totalFee: quote.totalFee,
estimatedTime: quote.estimatedTime,
priceImpact: quote.priceImpact
};
} catch (error) {
console.error('Failed to get cross-chain quote:', error);
throw error;
}
}
}
// Usage example
async function main() {
const swapper = new CrossChainSwapExample(
process.env.ETHEREUM_RPC_URL,
process.env.PRIVATE_KEY
);
// Get quote first
const quote = await swapper.getCrossChainQuote({
sourceChain: 'ethereum',
sourceToken: '0xA0b86a33E6441e1a02c4e4670dd96EA0f25A632',
sourceAmount: ethers.utils.parseUnits('1000', 6),
destinationChain: 'polygon',
destinationToken: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'
});
console.log('Cross-chain swap quote:', quote);
// Execute if quote is acceptable
if (parseFloat(ethers.utils.formatUnits(quote.estimatedOutput, 6)) >= 950) {
const result = await swapper.executeCrossChainSwap();
console.log('Swap result:', result);
}
}
// Run example
main().catch(console.error);
Smart Contract Implementation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ICrossChainAggregator.sol";
import "./interfaces/IIXFIGateway.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract CrossChainSwapExample is ReentrancyGuard {
ICrossChainAggregator public immutable aggregator;
IIXFIGateway public immutable gateway;
event CrossChainSwapInitiated(
address indexed user,
string sourceChain,
address sourceToken,
uint256 sourceAmount,
string destinationChain,
address destinationToken,
address destinationReceiver
);
constructor(address _aggregator, address _gateway) {
aggregator = ICrossChainAggregator(_aggregator);
gateway = IIXFIGateway(_gateway);
}
/**
* @dev Execute a cross-chain token swap
* @param sourceToken Address of token to swap from
* @param sourceAmount Amount of source token to swap
* @param destinationChain Target blockchain name
* @param destinationToken Address of token to receive on destination chain
* @param destinationReceiver Address to receive tokens on destination chain
* @param minDestinationAmount Minimum amount to receive (slippage protection)
* @param routerType DEX router type to use for swaps
*/
function executeCrossChainSwap(
address sourceToken,
uint256 sourceAmount,
string calldata destinationChain,
address destinationToken,
address destinationReceiver,
uint256 minDestinationAmount,
uint8 routerType
) external payable nonReentrant {
require(sourceAmount > 0, "Invalid amount");
require(bytes(destinationChain).length > 0, "Invalid destination chain");
require(destinationReceiver != address(0), "Invalid receiver");
// Transfer source tokens from user
IERC20(sourceToken).transferFrom(msg.sender, address(this), sourceAmount);
// Approve aggregator to spend tokens
IERC20(sourceToken).approve(address(aggregator), sourceAmount);
// Prepare cross-chain swap parameters
ICrossChainAggregator.CrossChainSwapParams memory params =
ICrossChainAggregator.CrossChainSwapParams({
sourceSwap: ICrossChainAggregator.SwapParams({
tokenIn: sourceToken,
tokenOut: _getBridgeToken(sourceToken), // Convert to bridge-compatible token
amountIn: sourceAmount,
minAmountOut: _calculateMinBridgeAmount(sourceAmount),
routerType: routerType,
to: address(this),
deadline: block.timestamp + 1800, // 30 minutes
swapData: ""
}),
destinationChain: destinationChain,
destinationToken: destinationToken,
destinationReceiver: destinationReceiver,
minDestinationAmount: minDestinationAmount,
destinationSwapData: _encodeDestinationSwap(destinationToken, routerType)
});
// Execute cross-chain swap
aggregator.crossChainSwap{value: msg.value}(params);
emit CrossChainSwapInitiated(
msg.sender,
"ethereum", // Current chain
sourceToken,
sourceAmount,
destinationChain,
destinationToken,
destinationReceiver
);
}
/**
* @dev Batch multiple cross-chain swaps
*/
function executeBatchCrossChainSwap(
ICrossChainAggregator.CrossChainSwapParams[] calldata swaps
) external payable nonReentrant {
require(swaps.length > 0, "No swaps provided");
require(swaps.length <= 10, "Too many swaps");
uint256 totalGasPayment = msg.value / swaps.length;
for (uint i = 0; i < swaps.length; i++) {
ICrossChainAggregator.CrossChainSwapParams memory swap = swaps[i];
// Transfer source tokens
IERC20(swap.sourceSwap.tokenIn).transferFrom(
msg.sender,
address(this),
swap.sourceSwap.amountIn
);
// Approve aggregator
IERC20(swap.sourceSwap.tokenIn).approve(
address(aggregator),
swap.sourceSwap.amountIn
);
// Execute swap
aggregator.crossChainSwap{value: totalGasPayment}(swap);
}
}
/**
* @dev Execute cross-chain swap with automatic routing optimization
*/
function executeOptimizedCrossChainSwap(
address sourceToken,
uint256 sourceAmount,
string calldata destinationChain,
address destinationToken,
address destinationReceiver,
uint256 maxSlippageBps
) external payable nonReentrant {
// Get quotes from all available routers
uint8[] memory routerTypes = new uint8[](3);
routerTypes[0] = 0; // Uniswap V2
routerTypes[1] = 10; // Uniswap V3
routerTypes[2] = 30; // Curve
uint256 bestAmountOut = 0;
uint8 bestRouterType = 0;
for (uint i = 0; i < routerTypes.length; i++) {
try aggregator.getQuote(
sourceToken,
_getBridgeToken(sourceToken),
sourceAmount,
routerTypes[i]
) returns (uint256 amountOut, uint256) {
if (amountOut > bestAmountOut) {
bestAmountOut = amountOut;
bestRouterType = routerTypes[i];
}
} catch {
// Skip failed quotes
continue;
}
}
require(bestAmountOut > 0, "No valid quotes");
// Calculate minimum amount with slippage
uint256 minDestinationAmount = bestAmountOut -
(bestAmountOut * maxSlippageBps / 10000);
// Execute with best router
this.executeCrossChainSwap(
sourceToken,
sourceAmount,
destinationChain,
destinationToken,
destinationReceiver,
minDestinationAmount,
bestRouterType
);
}
/**
* @dev Get bridge-compatible token for cross-chain transfer
*/
function _getBridgeToken(address token) internal pure returns (address) {
// Map to Axelar-supported tokens
// This would be implemented based on supported bridge tokens
return token; // Simplified for example
}
/**
* @dev Calculate minimum bridge amount considering fees
*/
function _calculateMinBridgeAmount(uint256 amount) internal pure returns (uint256) {
// Account for bridge fees (typically 0.1%)
return amount - (amount * 10 / 10000);
}
/**
* @dev Encode destination chain swap data
*/
function _encodeDestinationSwap(
address destinationToken,
uint8 routerType
) internal pure returns (bytes memory) {
return abi.encode(destinationToken, routerType);
}
/**
* @dev Emergency function to recover stuck tokens
*/
function recoverTokens(address token, uint256 amount) external {
require(msg.sender == owner(), "Not authorized");
IERC20(token).transfer(msg.sender, amount);
}
/**
* @dev Estimate gas costs for cross-chain swap
*/
function estimateGasCost(
string calldata destinationChain,
address destinationToken
) external view returns (uint256) {
// This would integrate with Axelar's gas estimation
return 0.01 ether; // Simplified estimate
}
}
React Component
// examples/cross-chain-swap/CrossChainSwapComponent.jsx
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { useCrossChainSwap } from '../hooks/useCrossChainSwap';
const SUPPORTED_CHAINS = [
{ id: 'ethereum', name: 'Ethereum', chainId: 1 },
{ id: 'polygon', name: 'Polygon', chainId: 137 },
{ id: 'bsc', name: 'BSC', chainId: 56 },
{ id: 'arbitrum', name: 'Arbitrum', chainId: 42161 },
{ id: 'optimism', name: 'Optimism', chainId: 10 },
{ id: 'avalanche', name: 'Avalanche', chainId: 43114 }
];
const COMMON_TOKENS = {
ethereum: [
{ symbol: 'USDC', address: '0xA0b86a33E6441e1a02c4e4670dd96EA0f25A632', decimals: 6 },
{ symbol: 'USDT', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 },
{ symbol: 'WETH', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18 }
],
polygon: [
{ symbol: 'USDC', address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', decimals: 6 },
{ symbol: 'USDT', address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', decimals: 6 },
{ symbol: 'WMATIC', address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', decimals: 18 }
]
};
const CrossChainSwapComponent = () => {
const [sourceChain, setSourceChain] = useState('ethereum');
const [destinationChain, setDestinationChain] = useState('polygon');
const [sourceToken, setSourceToken] = useState('');
const [destinationToken, setDestinationToken] = useState('');
const [amount, setAmount] = useState('');
const [slippageTolerance, setSlippageTolerance] = useState(0.5);
const [quote, setQuote] = useState(null);
const {
executeCrossChainSwap,
getCrossChainQuote,
monitorSwapStatus,
loading,
error
} = useCrossChainSwap();
const handleGetQuote = async () => {
if (!sourceToken || !destinationToken || !amount) return;
try {
const quoteParams = {
sourceChain,
sourceToken,
sourceAmount: ethers.utils.parseUnits(amount, getTokenDecimals(sourceChain, sourceToken)),
destinationChain,
destinationToken
};
const quoteResult = await getCrossChainQuote(quoteParams);
setQuote(quoteResult);
} catch (err) {
console.error('Failed to get quote:', err);
}
};
const handleExecuteSwap = async () => {
if (!quote) return;
try {
const swapParams = {
sourceChain,
sourceToken,
sourceAmount: ethers.utils.parseUnits(amount, getTokenDecimals(sourceChain, sourceToken)),
destinationChain,
destinationToken,
minDestinationAmount: calculateMinOutput(),
slippageTolerance
};
const result = await executeCrossChainSwap(swapParams);
// Monitor swap progress
monitorSwapStatus(result.txHash, (status) => {
console.log('Swap status:', status);
});
} catch (err) {
console.error('Swap failed:', err);
}
};
const calculateMinOutput = () => {
if (!quote) return '0';
const output = parseFloat(ethers.utils.formatUnits(quote.estimatedOutput, getTokenDecimals(destinationChain, destinationToken)));
const minOutput = output * (1 - slippageTolerance / 100);
return ethers.utils.parseUnits(minOutput.toFixed(6), getTokenDecimals(destinationChain, destinationToken));
};
const getTokenDecimals = (chain, tokenAddress) => {
const tokens = COMMON_TOKENS[chain] || [];
const token = tokens.find(t => t.address.toLowerCase() === tokenAddress.toLowerCase());
return token ? token.decimals : 18;
};
const getAvailableTokens = (chain) => {
return COMMON_TOKENS[chain] || [];
};
return (
<div className="cross-chain-swap">
<h2>Cross-Chain Token Swap</h2>
<div className="swap-form">
{/* Source Chain Selection */}
<div className="input-group">
<label>From Chain</label>
<select
value={sourceChain}
onChange={(e) => setSourceChain(e.target.value)}
>
{SUPPORTED_CHAINS.map(chain => (
<option key={chain.id} value={chain.id}>
{chain.name}
</option>
))}
</select>
</div>
{/* Source Token Selection */}
<div className="input-group">
<label>From Token</label>
<select
value={sourceToken}
onChange={(e) => setSourceToken(e.target.value)}
>
<option value="">Select Token</option>
{getAvailableTokens(sourceChain).map(token => (
<option key={token.address} value={token.address}>
{token.symbol}
</option>
))}
</select>
</div>
{/* Amount Input */}
<div className="input-group">
<label>Amount</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
/>
</div>
{/* Destination Chain Selection */}
<div className="input-group">
<label>To Chain</label>
<select
value={destinationChain}
onChange={(e) => setDestinationChain(e.target.value)}
>
{SUPPORTED_CHAINS.filter(chain => chain.id !== sourceChain).map(chain => (
<option key={chain.id} value={chain.id}>
{chain.name}
</option>
))}
</select>
</div>
{/* Destination Token Selection */}
<div className="input-group">
<label>To Token</label>
<select
value={destinationToken}
onChange={(e) => setDestinationToken(e.target.value)}
>
<option value="">Select Token</option>
{getAvailableTokens(destinationChain).map(token => (
<option key={token.address} value={token.address}>
{token.symbol}
</option>
))}
</select>
</div>
{/* Slippage Tolerance */}
<div className="input-group">
<label>Slippage Tolerance (%)</label>
<input
type="number"
step="0.1"
value={slippageTolerance}
onChange={(e) => setSlippageTolerance(parseFloat(e.target.value))}
/>
</div>
{/* Get Quote Button */}
<button
onClick={handleGetQuote}
disabled={loading || !sourceToken || !destinationToken || !amount}
>
{loading ? 'Getting Quote...' : 'Get Quote'}
</button>
</div>
{/* Quote Display */}
{quote && (
<div className="quote-section">
<h3>Quote Details</h3>
<div className="quote-info">
<div className="quote-item">
<span>Estimated Output:</span>
<span>{ethers.utils.formatUnits(quote.estimatedOutput, getTokenDecimals(destinationChain, destinationToken))}</span>
</div>
<div className="quote-item">
<span>Bridge Fee:</span>
<span>{ethers.utils.formatEther(quote.bridgeFee)} ETH</span>
</div>
<div className="quote-item">
<span>Gas Fee:</span>
<span>{ethers.utils.formatEther(quote.gasFee)} ETH</span>
</div>
<div className="quote-item">
<span>Price Impact:</span>
<span>{quote.priceImpact}%</span>
</div>
<div className="quote-item">
<span>Estimated Time:</span>
<span>{quote.estimatedTime} minutes</span>
</div>
</div>
<button
onClick={handleExecuteSwap}
disabled={loading}
className="execute-button"
>
{loading ? 'Executing Swap...' : 'Execute Cross-Chain Swap'}
</button>
</div>
)}
{/* Error Display */}
{error && (
<div className="error">
Error: {error.message}
</div>
)}
</div>
);
};
export default CrossChainSwapComponent;
Custom Hook
// hooks/useCrossChainSwap.js
import { useState, useCallback } from 'react';
import { ethers } from 'ethers';
import { useIXFI } from './useIXFI';
export const useCrossChainSwap = () => {
const { aggregator, gateway } = useIXFI();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const getCrossChainQuote = useCallback(async (params) => {
setLoading(true);
setError(null);
try {
const quote = await aggregator.getCrossChainQuote(params);
return quote;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [aggregator]);
const executeCrossChainSwap = useCallback(async (params) => {
setLoading(true);
setError(null);
try {
// Approve tokens if needed
if (params.sourceToken !== ethers.constants.AddressZero) {
await approveToken(params.sourceToken, params.sourceAmount);
}
// Execute swap
const tx = await aggregator.crossChainSwap(params);
return {
txHash: tx.hash,
receipt: await tx.wait()
};
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [aggregator]);
const monitorSwapStatus = useCallback(async (txHash, callback) => {
const pollInterval = 10000; // 10 seconds
const maxAttempts = 180; // 30 minutes
let attempts = 0;
const poll = async () => {
try {
const status = await getSwapStatus(txHash);
callback(status);
if (status.completed || attempts >= maxAttempts) {
return;
}
attempts++;
setTimeout(poll, pollInterval);
} catch (err) {
console.error('Error polling swap status:', err);
setTimeout(poll, pollInterval);
}
};
poll();
}, []);
const getSwapStatus = async (txHash) => {
// This would integrate with Axelar's API or indexer
const response = await fetch(`https://api.axelar.dev/cross-chain/tx/${txHash}`);
const data = await response.json();
return {
status: data.status,
sourceChainConfirmed: data.sourceChainConfirmed,
bridgeProcessed: data.bridgeProcessed,
destinationChainExecuted: data.destinationChainExecuted,
completed: data.status === 'executed',
destinationTxHash: data.destinationTransactionHash
};
};
const approveToken = async (tokenAddress, amount) => {
const tokenContract = new ethers.Contract(
tokenAddress,
['function approve(address spender, uint256 amount) returns (bool)'],
aggregator.signer
);
const tx = await tokenContract.approve(aggregator.address, amount);
await tx.wait();
};
return {
getCrossChainQuote,
executeCrossChainSwap,
monitorSwapStatus,
loading,
error
};
};
Advanced Features
Multi-Hop Cross-Chain Swaps
// Advanced multi-hop cross-chain swap
class MultiHopCrossChainSwap {
constructor(ixfiSDK) {
this.sdk = ixfiSDK;
}
async executeMultiHopSwap(hops) {
/*
Example hops:
[
{ chain: 'ethereum', tokenIn: 'ETH', tokenOut: 'USDC' },
{ chain: 'polygon', tokenIn: 'USDC', tokenOut: 'MATIC' },
{ chain: 'bsc', tokenIn: 'MATIC', tokenOut: 'BNB' }
]
*/
const results = [];
let currentAmount = hops[0].amountIn;
for (let i = 0; i < hops.length; i++) {
const hop = hops[i];
if (i === 0) {
// First hop - local swap
const result = await this.sdk.aggregator.executeSwap({
tokenIn: hop.tokenIn,
tokenOut: hop.tokenOut,
amountIn: currentAmount,
minAmountOut: hop.minAmountOut,
routerType: hop.routerType
});
currentAmount = result.amountOut;
results.push(result);
} else {
// Subsequent hops - cross-chain
const result = await this.sdk.gateway.crossChainSwap({
sourceChain: hops[i-1].chain,
sourceToken: hops[i-1].tokenOut,
sourceAmount: currentAmount,
destinationChain: hop.chain,
destinationToken: hop.tokenOut,
minDestinationAmount: hop.minAmountOut
});
currentAmount = result.estimatedOutput;
results.push(result);
}
}
return results;
}
}
Gas Optimization Strategies
// Gas-optimized cross-chain swaps
class GasOptimizedCrossChainSwap {
async executeWithOptimalGas(params) {
// 1. Monitor gas prices across chains
const gasPrices = await this.getGasPricesAcrossChains([
params.sourceChain,
params.destinationChain
]);
// 2. Calculate optimal timing
const optimalTiming = this.calculateOptimalTiming(gasPrices);
// 3. Execute with dynamic gas pricing
return await this.executeWithDynamicGas(params, optimalTiming);
}
async getGasPricesAcrossChains(chains) {
const prices = {};
for (const chain of chains) {
const gasPrice = await this.getChainGasPrice(chain);
prices[chain] = gasPrice;
}
return prices;
}
calculateOptimalTiming(gasPrices) {
// Implement gas price prediction algorithm
return {
shouldWait: false,
estimatedOptimalTime: Date.now(),
potentialSavings: 0
};
}
}
Testing
// test/cross-chain-swap.test.js
describe('Cross-Chain Swap Example', () => {
it('should execute cross-chain swap successfully', async () => {
const swapper = new CrossChainSwapExample(provider, privateKey);
const result = await swapper.executeCrossChainSwap();
expect(result.sourceTransaction).to.be.a('string');
expect(result.destinationTransaction).to.be.a('string');
});
it('should get accurate cross-chain quotes', async () => {
const quote = await swapper.getCrossChainQuote({
sourceChain: 'ethereum',
sourceToken: USDC_ADDRESS,
sourceAmount: ethers.utils.parseUnits('1000', 6),
destinationChain: 'polygon',
destinationToken: USDT_ADDRESS
});
expect(quote.estimatedOutput).to.be.gt(0);
expect(quote.bridgeFee).to.be.gt(0);
});
});
Resources
Last updated