转账费代币支持

转账费代币(Fee-on-Transfer Tokens)是一类在转账时自动扣除一定费用的ERC-20代币。JAMM DEX通过专门的函数支持这类代币的交易,确保交换过程的准确性和可靠性。

转账费代币的特点

工作原理

转账费代币在每次transfertransferFrom调用时会扣除一定比例的代币作为费用:

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

// 转账费代币转账
function transfer(address to, uint256 amount) external returns (bool) {
    uint256 fee = amount * feeRate / 10000; // 计算费用
    uint256 actualAmount = amount - fee; // 实际转账数量
    _transfer(msg.sender, to, actualAmount);
    _transfer(msg.sender, feeRecipient, fee); // 转账费用
    return true;
}

常见类型

  1. 通缩代币: 每次转账销毁一定比例的代币

  2. 反射代币: 将转账费用分配给所有持有者

  3. 税收代币: 将转账费用发送到指定地址

  4. 动态费率代币: 根据条件动态调整费率

JAMM DEX的支持机制

标准交换 vs 转账费支持交换

标准交换函数

  • 预先计算精确的输入/输出数量

  • 假设转账数量等于实际到账数量

  • 不适用于转账费代币

转账费支持函数

  • 基于实际到账数量进行计算

  • 动态调整交换逻辑

  • 专门处理转账费代币

支持的函数

JAMM DEX提供以下转账费支持函数:

// 精确输入,支持转账费
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external;

// JU换代币,支持转账费
function swapExactETHForTokensSupportingFeeOnTransferTokens(
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external payable;

// 代币换JU,支持转账费
function swapExactTokensForETHSupportingFeeOnTransferTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    uint24[] calldata fees,
    address to,
    address referrer,
    uint deadline
) external;

实现原理

内部交换逻辑

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);

        // 关键:基于实际余额计算输入数量
        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
        );
    }
}

关键差异

  • 不预先计算输入数量

  • 基于交易对的实际余额变化计算

  • 每一跳都重新计算输入数量

使用指南

代币到代币交换

async function swapFeeOnTransferTokens(
    tokenIn,
    tokenOut,
    amountIn,
    slippagePercent,
    userAddress,
    signer
) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 1. 检查代币是否为转账费代币
    const isFeeOnTransfer = await checkIfFeeOnTransfer(tokenIn);
    
    if (!isFeeOnTransfer) {
        console.warn("代币可能不是转账费代币,考虑使用标准交换函数");
    }
    
    // 2. 预估输出(注意:这只是估算)
    const path = [tokenIn, tokenOut];
    const fees = [100];
    
    // 对于转账费代币,预估可能不准确
    let estimatedOutput;
    try {
        const amounts = await router.getAmountsOut(amountIn, path, fees);
        estimatedOutput = amounts[1];
    } catch (error) {
        // 如果预估失败,使用保守的最小输出
        estimatedOutput = ethers.BigNumber.from(0);
    }
    
    // 3. 设置较大的滑点容忍度
    const adjustedSlippage = Math.max(slippagePercent, 5); // 至少5%滑点
    const slippageBps = Math.floor(adjustedSlippage * 100);
    const amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
    
    // 4. 执行交换
    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("转账费代币交换已提交:", tx.hash);
    return tx;
}

JU到转账费代币

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];
    
    // 对于转账费代币,设置更保守的最小输出
    const adjustedSlippage = Math.max(slippagePercent, 8); // 至少8%滑点
    const slippageBps = Math.floor(adjustedSlippage * 100);
    
    // 尝试预估输出
    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("无法预估输出,使用0作为最小输出");
    }
    
    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;
}

转账费代币到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];
    
    // 设置保守的最小输出
    const adjustedSlippage = Math.max(slippagePercent, 10); // 至少10%滑点
    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("无法预估输出");
    }
    
    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;
}

转账费代币检测

自动检测方法

async function checkIfFeeOnTransfer(tokenAddress, testAmount = null) {
    try {
        const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
        
        // 方法1: 检查合约代码中是否包含费用相关关键词
        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' };
        }
        
        // 方法2: 模拟转账测试(需要测试代币)
        if (testAmount) {
            const result = await simulateTransfer(tokenAddress, testAmount);
            return result;
        }
        
        // 方法3: 检查已知的转账费代币列表
        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("检测转账费代币失败:", error);
        return { isFeeOnTransfer: false, method: 'error', confidence: 'unknown' };
    }
}

async function simulateTransfer(tokenAddress, testAmount) {
    // 这需要在测试环境中进行,或者使用静态调用
    // 实际实现需要更复杂的逻辑
    try {
        const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
        
        // 使用callStatic模拟转账
        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(), // 基点
            method: 'simulation',
            confidence: 'high'
        };
    } catch (error) {
        return { isFeeOnTransfer: false, method: 'simulation_failed', confidence: 'unknown' };
    }
}

已知转账费代币列表

// 维护一个已知的转账费代币列表
const KNOWN_FEE_ON_TRANSFER_TOKENS = {
    // 主网地址示例(实际地址需要根据JuChain网络确定)
    'UNSURE': { name: 'Example Fee Token', feeRate: 200 }, // 2%费率
    // 添加更多已知的转账费代币
};

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;
}

最佳实践

1. 滑点管理

