Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-1930: 具有严格 gas 语义的 CALL。如果 gas 不足则回退。

Authors Ronan Sandford (@wighawag)
Created 2019-04-10
Discussion Link https://github.com/ethereum/EIPs/issues/1930

简单总结

为智能合约添加使用特定数量的 gas 执行调用的能力。如果无法做到,则执行应回退。

摘要

当前的 CALL, DELEGATE_CALL, STATIC_CALL 操作码不强制执行发送的 gas,它们只是将 gas 值视为最大值。这对于需要以精确的 gas 量执行调用的应用程序来说,会带来严重的问题。

例如,对于元交易,合约需要确保调用完全按照签名用户的意图执行。

但对于常见的用例也是如此,例如检查智能合约是否“链上”支持特定的接口(例如通过 EIP-165)。

这里提出的解决方案是添加新的调用语义,以强制执行指定的 gas 量:调用要么以精确的 gas 量进行,要么不执行,并且当前调用回退。

规范

有两种可能性

a) 一种是添加具有更严格 gas 语义的操作码变体

b) 另一种是考虑特定的 gas 值范围(以前从未使用的范围)具有严格的 gas 语义,同时将其他值保持不变

以下是详细的描述

选项 a)

  • 添加 CALL 操作码的新变体,其中指定的 gas 被强制执行,以便如果在调用点剩余的 gas 不足以将指定的 gas 提供给目标,则当前调用回退
  • 添加 DELEGATE_CALL 操作码的新变体,其中指定的 gas 被强制执行,以便如果在调用点剩余的 gas 不足以将指定的 gas 提供给目标,则当前调用回退
  • 添加 STATIC_CALL 操作码的新变体,其中指定的 gas 被强制执行,以便如果在调用点剩余的 gas 不足以将指定的 gas 提供给目标,则当前调用回退
a) 的理由

此解决方案的优点是避免了旧合约受到更改的任何可能性。另一方面,它引入了 3 个新的操作码。通过 EIP-1702,我们可以使旧的操作码过时。

选项 b)

对于所有允许将 gas 传递给另一个合约的操作码,请执行以下操作:

  • 如果最高有效位为 1,则将 31 个较低有效位视为要以严格意义提供给接收合约的 gas 量。因此,像 a) 一样,如果在调用点剩余的 gas 不足以将指定的 gas 提供给目标,则当前调用回退。
  • 如果第二个最高有效位为零,则认为整个值的行为与以前一样,也就是说,它充当最大值,即使没有足够的 gas,也可以将可以提供的 gas 提供给接收合约
b) 的理由

此解决方案依赖于以下事实:没有合约会给出任何大于或等于 0x8000000000000000000000000000000000000000000000000000000000000000 的值

请注意,solidity 例如不使用像 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 这样的值,因为它比传递 gasLeft 更昂贵。

但它的主要好处是不需要额外的操作码。

严格的 gas 语义

准确地说,关于严格的 gas 语义,基于 EIP-150,当前调用必须回退,除非 G >= I x 64/63,其中 G 是调用点剩余的 gas(在扣除调用本身的成本之后),I 是指定的 gas。

因此,代替

availableGas = availableGas - base
gas := availableGas - availableGas/64
...
if !callCost.IsUint64() || gas < callCost.Uint64() {
    return gas, nil
}

参见 https://github.com/ethereum/go-ethereum/blob/7504dbd6eb3f62371f86b06b03ffd665690951f2/core/vm/gas.go#L41-L48

我们将拥有

availableGas = availableGas - base
gas := availableGas - availableGas/64
if !callCost.IsUint64() || gas < callCost.Uint64() {
    return 0, errNotEnoughGas
}

理由

目前,作为这些操作码一部分指定的 gas 只是一个最大值。并且由于 EIP-150 的行为,外部调用可能会被赋予比预期更少的 gas(比作为 CALL 一部分指定的 gas 更少),而当前调用的其余部分则被赋予足够的 gas 以继续并成功。事实上,由于使用 EIP-150,外部调用最多被赋予 G - Math.floor(G/64),其中 G 是 CALL 时的 gasleft(),当前调用的其余部分被赋予 Math.floor(G/64),这在许多情况下对于交易成功来说已经足够了。例如,当 G = 6,400,000 时,交易的其余部分将被赋予 100,000 gas,这在许多情况下已经足够了。

