Fee-on-Transfer Token Support Guide

Overview

Fee-on-Transfer tokens are a class of ERC-20 tokens that automatically deduct a certain fee during transfers. JAMM DEX supports trading these tokens through specialized functions, ensuring accuracy and reliability in the swap process.

Characteristics of Fee-on-Transfer Tokens

How They Work

Fee-on-transfer tokens deduct a certain percentage of tokens as fees during each transfer or transferFrom call:

// Regular ERC-20 transfer
function transfer(address to, uint256 amount) external returns (bool) {
    _transfer(msg.sender, to, amount);
    return true;
}

// Fee-on-transfer token transfer
function transfer(address to, uint256 amount) external returns (bool) {
    uint256 fee = amount * feeRate / 10000; // Calculate fee
    uint256 actualAmount = amount - fee; // Actual transfer amount
    _transfer(msg.sender, to, actualAmount);
    _transfer(msg.sender, feeRecipient, fee); // Transfer fee
    return true;
}

Common Types

  1. Deflationary tokens: Burn a certain percentage of tokens on each transfer

  2. Reflection tokens: Distribute transfer fees to all holders

  3. Tax tokens: Send transfer fees to a specified address

  4. Dynamic fee tokens: Dynamically adjust fee rates based on conditions

JAMM DEX Support Mechanism

Standard Swaps vs Fee-on-Transfer Support Swaps

Standard swap functions:

  • Pre-calculate exact input/output amounts

  • Assume transfer amount equals actual received amount

  • Not suitable for fee-on-transfer tokens

Fee-on-transfer support functions:

  • Calculate based on actual received amounts

  • Dynamically adjust swap logic

  • Specifically handle fee-on-transfer tokens

Supported Functions

JAMM DEX provides the following fee-on-transfer support functions:

// Exact input, supporting transfer fees
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external;

