合约调用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); } }
问题原因分析
- 授权逻辑缺陷:合约每次调用swap前都直接执行
approve(router, _amount),部分ERC20代币实现禁止非零授权值直接覆盖,必须先将授权设为0再重新授权,否则approve调用会失败,导致后续swap无法执行。 - Gas估算偏差:带手续费的代币在转账时会触发额外逻辑(如自动扣除手续费并转账到指定地址),Remix的默认Gas估算无法覆盖这些额外操作,导致Gas不足触发回滚。
- 重复授权冗余:调试中出现的额外授权调用,正是因为每次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
相关产品推荐
相关产品推荐

