简介
本文旨在介绍 Solidity 中 gasleft() 函数的行为及其用途。
它是一个内置函数,用于检查合约调用期间的剩余 gas。它是始终存在于全局命名空间中的特殊变量和函数之一,因此不需要被导入。在 Solidity 0.4.21 版本之前,gasleft() 被称为 msg.gas(msg.gas)。
作者
本文由 Jesse Raymond(LinkedIn,Twitter)作为 RareSkills Technical Writing Program 的一部分共同编写。
为什么 gasleft() 很重要
智能合约消耗的 gas 量取决于运行代码的复杂性以及合约调用期间处理的数据量。
如果提供的 gas 不足,交易将因 “out of gas”(gas 耗尽)错误而回滚。正确使用 gasleft() 函数可以防止合约交易耗尽 gas 的情况。让我们在下一节中看一个示例。
防止 out-of-gas 错误的示例
在分发 Ether 时不耗尽 gas
在智能合约中通过循环向多个地址发送 Ether 可能会非常昂贵,尤其是在处理大型地址数组时。
如前所述,如果用于执行交易的 gas 量不足,函数将因 “out of gas” 错误而失败。
然而,可以使用 gasleft() 函数来确保剩余的 gas 足以进行下一次转账,否则就提前退出。
以下代码演示了这一点:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract GasConsumer{
uint constant MINIMUM_AMOUNT = 10_000;
// this is for illustration purposes only, not production
function distributeEther(address[] calldata receivers) external {
for (uint i = 0; i < receivers.length; i++) {
payable(receivers[i]).transfer(1 ether);
if (gasleft() < MINIMUM_AMOUNT) {
return;
}
}
}
receive() external payable {}
}
上述合约中的 “distributeEther” 函数接收一个地址数组,使用 for 循环遍历该数组,并使用 “transfer” 函数向每个地址发送 1 Ether。
“if 语句” 通过检查转账后的 “gas left” 是否小于 10,000(转账 ETH 需要 9,000 gas,其他次要操作码需要额外 1,000 gas),来判断循环中每次 Ether 转账后的剩余 gas 是否足够进行下一次转账。如果小于该值,交易将结束,而不会回滚之前的转账。
(请注意,除非地址是可信的,否则主动推送(push)Ether 并不是一个好主意。恶意的接收者可能会提供一个在接收 Ether 时会触发回滚的智能合约地址)。
基准测试代码
使用 gasleft() 函数测量执行成本
另一个示例是使用 gasleft() 函数来测量某段代码使用的总 gas 量。
以下是在 Remix 中的一个示例:

使用 gasleft 对 Solidity 代码进行基准测试
在这种情况下,gasleft() 函数用于计算使用 “updateArray” 函数将数字附加到 “numArr” 数组时消耗了多少 gas。这不是该函数使用的总 gas 量,而是通过第 30 行的 numArr.push(_num) 代码将数字附加到数组前后的 gas 消耗量。
解释高亮数字
我们将 “gasUsed” 设置为 public 变量,这样可以在函数执行后轻松查看其内容。通过部署 “GasCalc” 合约并调用 “updateArray” 函数来对此进行测试。
该函数返回一个元组,第一个条目的结果为 80,348,即 “initialGas”。
元组中的第二个条目是 “finalGas”,结果为 35,923,这包含了 gasleft() 函数本身的 gas 成本。
通过用 initial gas 减去 final gas,我们可以确定第 30 行的执行成本为 44,425。
gasleft 背后的操作码需要消耗 “2 gas” 来执行
Solidity 是一种高级语言,它被编译成在 Ethereum Virtual Machine (EVM) 上执行的字节码(bytecodes)。
根据 Ethereum 文档,gasleft() 函数的操作码是 GAS(字节码 0x5A),它消耗 “2 gas”。
实际应用
OpenZeppelin 代理 —— 用于将所有 gas 转发给实现合约
在 yul 中使用 gasleft
在 Solidity 智能合约中,也可以通过 yul(内联汇编)以 gas() 的形式访问 gasleft() 函数。
OpenZeppelin 代理合约是一个非常好的实现范例。它被用在 “delegatecall” 函数中,代理使用该函数调用实现合约。gas() 是一种便捷的方法,用于指定在此操作中使用最大可用 gas。

