You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

合约调用SwapExactTokensForTokensSupportingFeeOnTransferTokens交易回滚求助

合约调用带手续费代币兑换函数回滚问题排查与解决

问题描述

调用合约中的SwapExactTokensForTokensSupportingFeeOnTransferTokens进行带转账手续费的代币兑换时,交易回滚报错,但合约拥有足够余额。直接在交易所执行同类交易可正常完成。通过调试发现合约发起的交易多了一笔授权调用,且Remix提交时出现Gas估算错误。合约中SwapExactTokensForTokens函数调用正常,合约代码片段如下:

pragma solidity >=0.8.14;

interface IERC20 {
    function totalSupply() external view returns (uint);

    function balanceOf(address account) external view returns (uint);

    function transfer(address recipient, uint amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint amount) external returns (bool);

    function transferFrom(address sender, address recipient, uint amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
}

interface IUniswapV2Router {
    function getAmountsOut(uint256 amountIn, address[] memory path) external view returns (uint256[] memory amounts);

    function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external returns (uint256[] memory amounts);

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external returns (uint256[] memory amounts);
}


contract SwapTradeExecution {
    address owner;

    constructor(){
        // Set the owner to the account that deployes the contract
        owner = msg.sender;
    }

    modifier onlyOwner() {
        // Only owner can execute some functions
        require(msg.sender == owner, "Only the owner is allowed to execute");
        _;
    }

    function swapFeeOn(address router, address _tokenIn, address _tokenOut, uint256 _amount) internal {
        // Single swap function for uniswap v2 swap.
        IERC20(_tokenIn).approve(router, _amount);
        address[] memory path;
        path = new address[](2);
        path[0] = _tokenIn;
        path[1] = _tokenOut;
        uint deadline = block.timestamp + 300;
        IUniswapV2Router(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(_amount, 0, path, address(this), deadline);
    }

    function swapExternal(address router, address _tokenIn, address _tokenOut, uint256 _amount, uint256 minAmount) external onlyOwner {
        // Single swap function for uniswap v2 swap.
        IERC20(_tokenIn).approve(router, _amount);
        address[] memory path;
        path = new address[](2);
        path[0] = _tokenIn;
        path[1] = _tokenOut;
        uint deadline = block.timestamp + 300;
        IUniswapV2Router(router).swapExactTokensForTokens(_amount, minAmount, path, address(this), deadline);
    }


    function swapFeeOnExternal(address router, address _tokenIn, address _tokenOut, uint256 _amount,uint256 minAmount) external onlyOwner {
        // Single swap function for uniswap v2 swap.
        IERC20(_tokenIn).approve(router, _amount);
        address[] memory path;
        path = new address[](2);
        path[0] = _tokenIn;
        path[1] = _tokenOut;
        uint deadline = block.timestamp + 300;
        IUniswapV2Router(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(_amount, minAmount, path, address(this), deadline);
    }

    function tokenBalance(address token) public view returns (uint256) {
        IERC20 token = IERC20(token);
        return token.balanceOf(address(this));
    }

    function withdrawFunds(address token) external onlyOwner {
        IERC20 token = IERC20(token);
        uint balance = token.balanceOf(address(this));
        require(balance > 0, "No avaliable fund to withdraw");
        token.transfer(msg.sender, balance);
    }
}

问题原因分析

  1. 授权逻辑缺陷:合约每次调用swap前都直接执行approve(router, _amount),部分ERC20代币实现禁止非零授权值直接覆盖,必须先将授权设为0再重新授权,否则approve调用会失败,导致后续swap无法执行。
  2. Gas估算偏差:带手续费的代币在转账时会触发额外逻辑(如自动扣除手续费并转账到指定地址),Remix的默认Gas估算无法覆盖这些额外操作,导致Gas不足触发回滚。
  3. 重复授权冗余:调试中出现的额外授权调用,正是因为每次swap都重复执行approve,而未检查现有授权额度是否足够。

解决方案

1. 修复授权逻辑

优化approve调用逻辑,先检查当前授权额度,仅在额度不足时重新授权,且处理非零授权覆盖问题:

function swapFeeOnExternal(address router, address _tokenIn, address _tokenOut, uint256 _amount,uint256 minAmount) external onlyOwner {
    IERC20 tokenIn = IERC20(_tokenIn);
    uint256 currentAllowance = tokenIn.allowance(address(this), router);
    
    // 仅当当前授权小于需要的金额时,才重新授权
    if (currentAllowance < _amount) {
        // 若已有非零授权,先重置为0(兼容部分ERC20的限制)
        if (currentAllowance > 0) {
            tokenIn.approve(router, 0);
        }
        tokenIn.approve(router, _amount);
    }

    address[] memory path;
    path = new address[](2);
    path[0] = _tokenIn;
    path[1] = _tokenOut;
    uint deadline = block.timestamp + 300;
    IUniswapV2Router(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(_amount, minAmount, path, address(this), deadline);
}

也可以直接授权最大额度(type(uint256).max),避免频繁调用approve:

tokenIn.approve(router, type(uint256).max);

2. 手动设置Gas限额

在Remix或钱包提交交易时,手动调高Gas限额(建议比估算值多20%-50%),覆盖带手续费代币的额外操作消耗。

3. 验证代币手续费机制

确认目标代币的手续费扣除逻辑,确保合约在调用swap前的余额,扣除手续费后仍能满足_amount的转账要求。

内容的提问来源于stack exchange,提问作者user19820596

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 12:33:16