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

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

操作流程

  1. 用户先调用ERC20代币合约的approve函数,参数spender填TestCase合约地址,amount填要转账的数量。
  2. 用户再调用TestCase合约的transferFromCallerToContract函数,传入转账数量,即可完成转账。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 22:27:08