聚合国内IT技术精华文章,分享IT技术精华,帮助IT从业人士成长

深入探索 CALL 指令参数0

2021-10-25 13:30 浏览: 683113 次 我要评论(0 条) 字号:

此前和几位朋友交流过智能合约外部调用的问题,有点久了;最近开始有些时间,简单整理记录下

外部调用有好几种指令,下面以最常见的 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 前,堆栈和内存如下

Remix Debug

结合 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}) = 0C_{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 解题呢?// 一时挖坑一时爽



网友评论已有0条评论, 我也要评论

发表评论

*

* (保密)

Ctrl+Enter 快捷回复