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

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修饰符。
修复方案
  1. 将for循环的边界判断从i <= tokenIds.length修改为i < tokenIds.length,避免访问超出数组长度的非法索引。
  2. 在循环执行前新增参数校验,要求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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 00:01:06