Solidity含for循环的payable质押函数调用回滚报错排查
问题背景
我编写了一款包含payable类型函数的智能合约,使用Solidity版本为^0.8.0,相关代码如下:
合约构造函数
constructor( address payable _NFT_CONTRACT, address payable _TOKEN_CONTRACT) payable { NFT_CONTRACT = IERC721(_NFT_CONTRACT); TOKEN_CONTRACT = IERC20(_TOKEN_CONTRACT); }
stake质押函数
function stake(uint256[] calldata tokenIds, uint256[] calldata powers) external payable { for (uint i = 0; i <= tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; uint256 power = powers[i]; require(NFT_CONTRACT.ownerOf(tokenId) == msg.sender, "You can only stake your own token"); require(vault[tokenId].tokenId == 0, "You can only stake once"); NFT_CONTRACT.safeTransferFrom(msg.sender, address(this), tokenId); vault[tokenId] = Stake({ tokenId: tokenId, timestamp: block.timestamp, owner: msg.sender, power: power }); userStacks[msg.sender].push(tokenId); hasPaid[tokenId] = 0; } }
注:合约中定义了Stake结构体,以及vault、userStacks、hasPaid三个映射变量,相关定义代码如下:
struct Stake { address owner; uint256 tokenId; uint256 timestamp; uint256 power; } mapping(uint256 => Stake) public vault; mapping(address => uint256[]) public userStacks; mapping(uint256 => uint256) public hasPaid;
故障现象
移除stake函数中的for loop后,函数可以完全正常执行;保留循环逻辑时调用函数会触发回滚,报错信息如下:
revert The transaction has been reverted to the initial state. Note: The called function should be payable if you send value and the value you send should be less than your current balance. Debug the transaction to get more information.
测试时传入的参数示例为tokenIds=[1,2]、powers=[4,5]。
回滚原因
回滚的核心原因是for循环边界判断逻辑错误,触发了Solidity 0.8.0版本默认开启的数组越界检查:
- 传入的测试数组长度为2,数组成员的合法索引是0、1。循环条件写为
i <= tokenIds.length时,会多执行一次循环,当i=2时尝试访问tokenIds[2]和powers[2],超出数组有效索引范围,Solidity 0.8.0之后内置了数组越界、整数溢出的自动校验,越界访问会直接触发交易回滚。 - 现有代码缺少两个入参数组的长度一致性校验,如果用户传入长度不匹配的
tokenIds和powers数组,同样会触发越界回滚。 - 报错信息中提到的payable相关内容是EVM通用回滚提示,和本次故障无关,函数已经正确添加了
payable修饰符。
修复方案
- 将for循环的边界判断从
i <= tokenIds.length修改为i < tokenIds.length,避免访问超出数组长度的非法索引。 - 在循环执行前新增参数校验,要求
tokenIds.length == powers.length,避免两个数组长度不匹配导致的越界问题。
修复后的stake函数代码如下:
function stake(uint256[] calldata tokenIds, uint256[] calldata powers) external payable { // 新增数组长度一致性校验 require(tokenIds.length == powers.length, "Array length mismatch"); // 修正循环边界 for (uint i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; uint256 power = powers[i]; require(NFT_CONTRACT.ownerOf(tokenId) == msg.sender, "You can only stake your own token"); require(vault[tokenId].tokenId == 0, "You can only stake once"); NFT_CONTRACT.safeTransferFrom(msg.sender, address(this), tokenId); vault[tokenId] = Stake({ tokenId: tokenId, timestamp: block.timestamp, owner: msg.sender, power: power }); userStacks[msg.sender].push(tokenId); hasPaid[tokenId] = 0; } }
内容的提问来源于stack exchange,提问作者Platinum
相关产品推荐
相关产品推荐