function calculateFeeTokenSlippage(baseslippage, tokenInfo) {
    let adjustedSlippage = baseslippage;
    
    // 根据代币类型调整滑点
    if (tokenInfo.isFeeOnTransfer) {
        // 基础调整:增加转账费率的2倍作为额外滑点
        adjustedSlippage += (tokenInfo.feeRate || 500) * 2 / 100; // 转换为百分比
        
        // 最小滑点保护
        adjustedSlippage = Math.max(adjustedSlippage, 5); // 至少5%
        
        // 最大滑点限制
        adjustedSlippage = Math.min(adjustedSlippage, 20); // 最多20%
    }
    
    return adjustedSlippage;
}

2. 交换前验证

async function validateFeeTokenSwap(tokenIn, tokenOut, amountIn) {
    const validations = [];
    
    // 检查输入代币
    const tokenInInfo = await checkIfFeeOnTransfer(tokenIn);
    if (tokenInInfo.isFeeOnTransfer) {
        validations.push({
            type: 'warning',
            message: `输入代币 ${tokenIn} 是转账费代币,实际输入可能少于预期`
        });
    }
    
    // 检查输出代币
    const tokenOutInfo = await checkIfFeeOnTransfer(tokenOut);
    if (tokenOutInfo.isFeeOnTransfer) {
        validations.push({
            type: 'warning', 
            message: `输出代币 ${tokenOut} 是转账费代币,实际输出可能少于预期`
        });
    }
    
    // 检查流动性
    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: '交易对不存在'
        });
    } 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: '交易对流动性不足'
            });
        }
    }
    
    return validations;
}

3. 交换结果分析

async function analyzeFeeTokenSwapResult(transactionReceipt, expectedOutput) {
    try {
        // 解析交换事件
        const swapEvents = parseSwapEvents(transactionReceipt);
        
        if (swapEvents.length === 0) {
            return { success: false, error: '未找到交换事件' };
        }
        
        const lastSwap = swapEvents[swapEvents.length - 1];
        const actualOutput = lastSwap.amount0Out.gt(0) ? lastSwap.amount0Out : lastSwap.amount1Out;
        
        // 计算实际滑点
        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(), // 基点
            swapCount: swapEvents.length,
            gasUsed: transactionReceipt.gasUsed
        };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

4. 多跳转账费代币交换

async function multiHopFeeTokenSwap(path, fees, amountIn, userAddress, signer) {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
    
    // 检查路径中的转账费代币
    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("检测到转账费代币:", feeTokens);
    }
    
    // 计算动态滑点
    let totalFeeRate = feeTokens.reduce((sum, token) => sum + (token.feeRate || 500), 0);
    let slippagePercent = Math.max(5, totalFeeRate / 50); // 基于总费率计算滑点
    
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    // 使用支持转账费的函数
    const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
        amountIn,
        0, // 设置为0,因为难以准确预估
        path,
        fees,
        userAddress,
        ethers.constants.AddressZero,
        deadline
    );
    
    return tx;
}

监控和调试

交换监控

async function monitorFeeTokenSwaps() {
    const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
    
    // 监听支持转账费的交换事件
    const filter = router.filters.SwapExactTokensForTokensSupportingFeeOnTransferTokens();
    
    router.on(filter, async (sender, amountIn, amountOutMin, path, fees, to, referrer, deadline, event) => {
        console.log("转账费代币交换:", {
            sender,
            path: path.map(addr => addr.slice(0, 6) + '...'),
            amountIn: ethers.utils.formatEther(amountIn),
            amountOutMin: ethers.utils.formatEther(amountOutMin),
            transactionHash: event.transactionHash
        });
        
        // 分析交换结果
        const receipt = await event.getTransactionReceipt();
        const analysis = await analyzeFeeTokenSwapResult(receipt, amountOutMin);
        console.log("交换分析:", analysis);
    });
}

调试工具

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);
        
        // 解析交易输入
        const decodedInput = this.router.interface.parseTransaction({ data: transaction.data });
        
        // 分析交换路径
        const path = decodedInput.args.path;
        const fees = decodedInput.args.fees;
        
        console.log("交换调试信息:");
        console.log("- 路径:", path);
        console.log("- 费率:", fees);
        console.log("- Gas使用:", receipt.gasUsed.toString());
        
        // 检查每个代币是否为转账费代币
        for (let i = 0; i < path.length; i++) {
            const tokenInfo = await checkIfFeeOnTransfer(path[i]);
            console.log(`- 代币${i} (${path[i]}):`, tokenInfo);
        }
        
        // 分析交换事件
        const swapEvents = parseSwapEvents(receipt);
        console.log("- 交换事件数量:", swapEvents.length);
        
        return {
            transaction,
            receipt,
            decodedInput,
            swapEvents
        };
    }
}

总结

转账费代币支持是JAMM DEX的重要功能,本指南涵盖了:

  1. 转账费代币原理: 工作机制和常见类型

  2. JAMM DEX支持: 专门的支持函数和实现原理

  3. 使用方法: 各种场景下的交换实现

  4. 代币检测: 自动识别转账费代币的方法

  5. 最佳实践: 滑点管理、验证、分析等

  6. 监控调试: 交换监控和问题调试工具

通过正确使用转账费代币支持功能,开发者可以确保这类特殊代币在JAMM DEX上的正常交易。

Last updated