Basic Token Swap

This example demonstrates how to perform a basic token swap using the IXFI Protocol's DEX aggregation capabilities.

Overview

A basic token swap allows users to exchange one token for another through the best available route across multiple DEX protocols, all in a single transaction.

Prerequisites

  • Wallet with tokens to swap

  • Sufficient gas for transaction execution

  • Basic understanding of ERC20 tokens

Smart Contract Integration

Simple Swap Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/ICrossChainAggregator.sol";

contract BasicSwapExample {
    using SafeERC20 for IERC20;
    
    ICrossChainAggregator public immutable aggregator;
    
    constructor(address _aggregator) {
        aggregator = ICrossChainAggregator(_aggregator);
    }
    
    function swapTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOutMin,
        address recipient
    ) external returns (uint256 amountOut) {
        // Transfer input tokens from user
        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
        
        // Approve aggregator to spend tokens
        IERC20(tokenIn).safeApprove(address(aggregator), amountIn);
        
        // Get quote for the swap
        uint256 expectedOut = aggregator.getAmountOut(
            amountIn,
            tokenIn,
            tokenOut
        );
        
        require(expectedOut >= amountOutMin, "Insufficient output amount");
        
        // Execute the swap
        amountOut = aggregator.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            tokenIn,
            tokenOut,
            recipient
        );
        
        return amountOut;
    }
}

Frontend Integration

Using ethers.js

import { ethers } from 'ethers';

// Contract addresses (replace with actual addresses)
const AGGREGATOR_ADDRESS = "0x...";
const TOKEN_A_ADDRESS = "0x...";
const TOKEN_B_ADDRESS = "0x...";

// Contract ABIs
const aggregatorABI = [
    "function getAmountOut(uint256 amountIn, address tokenIn, address tokenOut) view returns (uint256)",
    "function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address tokenIn, address tokenOut, address recipient) returns (uint256)"
];

const erc20ABI = [
    "function balanceOf(address owner) view returns (uint256)",
    "function approve(address spender, uint256 amount) returns (bool)",
    "function allowance(address owner, address spender) view returns (uint256)"
];

async function performBasicSwap() {
    // Initialize provider and signer
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const userAddress = await signer.getAddress();
    
    // Initialize contracts
    const aggregator = new ethers.Contract(AGGREGATOR_ADDRESS, aggregatorABI, signer);
    const tokenA = new ethers.Contract(TOKEN_A_ADDRESS, erc20ABI, signer);
    
    // Swap parameters
    const amountIn = ethers.parseEther("100"); // 100 Token A
    const slippageTolerance = 0.01; // 1%
    
    try {
        // Step 1: Check user balance
        const balance = await tokenA.balanceOf(userAddress);
        console.log(`Token A Balance: ${ethers.formatEther(balance)}`);
        
        if (balance < amountIn) {
            throw new Error("Insufficient Token A balance");
        }
        
        // Step 2: Get quote
        const expectedOut = await aggregator.getAmountOut(
            amountIn,
            TOKEN_A_ADDRESS,
            TOKEN_B_ADDRESS
        );
        
        const amountOutMin = expectedOut * BigInt(Math.floor((1 - slippageTolerance) * 1000)) / 1000n;
        
        console.log(`Expected Output: ${ethers.formatEther(expectedOut)} Token B`);
        console.log(`Minimum Output: ${ethers.formatEther(amountOutMin)} Token B`);
        
        // Step 3: Check and set allowance
        const currentAllowance = await tokenA.allowance(userAddress, AGGREGATOR_ADDRESS);
        
        if (currentAllowance < amountIn) {
            console.log("Approving tokens...");
            const approveTx = await tokenA.approve(AGGREGATOR_ADDRESS, amountIn);
            await approveTx.wait();
            console.log("Approval confirmed");
        }
        
        // Step 4: Execute swap
        console.log("Executing swap...");
        const swapTx = await aggregator.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            TOKEN_A_ADDRESS,
            TOKEN_B_ADDRESS,
            userAddress
        );
        
        const receipt = await swapTx.wait();
        console.log(`Swap completed! Transaction hash: ${receipt.hash}`);
        
        // Parse the actual output amount from events
        const swapEvent = receipt.logs.find(log => 
            log.topics[0] === ethers.id("TokenSwap(address,address,uint256,uint256,address)")
        );
        
        if (swapEvent) {
            const decoded = ethers.AbiCoder.defaultAbiCoder().decode(
                ['address', 'address', 'uint256', 'uint256', 'address'],
                swapEvent.data
            );
            console.log(`Actual output: ${ethers.formatEther(decoded[3])} Token B`);
        }
        
    } catch (error) {
        console.error("Swap failed:", error);
        throw error;
    }
}

