EVM Opcodes(3)
原文章:https://www.wtf.academy/evm-opcodes-102
在接下来的部分将引入一些较为复杂的操作码。
Return 指令
这一部分会介绍与返回数据(return)相关的 3 个指令:RETURN
、RETURNDATASIZE
和RETURNDATACOPY
。他们是 Solidity 中return
关键字的基础。
返回数据
EVM的返回数据,通常称为returnDara
,本质上是一个字节数组。他不遵循固定的数据结构,而是简单地表示为连续的字节。当合约函数需要返回复杂数据类型(如结构体或数组)时,这些数据将按照 ABI 规范被编码为字节,并存储在returnData
中,供其他函数或合约访问。
RETURN
- 操作码:
0xF3
- gas 消耗:内存拓展成本
- 功能:从指定的内存位置提取数据。存储在
returnData
中,并终止当前的操作。此指令需要从堆栈中取出两个参数:内存的起始位置mem_offset
和数据的长度length
。 - 使用场景:当需要将数据返回给外部函数或交易时
示例:
60a26000526001601ff3
(PUSH1 a2 PUSH1 00 MSTORE PUSH1 1 PUSH1 1f RETURN)。这个字节码将a2
存在内存中,然后使用RETURN
指令将a2
复制到returnData
中。
RETURNDATASIZE
- 操作码:
0x3D
- gas 消耗:
2
- 功能:将
returnData
的大小推入堆栈。 - 使用场景:使用上一个调用返回的数据
RETURNDATACOPY
- 操作码:
0x3E
- gas 消耗:
3 + 3 * 数据长度 + 内存拓展成本
- 功能:将
returnData
中的某段数据复制到内存中。此指令需要从堆栈中取出三个参数:内存的起始位置mem_offset
,返回数据的起始位置return_offset
,和数据的长度length
。 - 使用场景:使用上一个调用返回的部分数据
Revert 指令
REVERT
- 操作码:
0xFD
- gas 消耗:内存拓展消耗
当合约运行出错,或者达到了某种条件需要终止执行并返回错误信息时,可以使用REVERT
指令。**REVERT
指令会终止交易的执行,返回一个错误信息,并且所有状态更改都不会生效**。他从堆栈中弹出两个参数:内存中错误信息的起始位置mem_offset
和错误信息的长度length
。
示例:
60aa6000526001601ffd
(PUSH1 aa PUSH1 0 MSTORE PUSH1 1 PUSH1 1f REVERT),这个字节码将aa
存在内存中,然后使用REVERT
指令进行回滚,并将aa
复制到returnData
中。
INVALID
- 操作码:
0xFE
- gas 消耗:所有剩余的 gas
INVALID
是 EVM 中用来表示无效操作的指令。当 EVM 遇到无法识别的操作码时,或者在故意触发异常的情境下,会执行INVALID
指令,导致所有状态更改都不会生效,并且消耗掉所有的 gas。他确保了当合约试图执行未定义的操作时,不会无所作为或产生不可预期的行为,而是会安全地停止执行,对 EVM 的安全至关重要。
调用系列指令
CALL
- 操作码:
0xF1
- gas 消耗:比较复杂,包含内存拓展和代码执行等成本
CALL
指令会创建一个子环境来执行其他合约的部分代码,发送ETH
,并返回数据。返回数据可以使用RETURNDATASIZE
和RETURNDATACOPY
获取。若执行成功,会将1
压入堆栈;否则,则压入0
。如果目标合约没有代码,仍将1
压入堆栈(视为成功)。如果账户ETH
余额小于要发送的ETH
数量,调用失败,但当前交易不会回滚。
它从堆栈中弹出7个参数,依次为:
gas
:为这次调用分配的gas量。to
:被调用合约的地址。value
:要发送的以太币数量,单位为wei
。mem_in_start
:输入数据(calldata)在内存的起始位置。mem_in_size
:输入数据的长度。mem_out_start
:返回数据(returnData)在内存的起始位置。mem_out_size
:返回数据的长度。
关于 CALL 的例子,参考原文章。
DELEGATECALL
- 操作码:
0xF4
- gas 消耗:比较复杂,包含内存拓展和代码执行等成本
DELEGATECALL
指令与CALL
有许多相似之处,但关键的区别在于调用的上下文不同,它在代理合约和可升级合约中被广泛应用。它的设计目的是允许一个合约借用其他合约的代码,但代码是在原始合约的上下文中执行。这使得一份代码可以被多个合约重复使用,而无需重新部署。使用DELEGATECALL
时msg.sender
和msg.value
保持不变,修改的存储变量也是原始合约的。
它从堆栈中弹出6个参数,与CALL
不同,它不包括value
,因为ETH不会被发送:
gas
:为这次调用分配的gas量。to
:被调用合约的地址。mem_in_start
:输入数据(calldata)在内存的起始位置。mem_in_size
:输入数据的长度。mem_out_start
:返回数据(returnData)在内存的起始位置。mem_out_size
:返回数据的长度。
有两个关键点需要注意:
DELEGATECALL
不会更改msg.sender
和msg.value
。DELEGATECALL
改变的存储(storage)是原始合约的存储。- 与
CALL
不同,DELEGATECALL
不会传递ETH值,因此少一个value
参数。
CALLCODE
- 操作码:
0xF2
CALLCODE
与DELEGATECALL
非常相似,但当修改状态变量时,它会更改调用者的合约状态而不是被调用者的合约状态。由于这个原因,CALLCODE
在某些情况下可能会引起意料之外的行为,目前被视为已弃用。建议大家使用DELEGATECALL
,而不是CALLCODE
。
我们根据EIP-2488,将CALLCODE
视为已弃用:每次调用在堆栈中压入0
(视为调用失败)。
STATICCALL
- 操作码:
0xFA
- gas 消耗:内存拓展成本 + 地址操作成本
STATICCALL
指令会创建一个子环境来执行其他合约的部分代码,并返回数据。返回数据可以使用RETURNDATASIZE
和RETURNDATACOPY
获取。若执行成功,会将1
压入堆栈;否则,则压入0
。如果目标合约没有代码,仍将1
压入堆栈(视为成功)。
和CALL
指令的不同,**STATICCALL
不能发送ETH
,也不能改变合约的状态。它不允许子环境执行的代码中包含以下指令**:
CREATE
,CREATE2
,SELFDESTRUCT
LOG0
-LOG4
SSTORE
value
不为0的CALL
它从堆栈中弹出6个参数,依次为:
gas
:为这次调用分配的gas量。to
:被调用合约的地址。mem_in_start
:输入数据(calldata)在内存的起始位置。mem_in_size
:输入数据的长度。mem_out_start
:返回数据(returnData)在内存的起始位置。mem_out_size
:返回数据的长度。
CREATE 指令
initcode(初始代码)
以太坊有两种交易,一种是合约调用,一种是合约创建。在合约创建的交易中,to
字段设为空,而data
字段应填写为合约的初始代码(initcode
)。initcode
也是字节码,但他只在合约创建时执行一次,目的是为新合约设置必要的状态和返回最终的合约字节码(contract code
)。
下面,我么来看一个简单的initcode
:63ffffffff6000526004601cf3
。它的指令形式为:
1 | PUSH4 ffffffff |
它先用MSTORE
指令把ffffffff
拷贝到内存中,然后用RETURN
指令将它们拷贝到返回数据中。这段initcode
会把新合约的字节码设置为ffffffff
。
CREATE
- 操作码:
F0
- gas 消耗:内存扩展成本 + 部署代码执行成本 + 代码存款成本
在EVM中,当一个合约想要创建一个新的合约时,会使用CREATE
指令。它的简化流程:
从堆栈中弹出
value
(向新合约发送的ETH)、mem_offset
和length
(新合约的initcode
在内存中的初始位置和长度)。计算新合约的地址,计算方法为:
1
address = keccak256(rlp([sender_address,sender_nonce]))[12:]
更新ETH余额。
初始化新的EVM上下文
evm_create
,用于执行initcode
。在
evm_create
中执行initcode
。如果执行成功,则更新创建的账户状态:更新
balance
,将nonce
初始化为0
,将code
字段设为evm_create
的返回数据,将storage
字段设置为evm_create
的storage
。如果成功,则将新合约地址推入堆栈;若失败,将
0
推入堆栈。
CREATE2 指令
CREATE vs CREATE2
传统的CREATE
指令通过调用者的地址和nonce来确定新合约的地址,而CREATE2
则提供了一种新的计算方法,使我们可以在合约部署之前预知它的地址。
与CREATE
不同,**CREATE2
使用调用者地址、盐(一个自定义的256位的值)以及initcode
的哈希来确定新合约的地址**,计算方法如下:
1 | address = keccak256( 0xff + sender_address + salt + keccak256(init_code))[12:] |
这样的好处是,只要你知道initcode
,盐值和发送者的地址,就可以预先知道新合约的地址,而不需要现在部署它。而CREATE
计算的地址取决于部署账户的nonce
,也就是说,在nonce
不确定的情况下(合约还未部署,nonce可能会增加),没法确定新合约的地址。
CREATE2
- 操作码:
F5
- gas 消耗:十分复杂。。详见EVMCODE
在EVM中,CREATE2
指令的简化流程如下:
- 从堆栈中弹出
value
(向新合约发送的ETH)、mem_offset
、length
(新合约的initcode
在内存中的初始位置和长度)以及salt
。 - 使用上面的公式计算新合约的地址。
- 之后的步骤同
CREATE
指令:初始化新的EVM上下文、执行initcode
、更新创建的账户状态、返回新合约地址或0
(如果失败)。
Selfdestruct 指令
它可以让合约自毁。这个指令可能在未来会被弃用,见EIP-4758和EIP-6049。
基本概念
EVM中的SELFDESTRUCT
指令可以让合约自行销毁,并将账户中的ETH余额发送到指定地址。这个指令一些特殊的地方:
- 使用
SELFDESTRUCT
指令时,当前合约会被标记为待销毁。但实际的销毁操作会在整个交易完成后进行。 - 合约的
ETH
余额会被发送到指定的地址,并且这一过程保证会成功的。 - 如果指定的地址是一个合约,那么该合约的代码不会被执行,即不会像平常的
ETH
转账执行目标地址的fallback
方法。 - 如果指定的地址不存在,则会为其创建一个新的账户,并存储这些ETH。
- 一旦合约被销毁,其代码和数据都会永久地从链上删除,无法恢复。销毁合约可能会影响到与它互动的其他合约或服务。
SELFDESTRUCT 指令
- 操作码:
0xFF
- gas 消耗:静态气体为5000。如果将正余额发送到空帐户,则动态气体为25000。如果一个账户的余额为0,其nonce为0且没有代码,则该账户为空。此外,如果地址是冷钱包,则还有额外的动态费用2600。请参阅访问集部分。否则没有其他附加费用。
SELFDESTRUCT
指令的工作流程如下:
- 从堆栈中弹出接收
ETH
的指定地址。 - 将当前合约的余额转移到指定地址。
- 销毁合约。
Gas 指令
什么是 Gas
在EVM中,交易和执行智能合约需要消耗计算资源。为了防止用户恶意的滥用网络资源和补偿验证者所消耗的计算能源,以太坊引入了一种称为Gas的计费机制,使每一笔交易都有一个关联的成本。
在发起交易时,用户设定一个最大Gas数量(gasLimit
)和每单位Gas的价格(gasPrice
)。如果交易执行超出了gasLimit
,交易会回滚,但已消耗的Gas不会退还。
Gas 规则
以太坊上的Gas用gwei
衡量,它是ETH
的子单位,1 ETH = 10^9 gwei
。一笔交易的Gas成本等于每单位gas价格乘以交易的gas消耗,即gasPrice * gasUsed
。gas价格会随着时间的推移而变化,具体取决于当前对区块空间的需求。gas消耗由很多因素决定,并且每个以太坊版本都会有所改动,下面总结下:
calldata
大小:calldata
中的每个字节都需要花费gas,交易数据的大小越大,gas消耗就越高。calldata
每个零字节花费4
Gas,每个非零字节花费16
Gas(伊斯坦布尔硬分叉之前为 64 个)。- 内在gas:每笔交易的内在成本为21000 Gas。除了交易成本之外,创建合约还需要 32000 Gas。该成本是在任何操作码执行之前从交易中支付的。
opcode
固定成本:每个操作码在执行时都有固定的成本,以Gas为单位。对于所有执行,该成本都是相同的。比如每个ADD
指令消耗3
Gas。opcode
动态成本:一些指令消耗更多的计算资源取决于其参数。因此,除了固定成本之外,这些指令还具有动态成本。比如SHA3
指令消耗的Gas随参数长度增长。- 内存拓展成本:在EVM中,合约可以使用操作码访问内存。当首次访问特定偏移量的内存(读取或写入)时,内存可能会触发扩展,产生gas消耗。比如
MLOAD
或RETURN
。 - 访问集成本:对于每个外部交易,EVM会定义一个访问集,记录交易过程中访问过的合约地址和存储槽(slot)。访问成本根据数据是否已经被访问过(热)或是首次被访问(冷)而有所不同。
- Gas退款:
SSTORE
的一些操作(比如清除存储)可以触发Gas退款。退款会在交易结束时执行,上限为总Gas消耗的20%(从伦敦硬分叉开始)。
更详细的Gas消耗信息可以参考evm.codes。
GAS 指令
- 操作码:
0x5A
- gas 消耗:
2
通过GAS
指令,智能合约可以实时地查询还剩下多少Gas,从而做出相应的决策。