// JU for tokens, supporting transfer fees
function swapExactETHForTokensSupportingFeeOnTransferTokens(
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external payable;

// Tokens for JU, supporting transfer fees
function swapExactTokensForETHSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external;

Implementation Principles

Internal Swap Logic

function _swapSupportingFeeOnTransferTokens(
    address[] memory path,
    uint24[] memory fees,
    address _to,
    address _referrer
) internal virtual {
    for (uint i; i < path.length - 1; i++) {
        SwapData memory data;
        (data.input, data.output) = (path[i], path[i + 1]);
        (data.token0, ) = JAMMLibrary.sortTokens(data.input, data.output);

        IJAMMPair pair = IJAMMPair(
            JAMMLibrary.pairFor(factory, data.input, data.output, fees[i])
        );
        (uint reserve0, uint reserve1, ) = pair.getReserves();

        (uint reserveInput, uint reserveOutput) = data.input == data.token0
            ? (reserve0, reserve1)
            : (reserve1, reserve0);

        // Key: Calculate input amount based on actual balance
        data.amountInput =
            IERC20(data.input).balanceOf(address(pair)) -
            reserveInput;
            
        data.amountOutput = JAMMLibrary.getAmountOut(
            data.amountInput,
            reserveInput,
            reserveOutput,
            fees[i]
        );

        (data.amount0Out, data.amount1Out) = data.input == data.token0
            ? (uint(0), data.amountOutput)
            : (data.amountOutput, uint(0));

        address to = i < path.length - 2
            ? JAMMLibrary.pairFor(
                factory,
                data.output,
                path[i + 2],
                fees[i + 1]
            )
            : _to;

        pair.swap(
            data.amount0Out,
            data.amount1Out,
            to,
            new bytes(0),
            _referrer
        );
    }
}

Key Differences:

  • Don't pre-calculate input amounts

  • Calculate based on actual balance changes in trading pairs

  • Recalculate input amount for each hop

Usage Guide

Token to Token Swaps

async function swapFeeOnTransferTokens(
    tokenIn,
    tokenOut,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 1. Check if token is fee-on-transfer
    const isFeeOnTransfer = await checkIfFeeOnTransfer(tokenIn);
    
    if (!isFeeOnTransfer) {
        console.warn("Token may not be fee-on-transfer, consider using standard swap functions");
    }
    
    // 2. Estimate output (note: this is only an estimate)
    const path = [tokenIn, tokenOut];
    const fees = [100];
    
    // For fee-on-transfer tokens, estimates may be inaccurate
    let estimatedOutput;
    try {
        const amounts = await router.getAmountsOut(amountIn, path, fees);
        estimatedOutput = amounts[1];
    } catch (error) {
        // If estimation fails, use conservative minimum output
        estimatedOutput = ethers.BigNumber.from(0);
    }
    
    // 3. Set larger slippage tolerance
    const adjustedSlippage = Math.max(slippagePercent, 5); // At least 5% slippage
    const slippageBps = Math.floor(adjustedSlippage * 100);
    const amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
    
    // 4. Execute swap
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
        amountIn,
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    console.log("Fee-on-transfer token swap submitted:", tx.hash);
    return tx;
}

JU to Fee-on-Transfer Token

async function swapJUForFeeOnTransferToken(
    tokenOut,
    juAmount,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [WJU_ADDRESS, tokenOut];
    const fees = [100];
    
    // For fee-on-transfer tokens, set more conservative minimum output
    const adjustedSlippage = Math.max(slippagePercent, 8); // At least 8% slippage
    const slippageBps = Math.floor(adjustedSlippage * 100);
    
    // Try to estimate output
    let amountOutMin = ethers.BigNumber.from(0);
    try {
        const amounts = await router.getAmountsOut(juAmount, path, fees);
        const estimatedOutput = amounts[1];
        amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
    } catch (error) {
        console.warn("Cannot estimate output, using 0 as minimum output");
    }
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactETHForTokensSupportingFeeOnTransferTokens(
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline,
        { value: juAmount }
    );
    
    return tx;
}

Fee-on-Transfer Token to JU

async function swapFeeOnTransferTokenForJU(
    tokenIn,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    const path = [tokenIn, WJU_ADDRESS];
    const fees = [100];
    
    // Set conservative minimum output
    const adjustedSlippage = Math.max(slippagePercent, 10); // At least 10% slippage
    const slippageBps = Math.floor(adjustedSlippage * 100);
    
    let amountOutMin = ethers.BigNumber.from(0);
    try {
        const amounts = await router.getAmountsOut(amountIn, path, fees);
        const estimatedOutput = amounts[1];
        amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
    } catch (error) {
        console.warn("Cannot estimate output");
    }
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.swapExactTokensForETHSupportingFeeOnTransferTokens(
        amountIn,
        amountOutMin,
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

Fee-on-Transfer Token Detection

Automatic Detection Methods

async function checkIfFeeOnTransfer(tokenAddress, testAmount = null) {
    try {
        const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
        
        // Method 1: Check contract code for fee-related keywords
        const code = await provider.getCode(tokenAddress);
        const feeKeywords = ['fee', 'tax', 'burn', 'reflect', 'deflationary'];
        const hasFeekeywords = feeKeywords.some(keyword => 
            code.toLowerCase().includes(ethers.utils.id(keyword).slice(2, 10))
        );
        
        if (hasFeekeywords) {
            return { isFeeOnTransfer: true, method: 'code_analysis', confidence: 'medium' };
        }
        
        // Method 2: Simulate transfer test (requires test tokens)
        if (testAmount) {
            const result = await simulateTransfer(tokenAddress, testAmount);
            return result;
        }
        
        // Method 3: Check known fee-on-transfer token list
        const knownFeeTokens = await getKnownFeeOnTransferTokens();
        if (knownFeeTokens.includes(tokenAddress.toLowerCase())) {
            return { isFeeOnTransfer: true, method: 'known_list', confidence: 'high' };
        }
        
        return { isFeeOnTransfer: false, method: 'default', confidence: 'low' };
        
    } catch (error) {
        console.error("Fee-on-transfer token detection failed:", error);
        return { isFeeOnTransfer: false, method: 'error', confidence: 'unknown' };
    }
}

async function simulateTransfer(tokenAddress, testAmount) {
    // This needs to be done in a test environment or using static calls
    // Actual implementation requires more complex logic
    try {
        const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
        
        // Use callStatic to simulate transfer
        const balanceBefore = await token.balanceOf(testAddress);
        await token.callStatic.transfer(testAddress, testAmount);
        const balanceAfter = await token.balanceOf(testAddress);
        
        const actualReceived = balanceAfter.sub(balanceBefore);
        const feeRate = testAmount.sub(actualReceived).mul(10000).div(testAmount);
        
        return {
            isFeeOnTransfer: feeRate.gt(0),
            feeRate: feeRate.toNumber(), // Basis points
            method: 'simulation',
            confidence: 'high'
        };
    } catch (error) {
        return { isFeeOnTransfer: false, method: 'simulation_failed', confidence: 'unknown' };
    }
}

Known Fee-on-Transfer Token List

// Maintain a list of known fee-on-transfer tokens
const KNOWN_FEE_ON_TRANSFER_TOKENS = {
    // Mainnet addresses example (actual addresses need to be determined based on JuChain network)
    'UNSURE': { name: 'Example Fee Token', feeRate: 200 }, // 2% fee rate
    // Add more known fee-on-transfer tokens
};

async function getKnownFeeOnTransferTokens() {
    return Object.keys(KNOWN_FEE_ON_TRANSFER_TOKENS).map(addr => addr.toLowerCase());
}

function getTokenFeeInfo(tokenAddress) {
    return KNOWN_FEE_ON_TRANSFER_TOKENS[tokenAddress.toLowerCase()] || null;
}

Best Practices

1. Slippage Management

function calculateFeeTokenSlippage(baseslippage, tokenInfo) {
    let adjustedSlippage = baseslippage;
    
    // Adjust slippage based on token type
    if (tokenInfo.isFeeOnTransfer) {
        // Base adjustment: add 2x the transfer fee rate as additional slippage
        adjustedSlippage += (tokenInfo.feeRate || 500) * 2 / 100; // Convert to percentage
        
        // Minimum slippage protection
        adjustedSlippage = Math.max(adjustedSlippage, 5); // At least 5%
        
        // Maximum slippage limit
        adjustedSlippage = Math.min(adjustedSlippage, 20); // At most 20%
    }
    
    return adjustedSlippage;
}

2. Pre-Swap Validation

async function validateFeeTokenSwap(tokenIn, tokenOut, amountIn) {
    const validations = [];
    
    // Check input token
    const tokenInInfo = await checkIfFeeOnTransfer(tokenIn);
    if (tokenInInfo.isFeeOnTransfer) {
        validations.push({
            type: 'warning',
            message: `Input token ${tokenIn} is fee-on-transfer, actual input may be less than expected`
        });
    }
    
    // Check output token
    const tokenOutInfo = await checkIfFeeOnTransfer(tokenOut);
    if (tokenOutInfo.isFeeOnTransfer) {
        validations.push({
            type: 'warning', 
            message: `Output token ${tokenOut} is fee-on-transfer, actual output may be less than expected`
        });
    }
    
    // Check liquidity
    const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
    const pairAddress = await factory.getPair(tokenIn, tokenOut, 100);
    
    if (pairAddress === ethers.constants.AddressZero) {
        validations.push({
            type: 'error',
            message: 'Trading pair does not exist'
        });
    } else {
        const pair = new ethers.Contract(pairAddress, pairABI, provider);
        const reserves = await pair.getReserves();
        
        if (reserves.reserve0.eq(0) || reserves.reserve1.eq(0)) {
            validations.push({
                type: 'error',
                message: 'Insufficient liquidity in trading pair'
            });
        }
    }
    
    return validations;
}

3. Swap Result Analysis

async function analyzeFeeTokenSwapResult(transactionReceipt, expectedOutput) {
    try {
        // Parse swap events
        const swapEvents = parseSwapEvents(transactionReceipt);
        
        if (swapEvents.length === 0) {
            return { success: false, error: 'No swap events found' };
        }
        
        const lastSwap = swapEvents[swapEvents.length - 1];
        const actualOutput = lastSwap.amount0Out.gt(0) ? lastSwap.amount0Out : lastSwap.amount1Out;
        
        // Calculate actual slippage
        let actualSlippage = ethers.BigNumber.from(0);
        if (expectedOutput.gt(0)) {
            actualSlippage = expectedOutput.sub(actualOutput).mul(10000).div(expectedOutput);
        }
        
        return {
            success: true,
            expectedOutput: expectedOutput,
            actualOutput: actualOutput,
            actualSlippage: actualSlippage.toNumber(), // Basis points
            swapCount: swapEvents.length,
            gasUsed: transactionReceipt.gasUsed
        };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

4. Multi-Hop Fee-on-Transfer Token Swaps

async function multiHopFeeTokenSwap(path, fees, amountIn, userAddress, signer) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // Check fee-on-transfer tokens in path
    const feeTokens = [];
    for (let i = 0; i < path.length; i++) {
        const tokenInfo = await checkIfFeeOnTransfer(path[i]);
        if (tokenInfo.isFeeOnTransfer) {
            feeTokens.push({ index: i, address: path[i], ...tokenInfo });
        }
    }
    
    if (feeTokens.length > 0) {
        console.log("Fee-on-transfer tokens detected:", feeTokens);
    }
    
    // Calculate dynamic slippage
    let totalFeeRate = feeTokens.reduce((sum, token) => sum + (token.feeRate || 500), 0);
    let slippagePercent = Math.max(5, totalFeeRate / 50); // Calculate slippage based on total fee rate
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    // Use fee-on-transfer supporting function
    const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
        amountIn,
        0, // Set to 0 because difficult to estimate accurately
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

Monitoring and Debugging

Swap Monitoring

async function monitorFeeTokenSwaps() {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    // Monitor fee-on-transfer supporting swap events
    const filter = router.filters.SwapExactTokensForTokensSupportingFeeOnTransferTokens();
    
    router.on(filter, async (sender, amountIn, amountOutMin, path, fees, to, referrer, deadline, event) => {
        console.log("Fee-on-transfer token swap:", {
            sender,
            path: path.map(addr => addr.slice(0, 6) + '...'),
            amountIn: ethers.utils.formatEther(amountIn),
            amountOutMin: ethers.utils.formatEther(amountOutMin),
            transactionHash: event.transactionHash
        });
        
        // Analyze swap result
        const receipt = await event.getTransactionReceipt();
        const analysis = await analyzeFeeTokenSwapResult(receipt, amountOutMin);
        console.log("Swap analysis:", analysis);
    });
}

Debugging Tools

class FeeTokenDebugger {
    constructor(routerAddress, provider) {
        this.router = new ethers.Contract(routerAddress, routerABI, provider);
        this.provider = provider;
    }
    
    async debugSwap(transactionHash) {
        const receipt = await this.provider.getTransactionReceipt(transactionHash);
        const transaction = await this.provider.getTransaction(transactionHash);
        
        // Parse transaction input
        const decodedInput = this.router.interface.parseTransaction({ data: transaction.data });
        
        // Analyze swap path
        const path = decodedInput.args.path;
        const fees = decodedInput.args.fees;
        
        console.log("Swap debug info:");
        console.log("- Path:", path);
        console.log("- Fees:", fees);
        console.log("- Gas used:", receipt.gasUsed.toString());
        
        // Check if each token is fee-on-transfer
        for (let i = 0; i < path.length; i++) {
            const tokenInfo = await checkIfFeeOnTransfer(path[i]);
            console.log(`- Token${i} (${path[i]}):`, tokenInfo);
        }
        
        // Analyze swap events
        const swapEvents = parseSwapEvents(receipt);
        console.log("- Swap event count:", swapEvents.length);
        
        return {
            transaction,
            receipt,
            decodedInput,
            swapEvents
        };
    }
}

Summary

Fee-on-transfer token support is an important feature of JAMM DEX. This guide covers:

  1. Fee-on-Transfer Token Principles: Working mechanisms and common types

  2. JAMM DEX Support: Specialized support functions and implementation principles

  3. Usage Methods: Swap implementations in various scenarios

  4. Token Detection: Methods to automatically identify fee-on-transfer tokens

  5. Best Practices: Slippage management, validation, analysis, etc.

  6. Monitoring and Debugging: Swap monitoring and troubleshooting tools

By correctly using fee-on-transfer token support features, developers can ensure normal trading of these special tokens on JAMM DEX.