此前和几位朋友交流过智能合约外部调用的问题,有点久了;最近开始有些时间,简单整理记录下
外部调用有好几种指令,下面以最常见的 CALL
为例
问题
讨论最多的是 CALL
指令的参数0 gas
具体的作用,比如:
问题一:参数0 是否无用,为什么 opCall()
中直接 pop 掉了?
func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
stack := scope.Stack
// Pop gas. The actual gas in interpreter.evm.callGasTemp.
// We can use this as a temporary value
temp := stack.pop()
gas := interpreter.evm.callGasTemp
// Pop other call parameters.
addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
// Too Long Not Listed
// ...
}
问题二:此前 Paradigm CTF 2021: BabySandbox 题解,能否稍做解释?
问题三:题解测试有效,但为什么修改原题,CALL
时 0x4000 改为 0x30000 的话,题解无效?
pragma solidity 0.7.0;
contract BabySandbox {
function run(address code) external payable {
assembly {
// if we're calling ourselves, perform the privileged delegatecall
if eq(caller(), address()) {
switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
// ensure enough gas
if lt(gas(), 0xf000) {
revert(0x00, 0x00)
}
// load calldata
calldatacopy(0x00, 0x00, calldatasize())
// run using staticcall
// if this fails, then the code is malicious because it tried to change state
if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
revert(0x00, 0x00)
}
// if we got here, the code wasn't malicious
// run without staticcall since it's safe
switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
// revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
}
}
定义
黄皮书关于 gas 机制的介绍,确实比较散乱..
要解释上面的问题,首先需要理解 CALL
的定义:
\mathbf{i} \equiv \boldsymbol{\mu}_{\mathbf{m}}[ \boldsymbol{\mu}_{\mathbf{s}}[3] \dots (\boldsymbol{\mu}_{\mathbf{s}}[3] + \boldsymbol{\mu}_{\mathbf{s}}[4] - 1) ]
\begin{aligned}
(\boldsymbol{\sigma}', g', A^+, \mathbf{o}) \equiv \begin{cases}{\Theta}(\boldsymbol{\sigma}, I_{\mathrm{a}}, I_{\mathrm{o}}, t, t, C_{\text{\tiny CALLGAS}}(\boldsymbol{\mu}), I_{\mathrm{p}}, \boldsymbol{\mu}_{\mathbf{s}}[2], \boldsymbol{\mu}_{\mathbf{s}}[2], \mathbf{i}, I_{\mathrm{e}} + 1, I_{\mathrm{w}}) & \text{if} \ \boldsymbol{\mu}_{\mathbf{s}}[2] \leqslant \boldsymbol{\sigma}[I_{\mathrm{a}}]_{\mathrm{b}} \;\wedge I_{\mathrm{e}} < 1024 \\ (\boldsymbol{\sigma}, g, \varnothing, ()) & \text{otherwise} \end{cases}
\end{aligned}
n \equiv \min(\{ \boldsymbol{\mu}_{\mathbf{s}}[6], \lVert \mathbf{o} \rVert\})
\boldsymbol{\mu}'_{\mathbf{m}}[ \boldsymbol{\mu}_{\mathbf{s}}[5] \dots (\boldsymbol{\mu}_{\mathbf{s}}[5] + n - 1) ] = \mathbf{o}[0 \dots (n - 1)]
\boldsymbol{\mu}'_{\mathbf{o}} = \mathbf{o}
\boldsymbol{\mu}'_{\mathrm{g}} \equiv \boldsymbol{\mu}_{\mathrm{g}} + g'
\boldsymbol{\mu}'_{\mathbf{s}}[0] \equiv x
A' \equiv A \Cup A^+
t \equiv \boldsymbol{\mu}_{\mathbf{s}}[1] \bmod 2^{160}
where x=0
if the code execution for this operation failed due to an {exceptional\ halting}
(or for a \text{\small REVERT}
) \boldsymbol{\sigma}' = \varnothing
or if \boldsymbol{\mu}_{\mathbf{s}}[2] > \boldsymbol{\sigma}[I_{\mathrm{a}}]_{\mathrm{b}}
(not enough funds) or I_{\mathrm{e}} = 1024
(call depth limit reached); x=1
otherwise.
\boldsymbol{\mu}'_{\mathrm{i}} \equiv M(M(\boldsymbol{\mu}_{\mathrm{i}}, \boldsymbol{\mu}_{\mathbf{s}}[3], \boldsymbol{\mu}_{\mathbf{s}}[4]), \boldsymbol{\mu}_{\mathbf{s}}[5], \boldsymbol{\mu}_{\mathbf{s}}[6])
Thus the operand order is: gas, to, value, in offset, in size, out offset, out size.
C_{\text{\tiny CALL}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) + C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu})
C_{\text{\tiny CALLGAS}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv \begin{cases} C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) + G_{\mathrm{callstipend}} & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[2] \neq 0 \\ C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) & \text{otherwise} \end{cases}
C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv \begin{cases} \min\{ L(\boldsymbol{\mu}_{\mathrm{g}} - C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu})), \boldsymbol{\mu}_{\mathbf{s}}[0] \} & \text{if} \quad \boldsymbol{\mu}_{\mathrm{g}} \ge C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \\ \boldsymbol{\mu}_{\mathbf{s}}[0] & \text{otherwise}\end{cases}
C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv G_{\mathrm{call}} + C_{\text{\tiny XFER}}(\boldsymbol{\mu}) + C_{\text{\tiny NEW}}(\boldsymbol{\sigma}, \boldsymbol{\mu})
C_{\text{\tiny XFER}}(\boldsymbol{\mu}) \equiv \begin{cases}G_{\mathrm{callvalue}} & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[2] \neq 0 \\0 & \text{otherwise} \end{cases}
C_{\text{\tiny NEW}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv \begin{cases} G_{\mathrm{newaccount}} & \text{if} \quad \mathtt{DEAD}(\boldsymbol{\sigma}, \boldsymbol{\mu}_{\mathbf{s}}[1] \bmod 2^{160}) \wedge \boldsymbol{\mu}_{\mathbf{s}}[2] \neq 0 \\ 0 & \text{otherwise}\end{cases}
资料
除了 CALL
自身定义外,可能还需要参考附录:
Appendix G. Fee Schedule
Appendix H. Virtual Machine Specification H.1. Gas Cost
填坑
Paradigm CTF 2021: BabySandbox 题解挖了个坑,引出上面的问题二和问题三
这里尝试用另外一个坑的方式作为例子,算是把两个坑填一填~
在 Ethernaut 第13题 Gatekeeper One 题解中,有提到对某些 OPCODE 的 GAS 存在疑惑
比如题解中的测试交易,gas 消耗状况如下
Step | PC | Operation | Gas | GasCost | Depth |
---|---|---|---|---|---|
[131] | 377 | EXTCODESIZE | 2976410 | 2600 | 1 |
[132] | 378 | ISZERO | 2973810 | 3 | 1 |
[133] | 379 | DUP1 | 2973807 | 3 | 1 |
[134] | 380 | ISZERO | 2973804 | 3 | 1 |
[135] | 381 | PUSH2 | 2973801 | 3 | 1 |
[136] | 384 | JUMPI | 2973798 | 10 | 1 |
[137] | 389 | JUMPDEST | 2973788 | 1 | 1 |
[138] | 390 | POP | 2973787 | 2 | 1 |
[139] | 391 | DUP8 | 2973785 | 3 | 1 |
[140] | 392 | CALL | 2973782 | 2891523 | 1 |
[141] | 0 | PUSH1 | 2891423 | 3 | 2 |
注意 [140],在准备调用 CALL
前,堆栈和内存如下
结合 CALL
定义的相关公式和上面截图,可知此时
\boldsymbol{\mu}_{\mathbf{s}}[0] = 0x2c1e9f = 2891423
\boldsymbol{\mu}_{\mathbf{s}}[2] = 0
\boldsymbol{\mu}_{\mathrm{g}} = 2973782
问题如下:
为什么 [141] 的 Gas 为 2891423,而 [140] 的 GasCost 为 2891523,这两个数字是怎么来的?
解答
上面例子中 (\boldsymbol{\mu}_{\mathbf{s}}[3], \boldsymbol{\mu}_{\mathbf{s}}[4]) \gt (\boldsymbol{\mu}_{\mathbf{s}}[5], \boldsymbol{\mu}_{\mathbf{s}}[6])
因此没有扩展内存,即内存相关的 gas 为 0
--
推导1 C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = 100
已知公式
C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv G_{\mathrm{call}} + C_{\text{\tiny XFER}}(\boldsymbol{\mu}) + C_{\text{\tiny NEW}}(\boldsymbol{\sigma}, \boldsymbol{\mu})
C_{\text{\tiny XFER}}(\boldsymbol{\mu}) \equiv \begin{cases}
G_{\mathrm{callvalue}} & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[2] \neq 0 \\
0 & \text{otherwise}
\end{cases}
C_{\text{\tiny NEW}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv \begin{cases}
G_{\mathrm{newaccount}} & \text{if} \quad \mathtt{DEAD}(\boldsymbol{\sigma}, \boldsymbol{\mu}_{\mathbf{s}}[1] \bmod 2^{160}) \wedge \boldsymbol{\mu}_{\mathbf{s}}[2] \neq 0 \\
0 & \text{otherwise}
\end{cases}
又有 \boldsymbol{\mu}_{\mathbf{s}}[2]
为 0
因此 C_{\text{\tiny XFER}}(\boldsymbol{\mu}) = 0
且 C_{\text{\tiny NEW}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = 0
因此 C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = G_{\mathrm{call}} + 0 + 0
再查看编译得到的 OPCODE
GatekeeperOne(target).enter.gas(sendGas)(_gateKey);
上面代码会先通过 EXTCODESIZE
检查 target
是否存在源码,然后 CALL
时再对 target
发起消息调用。
根据 EIP-2929: Gas cost increases for state access opcodes
前面 [131] 的 EXTCODESIZE
首次对该地址操作,消耗 2600 gas;因此接下来 [141] CALL
时,只需要消耗 100 gas,即 G_{\mathrm{call}} = 100
因此 C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = G_{\mathrm{call}} + 0 + 0 = 100
// github.com/ethereum/go-ethereum@v1.10.6/params/protocol_params.go
const (
ColdAccountAccessCostEIP2929 = uint64(2600) // COLD_ACCOUNT_ACCESS_COST
WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST
)
// github.com/ethereum/go-ethereum@v1.10.6/core/vm/eips.go
func enable2929(jt *JumpTable) {
jt[CALL].constantGas = params.WarmStorageReadCostEIP2929
jt[CALL].dynamicGas = gasCallEIP2929
}
// github.com/ethereum/go-ethereum@v1.10.6/core/vm/operations_acl.go
var (
gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall)
)
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
addr := common.Address(stack.Back(1).Bytes20())
// Check slot presence in the access list
warmAccess := evm.StateDB.AddressInAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
// the cost to charge for cold access, if any, is Cold - Warm
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
if !warmAccess {
evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available
// gas for call
if !contract.UseGas(coldCost) {
return 0, ErrOutOfGas
}
}
// Now call the old calculator, which takes into account
// - create new account
// - transfer value
// - memory expansion
// - 63/64ths rule
gas, err := oldCalculator(evm, contract, stack, mem, memorySize)
if warmAccess || err != nil {
return gas, err
}
// In case of a cold access, we temporarily add the cold charge back, and also
// add it to the returned gas. By adding it to the return, it will be charged
// outside of this function, as part of the dynamic gas, and that will make it
// also become correctly reported to tracers.
contract.Gas += coldCost
return gas + coldCost, nil
}
}
--
推导2 C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = 2891423
已知公式
C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv \begin{cases}
\min\{ L(\boldsymbol{\mu}_{\mathrm{g}} - C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu})), \boldsymbol{\mu}_{\mathbf{s}}[0] \} & \text{if} \quad \boldsymbol{\mu}_{\mathrm{g}} \ge C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu})\\
\boldsymbol{\mu}_{\mathbf{s}}[0] & \text{otherwise}
\end{cases}
其中
(318)
L(n) \equiv n - \lfloor n / 64 \rfloor
The Dark Side of Ethereum 1/64th CALL Gas Reduction
// github.com/ethereum/go-ethereum@v1.10.6/core/vm/gas.go
// callGas returns the actual gas cost of the call.
//
// The cost of gas was changed during the homestead price change HF.
// As part of EIP 150 (TangerineWhistle), the returned gas is gas - base * 63 / 64.
func callGas(isEip150 bool, availableGas, base uint64, callCost *uint256.Int) (uint64, error) {
if isEip150 {
availableGas = availableGas - base
gas := availableGas - availableGas/64
// If the bit length exceeds 64 bit we know that the newly calculated "gas" for EIP150
// is smaller than the requested amount. Therefore we return the new gas instead
// of returning an error.
if !callCost.IsUint64() || gas < callCost.Uint64() {
return gas, nil
}
}
if !callCost.IsUint64() {
return 0, ErrGasUintOverflow
}
return callCost.Uint64(), nil
}
参考截图,这里 \boldsymbol{\mu}_{\mathbf{s}}[0]
为 2891423,\boldsymbol{\mu}_{\mathrm{g}}
为 2973782,且如上所述 C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu})
为 100
因此 C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = \min\{ (2973782 - 100) - \lfloor (2973782 - 100) / 64 \rfloor, 2891423 \} = 2891423
又根据公式
C_{\text{\tiny CALLGAS}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv \begin{cases}
C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) + G_{\mathrm{callstipend}} & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[2] \neq 0 \\
C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) & \text{otherwise}
\end{cases}
因此 C_{\text{\tiny CALLGAS}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = 2891423
再根据公式
\begin{aligned}
(\boldsymbol{\sigma}', g', A^+, \mathbf{o}) \equiv \begin{cases}{\Theta}(\boldsymbol{\sigma}, I_{\mathrm{a}}, I_{\mathrm{o}}, t, t, C_{\text{\tiny CALLGAS}}(\boldsymbol{\mu}), I_{\mathrm{p}}, \boldsymbol{\mu}_{\mathbf{s}}[2], \boldsymbol{\mu}_{\mathbf{s}}[2], \mathbf{i}, I_{\mathrm{e}} + 1, I_{\mathrm{w}}) & \text{if} \ \boldsymbol{\mu}_{\mathbf{s}}[2] \leqslant \boldsymbol{\sigma}[I_{\mathrm{a}}]_{\mathrm{b}} \;\wedge I_{\mathrm{e}} < 1024 \\ (\boldsymbol{\sigma}, g, \varnothing, ()) & \text{otherwise} \end{cases}
\end{aligned}
其中,{\Theta}
第6个参数为表示目标合约的 gas
因此,[141] 的 Gas 为 2891423
--
推导3 C_{\text{\tiny CALL}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = 2891523
最后根据公式
C_{\text{\tiny CALL}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) \equiv C_{\text{\tiny GASCAP}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) + C_{\text{\tiny EXTRA}}(\boldsymbol{\sigma}, \boldsymbol{\mu})
因此,[140] 的 GasCost 为 C_{\text{\tiny CALL}}(\boldsymbol{\sigma}, \boldsymbol{\mu}) = 2891423 + 100 = 2891523
小结
理解上面的例子,应该就可以理解问题一和问题二了
至于问题三,为什么修改原题,加大 CALL
首个参数后题解无效?可以看看 C_{\text{\tiny GASCAP}}
中的 min
最后,此前的题解利用 EIP-2929: Gas cost increases for state access opcodes 的方式比较非主流,正经解答请参考官方题解~
最后的最后,可以思考下,假设当时题目首个参数确实为比较大的值,那么能否仍然利用 EIP-2929 解题呢?// 一时挖坑一时爽