在 Solidity 中使用 gasleft 在 delegate call 中转发所有 gas
链接:OpenZeppelin Proxy Contract
OpenZeppelin Minimal Forwarder —— 用于验证中继者是否发送了足够的 gas 来执行交易
“Relayer”(中继者)是一个链下实体,负责为其他用户的交易支付 gas,该交易会被发送到 “Forwarder”(转发器)合约并由其执行。
当用户向中继者发送请求时,用户会指定包含在交易中的 gas 数量,并对其请求进行数字签名。
然而,中继者可能不会遵守用户请求的 gas limit,而是发送较少的数量。这种攻击在 SWC Registry 中被记录为 SWC-126。
这会引发 gas-griefing 攻击。如果中继者对转发合约的调用成功了,但用户想要的子调用(sub call)失败了,中继者就可以“责怪”用户发送了一笔会回滚的交易,而真正的原因是由于中继者发送的 gas 不足导致子调用耗尽了 gas。
子调用可能会因 revert 或 out-of-gas 错误而失败,但通常不会给出失败原因,只会让布尔类型的 success 变量返回 false。因此,我们不知道子调用的失败是由于 gas 不足,还是由于原始发送者的指令有误。
我们可以使用 “gasleft” 来判断是哪种情况。
当执行 “call” 时,只有 63/64 的 gas 会被转发。这个 63/64 限制是在 EIP 150 中引入的。
子调用完成后,剩余的 gas 量应至少为用户指定的原始上限的 1/64。
如果在子调用之后剩余的 gas 少于所请求 gas 的 1/64,我们就能知道中继者并没有发送他们本应发送的全部 gas。
此处的转发合约会检查是否至少留有原始限制 1/63 的 gas,以此作为安全余量。
使用无效代码是为了让中继者的交易失败,从而明确表明交易失败是由中继者造成的,而不是因为子调用。您可以在此处阅读有关 gas griefing 攻击的更多信息。

链接:OpenZeppelin Minimal Forwarder Contract
Chainlink EthBalance Monitor 合约 —— 用于防止 out-of-gas 错误阻碍 Ether 的分发
这是本文第一个示例(在循环中分发 Ether)的实际应用。与之前的代码相比,此代码中包含更多的业务逻辑,但如果我们关注导致提前退出循环的 “gasleft()” 检查,我们会发现其在本质上是相同的设计。

Chainlink VRFCoordinatorV2 合约 —— 用于获取完成 Chainlink VRFCoordinatorV2 请求所消耗的 gas 量
Chainlink “VRFCoordinatorV2” 智能合约是一个“可验证随机函数(Verifiable Random Function)”协调器,用于在区块链上生成密码学安全的随机数。
该智能合约是用于请求和接收随机数的预言机(oracle)。(参见此处:VRFCoordinatorV2)。
gasleft() 函数被用于该合约的 “calculatePaymentAmount” 函数中,如果节点需要支付更多的 gas 来完成随机数生成,就会向用户收取更高的费用。
在这种情况下,gasleft() 越低,费用就会越高,因为随着消耗更多的 gas,(startGas - gasleft()) 的值会增加。

链接:Chainlink VRFCoordinatorV2 Contract
结论
在本文中,我们讨论了 gasleft() 的各种用例。这些用例包括防止 out-of-gas 错误、对 Solidity 代码执行成本进行基准测试、将所有 gas 转发给实现合约,以及防止中继者 DOS 攻击。
RareSkills Blockchain Bootcamp
请查看我们的高级 blockchain bootcamp 课程,以了解更多关于我们提供的专家级开发者培训的信息。
最初发布于 2023 年 2 月 4 日