ERC20合约claimFreeToken函数转账无报错未到账问题排查
问题根因
代码存在两处核心逻辑错误,完全不符合“从合约给用户发免费币”的预期:
- 转账转出主体完全错误。OpenZeppelin标准ERC20实现里的对外
transfer(to, amount)方法,默认是从调用这个方法的msg.sender地址扣钱,转给to地址。你在claimFreeToken里直接调用transfer(msg.sender, 1000*10**decimals())时,这个transfer的msg.sender其实是调用claim函数的外部用户,实际执行的逻辑是「从用户自己的地址转1000币到用户自己的地址」,和合约地址持有的代币余额完全没有关系,你盯着合约余额看自然会觉得“合约钱不够也不报错”。 - 没有配置正确的发币来源。整个合约仅在构造时把全部初始代币铸造给了部署者,合约地址本身初始余额为0,你既没有写“用户领取时铸造新币”的逻辑,也没有正确指定从合约地址划转代币的逻辑,根本不可能给用户转进去币。
你测试时如果用部署合约的地址调用claim,因为部署者本身持有初始发行的代币,自己转自己不会触发余额不足报错,但余额不会有任何变化,就会出现“不报错也收不到币”的现象;如果是零余额的普通用户调用,其实会触发余额不足回滚,但这个报错是因为用户自己没币,和合约地址余额无关。
修复方案
根据你的业务场景选一种实现即可:
方案1:领取时直接铸造新币(适合无硬顶的免费空投场景)
不需要提前给合约转币,用户领取时直接用内部_mint方法给用户发新币:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestToken is ERC20 { constructor(uint256 initialSupply) ERC20("Test Token", "TET") { _mint(msg.sender, initialSupply * (10**decimals())); } function claimFreeToken() public payable { _mint(msg.sender, 1000 * (10**decimals())); } }
方案2:从合约预存的空投池转币(适合有固定总量的场景)
如果要保留固定总量,提前把空投额度转到合约地址,领取时直接调用内部_transfer方法,指定从合约地址转币给用户(不要直接调外部transfer,否则会再次出现转出主体错误):
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestToken is ERC20 { constructor(uint256 initialSupply) ERC20("Test Token", "TET") { _mint(msg.sender, initialSupply * (10**decimals())); // 部署后需要部署者把计划用于空投的代币手动转到当前合约地址,作为空投储备 } function claimFreeToken() public payable { _transfer(address(this), msg.sender, 1000 * (10**decimals())); } }
补充提示:以上两种修复后的代码都没有加防重复领取限制,用户可以无限制调用claim撸代币,正式使用需要新增mapping记录地址领取状态。
内容的提问来源于stack exchange,提问作者Ali Can Almaçimeni
相关产品推荐
相关产品推荐

