ERC20 insufficient allowance错误排查:Lottery合约placeABid函数问题
调用Lottery合约的placeABid函数时触发「ERC20 insufficient allowance」回滚错误,目标是实现用户出价后将对应数量代币转回LotteryToken.owner()的逻辑,相关代码如下:
LotteryToken.sol
//SPDX-License-Identifier: MIT import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; pragma solidity ^0.8.4; contract LotteryToken is Ownable, ERC20{ mapping(address=>uint)private _balances; constructor(string memory name, string memory symbol, uint totalSupply)ERC20(name, symbol){ _mint(msg.sender, totalSupply); _balances[msg.sender] = totalSupply; } function increaseAllowance(address spender, uint256 addedValue) public virtual override returns (bool) { address owner = owner(); _approve(owner, spender, allowance(owner, spender) + addedValue); return true; } function _transfer( address _from, address _to, uint _amount )internal virtual override{ _balances[_from] -= _amount; _balances[_to] += _amount; super._transfer(_from, _to, _amount); } function balanceOf(address account)public view virtual override returns(uint){ return _balances[account]; } }
Lottery.sol
//SPDX-License-Identifier: MIT import "@openzeppelin/contracts/access/Ownable.sol"; import "./LotteryToken.sol"; import "./LotteryInfo.sol"; pragma solidity ^0.8.4; contract Lottery is Ownable{ LotteryInfo private lotteryInfo; LotteryToken private lotteryToken; event BidCorrect(address bidder, uint tokenId, uint defaultBid); constructor(){ } function initLotteryTokenContract(address _lotteryTokenContract)public onlyOwner{ lotteryToken = LotteryToken(_lotteryTokenContract); lotteryToken.increaseAllowance(address(this), lotteryToken.balanceOf(lotteryToken.owner())); } function initLotteryInfoContract(address _lotteryInfoContract)public onlyOwner{ lotteryInfo = LotteryInfo(_lotteryInfoContract); } function setItemToWin( string memory _name, uint _defaultBid, uint _endingTimeLottery )public onlyOwner{ require(_endingTimeLottery > block.timestamp, "Cannot set a date in the Past!"); lotteryInfo.setItemToWin(_name, _defaultBid, _endingTimeLottery); } function buyTokens(uint _quantity)external payable{ uint singleTokenPrice = 0.02 ether; uint finalPrice = singleTokenPrice * _quantity; require(msg.sender != owner() && msg.sender != lotteryToken.owner(), "Admin Cannot Buy Tokens!"); require(msg.value == finalPrice, "Please set the right price!"); payable(owner()).transfer(msg.value); address lotterytokenOwner = lotteryToken.owner(); lotteryToken.transferFrom(lotterytokenOwner, msg.sender, _quantity); } function placeABid(uint _tokenIds)external{ require(block.timestamp < lotteryInfo.getEndingTimeLottery(_tokenIds), "Lottery is closed!"); require(msg.sender != owner() && msg.sender != lotteryToken.owner(), "Admin Cannot Place A Bid!"); uint requiredTokenAmounts = lotteryInfo.getDefaultItemBid(_tokenIds); require(lotteryToken.balanceOf(msg.sender) >= requiredTokenAmounts, "No Necessary Funds To Partecipate!"); address lotteryTokenOwner = lotteryToken.owner(); lotteryToken.transferFrom(msg.sender, lotteryTokenOwner, requiredTokenAmounts); lotteryInfo.updateBid(_tokenIds, requiredTokenAmounts); lotteryInfo.updateAddress(_tokenIds, msg.sender); emit BidCorrect(msg.sender, _tokenIds, requiredTokenAmounts); } }
错误原因
placeABid中的授权缺失:
函数内执行lotteryToken.transferFrom(msg.sender, lotteryTokenOwner, requiredTokenAmounts),该操作要求用户(msg.sender)必须预先授权Lottery合约可以从其账户中转走对应数量的代币。当前代码未处理用户授权步骤,用户未授权时直接触发「insufficient allowance」错误。LotteryToken中increaseAllowance函数逻辑错误:
该函数硬编码使用owner()(LotteryToken合约所有者)作为授权主体,而非调用函数的msg.sender。这违背ERC20授权设计逻辑——授权应由代币持有者主动发起,当前实现会导致只有LotteryToken所有者能通过该函数授权,其他地址调用时会错误操作所有者的授权额度。冗余的代币余额管理:
LotteryToken自行实现_balances映射、balanceOf和_transfer函数,但ERC20合约已内置这些功能。重复实现不仅多余,还可能导致余额数据不一致(如构造函数中_mint已更新ERC20余额,又手动赋值_balances[msg.sender] = totalSupply,后续修改易出问题)。
修复方案
1. 修正LotteryToken的increaseAllowance函数
恢复OpenZeppelin ERC20默认逻辑,让授权由调用者(代币持有者)发起:
function increaseAllowance(address spender, uint256 addedValue) public virtual override returns (bool) { address owner = _msgSender(); _approve(owner, spender, allowance(owner, spender) + addedValue); return true; }
2. 移除LotteryToken中冗余的余额管理代码
删除自定义的_balances映射、balanceOf和_transfer函数,直接使用ERC20内置实现:
// 移除以下代码块: mapping(address=>uint)private _balances; function _transfer( address _from, address _to, uint _amount )internal virtual override{ _balances[_from] -= _amount; _balances[_to] += _amount; super._transfer(_from, _to, _amount); } function balanceOf(address account)public view virtual override returns(uint){ return _balances[account]; }
3. 处理用户出价前的授权逻辑
- 前端层面:在用户调用
placeABid前,引导用户调用LotteryToken的approve或increaseAllowance函数,授权Lottery合约转走至少requiredTokenAmounts数量的代币。 - 合约层面:在
placeABid中增加授权检查,若授权不足则回滚并提示:
function placeABid(uint _tokenIds)external{ require(block.timestamp < lotteryInfo.getEndingTimeLottery(_tokenIds), "Lottery is closed!"); require(msg.sender != owner() && msg.sender != lotteryToken.owner(), "Admin Cannot Place A Bid!"); uint requiredTokenAmounts = lotteryInfo.getDefaultItemBid(_tokenIds); require(lotteryToken.balanceOf(msg.sender) >= requiredTokenAmounts, "No Necessary Funds To Partecipate!"); // 新增授权检查 require(lotteryToken.allowance(msg.sender, address(this)) >= requiredTokenAmounts, "Insufficient allowance to place bid"); address lotteryTokenOwner = lotteryToken.owner(); lotteryToken.transferFrom(msg.sender, lotteryTokenOwner, requiredTokenAmounts); lotteryInfo.updateBid(_tokenIds, requiredTokenAmounts); lotteryInfo.updateAddress(_tokenIds, msg.sender); emit BidCorrect(msg.sender, _tokenIds, requiredTokenAmounts); }
内容的提问来源于stack exchange,提问作者Matteo Gigli