对于要求外部调用仅在有足够 gas 的情况下失败的合约来说,这是一个问题。智能合约钱包和一般的元交易中存在这种需求,其中执行交易的人不是执行数据的签名者。因为在这种情况下,合约需要确保调用完全按照签名用户的意图执行。

但对于简单的用例来说也是如此,例如检查合约是否通过 EIP-165 实现接口。 事实上,正如这样的 EIP 所指定的那样,supporstInterface 方法被限制为使用 30,000 gas,因此从理论上讲,可以确保 throw 不是由于 gas 不足造成的。不幸的是,由于不同的 CALL 操作码的行为方式,合约不能简单地依赖于指定的 gas 值。他们必须通过其他方式确保调用有足够的 gas。

事实上,如果调用者不确保为被调用者提供 30,000 gas 或更多 gas,则被调用者可能会因 gas 不足而 throw(而不是因为它不支持该接口),并且父调用将被赋予最多 476 gas 以继续。这将导致调用者错误地解释为被调用者未实现所讨论的接口。

虽然可以通过根据 EIP-150 在调用之前检查剩余的 gas 和所需的精确 gas 来强制执行此类要求(请参阅该 bug 报告 中提出的解决方案)或在调用之后(请参阅 此处 的本机元交易实现),如果 EVM 允许我们严格指定要给予 CALL 多少 gas,那么将会更好,这样合约实现就不需要密切遵循 EIP-150 行为和当前的 gas 价格。

这也将允许更改 EIP-150 的行为,而无需影响需要此严格 gas 行为的合约。

如前所述,这种严格的 gas 行为对于智能合约钱包和一般的元交易非常重要。 正如 Gnosis safe 的案例中所见,这个问题实际上已经出现在实际应用中,Gnosis safe 没有考虑 EIP-150 的行为,因此无法正确检查 gas,这要求 safe 所有者向其签名消息添加不必要的额外 gas,以避免丢失资金的可能性。请参阅 https://github.com/gnosis/safe-contracts/issues/100

至于 EIP-165,该问题已经存在于 EIP 中提供的示例实现中。请在此处查看问题的详细信息 此处

同样的问题也存在于 OpenZeppelin 实现中,OpenZeppelin 实现是许多人使用的库。它在调用具有 30,000 gas 的 supportsInterface 之前不会执行任何 gas 检查(请参阅 此处),因此容易受到上述问题的攻击。

虽然如今可以通过牢记 EIP-150 来检查 gas 来防止此类问题,但在操作码级别上的解决方案更为优雅。

事实上,目前强制执行发送正确 gas 量的两种可能方法如下:

1) 在调用之前完成的检查

uint256 gasAvailable = gasleft() - E;
require(gasAvailable - gasAvailable / 64  >= `txGas`, "not enough gas provided")
to.call.gas(txGas)(data); // CALL

其中 E 是从调用 gasleft() 到实际调用之间的操作所需的 gas 加上调用本身的 gas 成本。 虽然可以简单地高估 E 以防止在没有为当前调用提供足够 gas 的情况下执行调用,但最好让 EVM 自己完成精确的工作。随着 gas 价格的不断发展,拥有一种机制来确保将特定数量的 gas 传递给调用非常重要,因此可以使用此类机制,而无需依赖特定的 gas 价格。

2) 在调用之后完成的检查:

to.call.gas(txGas)(data); // CALL
require(gasleft() > txGas / 63, "not enough gas left");

此解决方案不需要计算 E 值,因此不依赖于特定的 gas 价格(EIP-150 的行为除外),因为如果给予调用的 gas 不足并因此而失败,则上述条件将始终失败,从而确保当前调用将回退。 但是,如果给出的 gas 较少并且外部调用 EARLY 回退或成功(因此调用后剩余的 gas > txGas / 63),则此检查仍然会通过。 如果作为 CALL 的一部分执行的代码由于针对所提供的 gas 进行检查而回退,则这可能是一个问题。就像元交易中的元交易一样。

与之前的解决方案类似,EVM 机制会更好。

向后兼容性

对于规范 a):向后兼容,因为它引入了新的操作码。

对于规范 b):向后兼容,因为它使用现有合约使用的范围之外的值范围(有待验证)

测试用例

实现

尚未完全实现。但请参阅规范以获取 geth 中的示例。

参考文献

  1. EIP-150, ./eip-150.md

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Ronan Sandford (@wighawag), "EIP-1930: 具有严格 gas 语义的 CALL。如果 gas 不足则回退。 [DRAFT]," Ethereum Improvement Proposals, no. 1930, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1930.