React Component Example

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

const BasicSwapComponent = () => {
    const [tokenABalance, setTokenABalance] = useState('0');
    const [tokenBBalance, setTokenBBalance] = useState('0');
    const [amountIn, setAmountIn] = useState('');
    const [expectedOut, setExpectedOut] = useState('0');
    const [isLoading, setIsLoading] = useState(false);
    const [provider, setProvider] = useState(null);
    const [signer, setSigner] = useState(null);

    // Initialize Web3 connection
    useEffect(() => {
        const initWeb3 = async () => {
            if (window.ethereum) {
                const provider = new ethers.BrowserProvider(window.ethereum);
                const signer = await provider.getSigner();
                setProvider(provider);
                setSigner(signer);
                await loadBalances(signer);
            }
        };
        initWeb3();
    }, []);

    // Load token balances
    const loadBalances = async (signer) => {
        const userAddress = await signer.getAddress();
        const tokenA = new ethers.Contract(TOKEN_A_ADDRESS, erc20ABI, signer);
        const tokenB = new ethers.Contract(TOKEN_B_ADDRESS, erc20ABI, signer);
        
        const balanceA = await tokenA.balanceOf(userAddress);
        const balanceB = await tokenB.balanceOf(userAddress);
        
        setTokenABalance(ethers.formatEther(balanceA));
        setTokenBBalance(ethers.formatEther(balanceB));
    };

    // Get quote when amount changes
    useEffect(() => {
        const getQuote = async () => {
            if (amountIn && parseFloat(amountIn) > 0 && signer) {
                try {
                    const aggregator = new ethers.Contract(AGGREGATOR_ADDRESS, aggregatorABI, signer);
                    const amountInWei = ethers.parseEther(amountIn);
                    
                    const quote = await aggregator.getAmountOut(
                        amountInWei,
                        TOKEN_A_ADDRESS,
                        TOKEN_B_ADDRESS
                    );
                    
                    setExpectedOut(ethers.formatEther(quote));
                } catch (error) {
                    console.error('Quote error:', error);
                    setExpectedOut('0');
                }
            } else {
                setExpectedOut('0');
            }
        };

        const timeoutId = setTimeout(getQuote, 500); // Debounce
        return () => clearTimeout(timeoutId);
    }, [amountIn, signer]);

    // Execute swap
    const handleSwap = async () => {
        if (!signer || !amountIn || parseFloat(amountIn) <= 0) return;

        setIsLoading(true);
        try {
            await performBasicSwap();
            await loadBalances(signer); // Refresh balances
            setAmountIn(''); // Clear input
        } catch (error) {
            alert(`Swap failed: ${error.message}`);
        } finally {
            setIsLoading(false);
        }
    };

    return (
        <div className="swap-container">
            <h2>Basic Token Swap</h2>
            
            <div className="balance-info">
                <p>Token A Balance: {tokenABalance}</p>
                <p>Token B Balance: {tokenBBalance}</p>
            </div>

            <div className="swap-form">
                <div className="input-group">
                    <label>Amount to Swap (Token A):</label>
                    <input
                        type="number"
                        value={amountIn}
                        onChange={(e) => setAmountIn(e.target.value)}
                        placeholder="0.0"
                        disabled={isLoading}
                    />
                </div>

                <div className="output-group">
                    <label>Expected Output (Token B):</label>
                    <input
                        type="text"
                        value={expectedOut}
                        readOnly
                        placeholder="0.0"
                    />
                </div>

                <button 
                    onClick={handleSwap}
                    disabled={isLoading || !amountIn || parseFloat(amountIn) <= 0}
                    className="swap-button"
                >
                    {isLoading ? 'Swapping...' : 'Swap Tokens'}
                </button>
            </div>
        </div>
    );
};

