Solidity错误Stack Too Deep

本文详细探讨了在Solidity中遇到的'Stack Too Deep'错误,该错误源于以太坊虚拟机(EVM)的限制。通过分析智能合约的字节码和EVM操作,揭示了错误出现的原因和解决策略,特别是与事件日志相关的触发因素。文章通过实例展示了如何通过调整参数顺序和理解操作码参数的堆栈管理来避免此类错误。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当一个人开始在Solidity编写智能合约时,他/她迟早会遇到一个非常烦人的障碍。“Stack Too Deep”错误。很容易陷入这个陷阱,当发生这种情况时,通常很难找到出路。公平地说,根本原因不在于Solidity本身,而在于以太坊虚拟机(EVM),因此可能会影响编译成EVM的其他语言(即LLL,Serpent,Viper),但这是一个微妙的区别在编写智能合约的日常工作中。

令人惊讶的是,考虑到这可能导致的烦恼程度,很难找到如何处理它的好资源,所以我决定写这篇文章试图对它有所启发,为了我自己的利益和其他任何人谁可能对它绝望。

一般而言,当代码需要访问堆栈中比第16个元素更深的slot(从顶部向下计数)时,似乎会生成此错误。但是,我们如何实现这一目标可以通过多种方式完成。这篇文章的目的不是提供关于如何产生这种错误的完整理论:根据我的经验,有太多的方法可以做到这一点。但它将为一个共同的触发器提供一个很好的理由,并希望让读者更多地了解EVM如何管理它的堆栈。甚至可以将相同的逻辑扩展到发生错误的其他情况,并寻找避免它的方法。

在Solidity中,大多数类型(例如基本类型,例如数字,地址和布尔值,但不是数组,结构和映射)都通过值传递给函数:当调用函数时,堆栈的一部分(即stock frame)被分配用于保存程序在函数返回时返回的地址和函数值类型输入和输出参数的副本。每个参数通常在堆栈中保存一个slot,其中每个slot为256位。

这提供了命中Stack Too Deep错误的最基本方法:总共有超过16个输入和输出参数。但实际上,如果我们希望该函数做一些有用的事情,我们必须非常小心,并且可能必须减少参数的数量。

为了测试这个,我在Remix中创建了一个小合约,如下所示:

pragma solidity ^0.4.24;
contract TestStackError {
  event LogValue(uint);
  function logArg(uint a1) public {
    emit LogValue(a1);
  }
}

Remix非常适合这样的调查,因为我们可以快速编写合约并对其进行查询,但基本上是因为Remix提供了一个功能强大的调试器,其中包含操作码反汇编以及堆栈,内存和存储的完整列表。代码中前后移动也很容易,这是我用任何语言提供的最佳调试体验之一。

这个合约非常简单:它没有状态变量,只有一个函数,它也非常简单。此函数只接受一个参数并记录它。

我将此合约复制到Remix中的新文件,编译并部署它。应该没有错误和警告,因此我转到Run选项卡,然后点击Deploy

然后,我扩展SimpleFunction合约的列表,并在logArg前面的框中输入单个值。我按下按钮并检查控制台中的输出:

如你所见,我输入了值7,并将其作为日志中的唯一元素返回。 虽然日志值得另外发布,但我在这里应该提到一些事情。

这是此调用的JSON格式的日志对象:

logs [
{
  "from": "0xef55bfac4228981e850936aaf042951f7b146e41",
  "topic": "0xfcf771399d75a67a6d0e730ae98d34c40b6bfe6ebf8053b98ddf4da8c2706250",
  "event": "LogValue",
  "args": {
    "0": "7",
    "length": 1
  }
}
  • 日志由emit关键字在solidity中创建,这会引发一个solidity event并对应于LOGn操作码。
  • 日志可以通过离线运行的客户端应用程序进行过滤。过滤器是日志中可用主题的条件。
  • 日志始终具有topic0,这是事件event签名的编码。
  • 可以通过索引参数来创建更多主题。最多可以有3个索引参数。其余的被视为事件数据。

在这个简单的例子中,我们可以很容易地确定只有一个主题0xfcf771399d75a67a6d0e730ae98d34c40b6bfe6ebf8053b98ddf4da2c2706250并且数据显示为日志对象的args成员的一部分。我们还可以验证代码是否按预期工作。

现在让我们测试这个合约的限制,并改变函数以接受最大数量的参数。

pragma solidity ^0.4.24;
contract TestStackError {
  event LogValue(uint);
  function logArg(uint a1, uint a2, uint a3, uint a4,
	uint a5, uint a6, uint a7, uint a8,
	uint a9, uint a10, uint a11, uint a12,
	uint a13, uint a14, uint a15, uint a16
  ) public {
    emit LogValue(a16);
  }
}

我有16个输入变量,没有输出变量,因此我只需要使用16个堆栈槽。 我调用传递值1到1

### Solidity 错误处理方法 #### 使用 `require` 函数 `require` 是一种常用的错误处理机制,主要用于验证输入条件。如果条件不满足,则会触发异常并回滚状态更改。此函数允许传递一条可选的消息作为参数,以便更清晰地描述失败原因。 ```solidity function withdraw(uint amount) public { require(amount <= address(this).balance, "Insufficient funds"); } ``` [^3] #### 应用 `assert` 断言 `assert` 用来检测内部错误或逻辑上的不可能情况。当断言失败时,意味着存在严重的程序缺陷,因此不会返回任何消息给调用者,并消耗所有剩余的 gas。通常只应在绝对确定的情况下使用 `assert````solidity uint storedData; function set(uint x) public { assert(x != 0); // This should never happen. storedData = x; } ``` #### 定义自定义错误类型 从版本 ^0.8.4 开始,Solidity 支持通过关键字 `error` 创建自定义错误对象。这种方式可以减少 gas 成本,因为相比于字符串信息而言,结构化数据更加紧凑。 ```solidity // SPDX-License-Identifier: MIT pragma solidity >=0.8.4; contract VendingMachine { error InsufficientFunds(); function buySnack() external payable { if (msg.value < snackPrice){ revert InsufficientFunds(); } // ... } } ``` --- ### 最佳实践建议 为了确保智能合约的安全性和可靠性,在编写代码期间应当遵循以下几点: - **优先选用 `require` 进行前置条件检查**:对于预期可能发生的外部因素引起的异常状况,应该采用 `require` 来捕获这些问题。 - **谨慎运用 `assert` 处理不可预见的情况**:仅限于那些理论上不应该发生的情形下才考虑使用 `assert`,并且要充分测试以确认其合理性。 - **尽可能利用自定义错误节省gas费用**:相较于传统的带有说明文字的方式来说,新的基于类型的错误表达形式能够有效降低交易成本。 - **保持良好的文档记录习惯**:无论是选择哪种方式进行错误报告,都应该附带足够的上下文信息帮助后续维护人员快速定位问题所在。 [^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值