IERC20 token.transfer在智能合约内调用持续失败问题排查
合约内调用ERC20转账失败问题
我开发了一个遵循IERC20接口的ERC20代币,在Remix部署到测试网后,直接调用代币的转账函数能正常在地址间转移代币,但在其他合约内调用token.transfer相关函数时始终失败。以下是最简测试案例:部署TestCase合约时传入已部署的ERC20代币地址,合约内有两个实现相同转账逻辑的函数:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.2 <0.9.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract TestCase { IERC20 token; bool tokenIsSmartContract; constructor( address tokenAddress ) { tokenIsSmartContract = tokenAddress.code.length > 0; token = IERC20(tokenAddress); } error InsufficientTokens(); error TokenPaymentFailed(); function transfer_from_caller_to_contract_v1() external { uint sendAmount = 1; uint balance = token.balanceOf(msg.sender); //verify they have enough balance if( balance < sendAmount ) revert InsufficientTokens(); //attempt to send 1 token from msg.sender (ie, the caller of foo()) to this contract address payable to = payable(address(this)); if( !token.transfer(to, sendAmount) ) revert TokenPaymentFailed(); } error FailedToApprove(); error InsufficientAllowance(); function transfer_from_caller_to_contract_v2() external { uint sendAmount = 1; uint balance = token.balanceOf(msg.sender); //verify they have enough balance if( balance < sendAmount ) revert InsufficientTokens(); //I want to send 1 token from the caller of foo() to this contract address payable to = payable(address(this)); //the address that will receive funds address spender = address(this); //I think spender would be this contract address owner = msg.sender; //the holder of the tokens address from = owner; //the owner is the person is the one we want to transfer from //Sets a value amount of tokens as the allowance of spender over the caller's tokens. if( !token.approve(spender, sendAmount) ) revert FailedToApprove(); //Returns the remaining number of tokens that spender will be allowed to spend // on behalf of owner through {transferFrom}. This is zero by default. uint allowed = token.allowance(owner, spender); if( allowed < sendAmount ) revert InsufficientAllowance(); //Moves a value amount of tokens from from to to using the allowance mechanism. // the value is then deducted from the caller's allowance. if( !token.transferFrom(from, to, sendAmount) ) revert TokenPaymentFailed(); } }
问题原因分析
1. transfer_from_caller_to_contract_v1函数逻辑错误
token.transfer(to, sendAmount)的执行逻辑是从调用该函数的地址(即当前TestCase合约地址)转出代币到目标地址,而非从msg.sender转出。你预期的是用户把代币转到合约,但实际是合约尝试用自己的余额转账,若合约没有代币余额,必然失败。
2. transfer_from_caller_to_contract_v2函数逻辑错误
token.approve(spender, sendAmount)是让合约地址授权给spender(同样是合约地址),而非让msg.sender授权给合约。用户的授权必须由用户主动调用代币合约的approve函数完成,合约内部调用approve只会修改合约自身的授权记录,和用户无关。- 由于用户未给合约授权,后续
allowance检查必然不通过,transferFrom也会失败。
修复方案
正确的合约实现
要实现用户通过合约转代币到合约地址,需先让用户完成授权,再由合约调用transferFrom:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.2 <0.9.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract TestCase { IERC20 public immutable token; constructor(address tokenAddress) { token = IERC20(tokenAddress); } error InsufficientTokens(); error TokenTransferFailed(); error InsufficientAllowance(); // 用户需先调用代币合约的approve函数,授权该合约可花费指定数量的代币 function transferFromCallerToContract(uint256 sendAmount) external { uint256 userBalance = token.balanceOf(msg.sender); if (userBalance < sendAmount) revert InsufficientTokens(); uint256 allowance = token.allowance(msg.sender, address(this)); if (allowance < sendAmount) revert InsufficientAllowance(); bool success = token.transferFrom(msg.sender, address(this), sendAmount); if (!success) revert TokenTransferFailed(); } }
操作流程
- 用户先调用ERC20代币合约的
approve函数,参数spender填TestCase合约地址,amount填要转账的数量。 - 用户再调用
TestCase合约的transferFromCallerToContract函数,传入转账数量,即可完成转账。
内容的提问来源于stack exchange,提问作者Frog Monkey
相关产品推荐
相关产品推荐