export default BasicSwapComponent;

CLI Example

Using Hardhat Script

// scripts/basic-swap.js
const { ethers } = require("hardhat");

async function main() {
    // Get signers
    const [deployer] = await ethers.getSigners();
    console.log("Swapping with account:", deployer.address);

    // Contract addresses
    const AGGREGATOR_ADDRESS = process.env.AGGREGATOR_ADDRESS;
    const TOKEN_A_ADDRESS = process.env.TOKEN_A_ADDRESS;
    const TOKEN_B_ADDRESS = process.env.TOKEN_B_ADDRESS;

    // Initialize contracts
    const aggregator = await ethers.getContractAt("CrossChainAggregator", AGGREGATOR_ADDRESS);
    const tokenA = await ethers.getContractAt("IERC20", TOKEN_A_ADDRESS);
    const tokenB = await ethers.getContractAt("IERC20", TOKEN_B_ADDRESS);

    // Swap parameters
    const amountIn = ethers.parseEther("10"); // 10 tokens
    const slippage = 0.005; // 0.5%

    try {
        // Check balance
        const balance = await tokenA.balanceOf(deployer.address);
        console.log(`Token A balance: ${ethers.formatEther(balance)}`);

        if (balance < amountIn) {
            throw new Error("Insufficient balance");
        }

        // Get quote
        const expectedOut = await aggregator.getAmountOut(
            amountIn,
            TOKEN_A_ADDRESS,
            TOKEN_B_ADDRESS
        );

        const amountOutMin = expectedOut * BigInt(Math.floor((1 - slippage) * 1000)) / 1000n;

        console.log(`Expected output: ${ethers.formatEther(expectedOut)}`);
        console.log(`Minimum output: ${ethers.formatEther(amountOutMin)}`);

        // Check allowance
        const allowance = await tokenA.allowance(deployer.address, AGGREGATOR_ADDRESS);
        
        if (allowance < amountIn) {
            console.log("Approving tokens...");
            const approveTx = await tokenA.approve(AGGREGATOR_ADDRESS, amountIn);
            await approveTx.wait();
            console.log("Approval confirmed");
        }

        // Execute swap
        console.log("Executing swap...");
        const swapTx = await aggregator.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            TOKEN_A_ADDRESS,
            TOKEN_B_ADDRESS,
            deployer.address
        );

        const receipt = await swapTx.wait();
        console.log(`Swap completed! Gas used: ${receipt.gasUsed}`);

        // Check new balances
        const newBalanceA = await tokenA.balanceOf(deployer.address);
        const newBalanceB = await tokenB.balanceOf(deployer.address);

        console.log(`New Token A balance: ${ethers.formatEther(newBalanceA)}`);
        console.log(`New Token B balance: ${ethers.formatEther(newBalanceB)}`);

    } catch (error) {
        console.error("Swap failed:", error);
        process.exit(1);
    }
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

Run the script:

npx hardhat run scripts/basic-swap.js --network <network-name>

Price Impact and Slippage

Understanding Slippage

Slippage is the difference between expected and actual execution price:

function calculateSlippage(expectedOut, actualOut) {
    const slippage = (expectedOut - actualOut) / expectedOut;
    return slippage * 100; // Return as percentage
}

// Example usage
const slippagePercent = calculateSlippage(
    ethers.parseEther("99.5"),   // Expected
    ethers.parseEther("99.0")    // Actual
);
console.log(`Slippage: ${slippagePercent.toFixed(2)}%`);

Price Impact Calculation

async function calculatePriceImpact(tokenIn, tokenOut, amountIn) {
    const aggregator = new ethers.Contract(AGGREGATOR_ADDRESS, aggregatorABI, provider);
    
    // Get quote for small amount (current price)
    const smallAmount = ethers.parseEther("1");
    const basePrice = await aggregator.getAmountOut(smallAmount, tokenIn, tokenOut);
    
    // Get quote for actual amount
    const actualQuote = await aggregator.getAmountOut(amountIn, tokenIn, tokenOut);
    const actualPrice = actualQuote * smallAmount / amountIn;
    
    // Calculate price impact
    const priceImpact = (basePrice - actualPrice) / basePrice;
    return priceImpact * 100; // Return as percentage
}

