• 如何"利用 "EVM 的特殊特性为用户最小化 solidity 智能合约的执行成本
  • 发布于 2个月前
  • 371 热度
    0 评论
  • 雾月
  • 0 粉丝 23 篇博客
  •   
本文将研究以太坊虚拟机(EVM)的内部工作,以说明如何 "利用 "EVM 的特殊特性,为用户最小化 solidity 智能合约的执行成本。社区发布许多关于 solidity 开发者可以利用的知识来设计和开发更安全、更节省 Gas 的智能合约。

本周我们看一下两种不同类型的优化,这些优化相对简单,可以应用于任何代码库,与我们为客户审计的大多数智能合约有关。第一个优化适用于所有版本的 Solidity。本文讨论的第二个优化只对pragma版本0.8.0以上有效。然而,让我们首先在高层次上阐明如何评估任何 EVM 指令的成本。

评估 EVM 指令成本
最核心的是,在 EVM 区块链上为交易引入 "Gas 成本"和为组装区块引入 "Gas 限制 "的理由是:1)引入额外的收入流,以激励 stakers(以前的矿工)确保和验证网络,以及 2)作为保护措施,防止拒绝服务(DoS)攻击,通过 Gas 上限禁止执行计算昂贵的任务(例如,具有显著限制的 "for "循环,否则会延迟区块创建率的中位数)。

为了在区块构建者补偿和有竞争力的计算系统之间取得余额,EVM 区块链根据网络行为者之间的带宽需求动态地调整其区块 Gas 限制。交易 Gas 成本通常发展缓慢,围绕着一个简单的基础;执行交易的计算成本。在中心化和/或传统的计算环境中,这可能看起来很简单。然而,在区块链生态系统中,由于区块链账本的状态变化在验证去中心化网络上执行的指令的节点网络中传播的方式,它有很大不同。

在这篇文章中,我们说明了 "内存" 的隐性成本如何抬高了 EVM 区块链上其他直接交易类型的成本,以及开发者如何优化他们的 dapps 以减少其 Gas 足迹。

EVM 本地变量的隐藏成本
当声明一个作为语句结果的局部变量时,会产生一个隐藏的 Gas 成本,它与我们声明的局部变量所需的 内存量成比例。当从存储空间读取变量(SLOAD),将它们存储到局部变量(参考 MSTORE 内存扩展[5] 增加了隐藏成本),以及在每次利用时读取它们(MLOAD)时,这种额外成本通常会被抵消。

在处理 EVM 的原始指令时,情况就不是这样了。事实上,区块链上的每笔交易都包含一组不可避免的数据。因此,数据集通过原始的 EVM 指令暴露给所有智能合约,这些指令消耗的 Gas 非常小。这是由于它们不需要额外的内存来读取特殊的数据槽,因为它们已经作为 EVM 的区块创建工作流程的一部分在内存中加载。

这些指令集很重要,他们围绕着交易的上下文数据,如msg.sender,block.timestamp,等等。因此,下面的合约实现事实上是低效的。

Context 合约是 OpenZeppelin 引入的一个实现,目的是简化合约的开发过程,可以很容易地升级到元交易兼容的合约。然而,到目前为止,它大多被滥用,并导致各种协议(包括 Aave V2 和 Aave V3)的 Gas 增加到不可忽略的程度,这是 Aave V3 的 "IncentivizedERC20 "实现的具体例子:

在上述函数中,gas 成本包含:_msgSender实现的 msg.sender Gas 成本(操作码 CALLER: 2 gas),以及_msgSender()调用本身(操作码 JUMP: 2gas以及返回变量的内存分配)两次。通过优化上述片段,我们可以将指令的 Gas 成本降低一半:

虽然这种优化本身可能微不足道,但在整个代码库中应用时,它将带来切实的节省。

Solidity 数学上的隐藏成本
隐性 Gas 成本不仅限于 EVM。开发人员需要认识到,Solidity 语言本身在其最新版本semver 8中引入了一些隐性成本,Solidity 默认执行安全算术。鉴于很多应用程序已经对不安全的算术操作进行了安全检查,作为其错误处理工作流程的一部分,内置的安全算术检查变得多余了,因此会产生多余的 Gas 增加。

值得庆幸的是,Solidity 还引入了一种新的代码块声明风格,指示编译器不安全地执行算术操作。unchecked代码块。只要操作被周围的语句和/或条件保证安全执行,就可以巧妙地利用这些代码块来大大减少特定合约的 Gas 成本。作为一个例子,让我们看一下复合CToken实现的_reduceReservesFresh函数的这一段:

在条件 reduceAmount > totalReserves 被评估为 false 之后,totalReservesNew 的计算和分配被执行。这意味着执行环境已经保证了 "totalReserves >= reduceAmount "这一特性,因此 "totalReservesNew "的计算可以在一个unchecked 的代码块中进行,因为它被保证能够正常执行。经过优化,上述代码块应该类似于这样:

另一种避免内置安全算术产生额外 Gas 的方法是在增量操作(++和--)期间使用unchecked。通常在for循环和任何0.8.X后的版本中进行操作都有此问题。每一次增量操作都会进行边界检查,当它们完全是多余的。下面提供一个非常简单的例子来说明这一点:

由于 Solidity 的固有限制,bar.length被保证适合于uint256变量,这意味着对i变量的每个循环执行安全增量将是多余的。为了优化这样的代码块,我们把增量移到 unchecked的代码块的末尾:

结论
EVM 是一个内在复杂的机器,因此已经开发了多种工具来帮助开发者使用高级语言(如 Solidity)在其系统中创建解决方案。然而,在 Solidity 编译器的自由下进行的简化,通常没有很好地转达给开发者社区,因此,程序员最终创造了低效的程序。
用户评论