Error Handling

Common Errors and Solutions

async function safeSwap(tokenIn, tokenOut, amountIn, amountOutMin, recipient) {
    try {
        const tx = await aggregator.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            tokenIn,
            tokenOut,
            recipient
        );
        return await tx.wait();
    } catch (error) {
        // Handle specific errors
        if (error.message.includes("insufficient output amount")) {
            throw new Error("Slippage tolerance exceeded. Try increasing slippage or reducing amount.");
        } else if (error.message.includes("transfer amount exceeds allowance")) {
            throw new Error("Token allowance insufficient. Please approve tokens first.");
        } else if (error.message.includes("transfer amount exceeds balance")) {
            throw new Error("Insufficient token balance for swap.");
        } else if (error.message.includes("no liquidity")) {
            throw new Error("No liquidity available for this trading pair.");
        } else {
            throw new Error(`Swap failed: ${error.message}`);
        }
    }
}

Gas Optimization

Batch Operations

// Optimized multi-step operations
function swapAndStake(
    address tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 amountOutMin,
    address stakingContract
) external {
    // Step 1: Swap tokens
    uint256 amountOut = aggregator.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        tokenIn,
        tokenOut,
        address(this) // Receive tokens to this contract
    );
    
    // Step 2: Stake received tokens
    IERC20(tokenOut).safeApprove(stakingContract, amountOut);
    IStaking(stakingContract).stake(amountOut);
}

Gas Estimation

async function estimateSwapGas(tokenIn, tokenOut, amountIn, amountOutMin, recipient) {
    try {
        const gasEstimate = await aggregator.estimateGas.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            tokenIn,
            tokenOut,
            recipient
        );
        
        // Add 20% buffer for gas price fluctuations
        return gasEstimate * 120n / 100n;
    } catch (error) {
        console.error("Gas estimation failed:", error);
        return 300000n; // Fallback gas limit
    }
}

Best Practices

1. Always Check Allowances

async function ensureAllowance(token, spender, amount, signer) {
    const tokenContract = new ethers.Contract(token, erc20ABI, signer);
    const userAddress = await signer.getAddress();
    
    const currentAllowance = await tokenContract.allowance(userAddress, spender);
    
    if (currentAllowance < amount) {
        const approveTx = await tokenContract.approve(spender, amount);
        await approveTx.wait();
    }
}

2. Implement Proper Slippage Protection

function calculateAmountOutMin(expectedOut, slippagePercent) {
    const slippageFactor = BigInt(Math.floor((100 - slippagePercent) * 100));
    return expectedOut * slippageFactor / 10000n;
}

3. Use Deadline for Time Protection

function getDeadline(minutesFromNow = 20) {
    return Math.floor(Date.now() / 1000) + (minutesFromNow * 60);
}

4. Monitor Transaction Status

async function waitForConfirmation(txHash, confirmations = 1) {
    const receipt = await provider.waitForTransaction(txHash, confirmations);
    
    if (receipt.status === 0) {
        throw new Error("Transaction failed");
    }
    
    return receipt;
}

Testing

Unit Tests

// test/BasicSwap.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("BasicSwap", function() {
    let aggregator, tokenA, tokenB, basicSwap;
    let owner, user;

    beforeEach(async function() {
        [owner, user] = await ethers.getSigners();
        
        // Deploy mock contracts and setup
        // ... deployment code
    });

    it("should perform basic token swap", async function() {
        const amountIn = ethers.parseEther("100");
        const expectedOut = await aggregator.getAmountOut(
            amountIn,
            tokenA.address,
            tokenB.address
        );
        
        await tokenA.connect(user).approve(basicSwap.address, amountIn);
        
        await expect(
            basicSwap.connect(user).swapTokens(
                tokenA.address,
                tokenB.address,
                amountIn,
                expectedOut * 99n / 100n, // 1% slippage
                user.address
            )
        ).to.emit(aggregator, "TokenSwap");
    });
});

Resources

Last updated