Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

EVM_Opcodes(3)

EVM Opcodes(3)

原文章:https://www.wtf.academy/evm-opcodes-102

在接下来的部分将引入一些较为复杂的操作码。

Return 指令

这一部分会介绍与返回数据(return)相关的 3 个指令:RETURNRETURNDATASIZERETURNDATACOPY。他们是 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中。

image-20231127154708677

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中。

image-20231127155345682

INVALID

  • 操作码:0xFE
  • gas 消耗:所有剩余的 gas

INVALID是 EVM 中用来表示无效操作的指令。当 EVM 遇到无法识别的操作码时,或者在故意触发异常的情境下,会执行INVALID指令,导致所有状态更改都不会生效,并且消耗掉所有的 gas。他确保了当合约试图执行未定义的操作时,不会无所作为或产生不可预期的行为,而是会安全地停止执行,对 EVM 的安全至关重要。

调用系列指令

CALL

  • 操作码:0xF1
  • gas 消耗:比较复杂,包含内存拓展和代码执行等成本

CALL指令会创建一个子环境来执行其他合约的部分代码,发送ETH,并返回数据。返回数据可以使用RETURNDATASIZERETURNDATACOPY获取。若执行成功,会将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有许多相似之处,但关键的区别在于调用的上下文不同,它在代理合约和可升级合约中被广泛应用。它的设计目的是允许一个合约借用其他合约的代码,但代码是在原始合约的上下文中执行。这使得一份代码可以被多个合约重复使用,而无需重新部署。使用DELEGATECALLmsg.sendermsg.value保持不变,修改的存储变量也是原始合约的。

它从堆栈中弹出6个参数,CALL不同,它不包括value,因为ETH不会被发送

  • gas:为这次调用分配的gas量。
  • to:被调用合约的地址。
  • mem_in_start:输入数据(calldata)在内存的起始位置。
  • mem_in_size:输入数据的长度。
  • mem_out_start:返回数据(returnData)在内存的起始位置。
  • mem_out_size:返回数据的长度。

有两个关键点需要注意:

  1. DELEGATECALL不会更改msg.sendermsg.value
  2. DELEGATECALL改变的存储(storage)是原始合约的存储。
  3. CALL不同,DELEGATECALL不会传递ETH值,因此少一个value参数。

CALLCODE

  • 操作码:0xF2

CALLCODEDELEGATECALL非常相似,但当修改状态变量时,它会更改调用者的合约状态而不是被调用者的合约状态。由于这个原因,CALLCODE在某些情况下可能会引起意料之外的行为,目前被视为已弃用。建议大家使用DELEGATECALL,而不是CALLCODE

我们根据EIP-2488,将CALLCODE视为已弃用:每次调用在堆栈中压入0(视为调用失败)。

STATICCALL

  • 操作码:0xFA
  • gas 消耗:内存拓展成本 + 地址操作成本

STATICCALL指令会创建一个子环境来执行其他合约的部分代码,并返回数据。返回数据可以使用RETURNDATASIZERETURNDATACOPY获取。若执行成功,会将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)。

下面,我么来看一个简单的initcode63ffffffff6000526004601cf3。它的指令形式为:

1
2
3
4
5
6
PUSH4 ffffffff
PUSH1 00
MSTORE
PUSH1 04
PUSH1 1c
RETURN

它先用MSTORE指令把ffffffff拷贝到内存中,然后用RETURN指令将它们拷贝到返回数据中。这段initcode会把新合约的字节码设置为ffffffff

CREATE

  • 操作码:F0
  • gas 消耗:内存扩展成本 + 部署代码执行成本 + 代码存款成本

在EVM中,当一个合约想要创建一个新的合约时,会使用CREATE指令。它的简化流程:

  1. 从堆栈中弹出value(向新合约发送的ETH)、mem_offsetlength(新合约的initcode在内存中的初始位置和长度)。

  2. 计算新合约的地址,计算方法为:

    1
    address = keccak256(rlp([sender_address,sender_nonce]))[12:]
  3. 更新ETH余额。

  4. 初始化新的EVM上下文evm_create,用于执行initcode

  5. evm_create中执行initcode

  6. 如果执行成功,则更新创建的账户状态:更新balance,将nonce初始化为0,将code字段设为evm_create的返回数据,将storage字段设置为evm_createstorage

  7. 如果成功,则将新合约地址推入堆栈;若失败,将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指令的简化流程如下:

  1. 从堆栈中弹出value(向新合约发送的ETH)、mem_offsetlength(新合约的initcode在内存中的初始位置和长度)以及salt
  2. 使用上面的公式计算新合约的地址。
  3. 之后的步骤同CREATE指令:初始化新的EVM上下文、执行initcode、更新创建的账户状态、返回新合约地址或0(如果失败)。

Selfdestruct 指令

它可以让合约自毁。这个指令可能在未来会被弃用,见EIP-4758EIP-6049

基本概念

EVM中的SELFDESTRUCT指令可以让合约自行销毁,并将账户中的ETH余额发送到指定地址。这个指令一些特殊的地方:

  1. 使用SELFDESTRUCT指令时,当前合约会被标记为待销毁。但实际的销毁操作会在整个交易完成后进行
  2. 合约的ETH余额会被发送到指定的地址,并且这一过程保证会成功的。
  3. 如果指定的地址是一个合约,那么该合约的代码不会被执行,即不会像平常的ETH转账执行目标地址的fallback方法。
  4. 如果指定的地址不存在,则会为其创建一个新的账户,并存储这些ETH。
  5. 一旦合约被销毁,其代码和数据都会永久地从链上删除,无法恢复。销毁合约可能会影响到与它互动的其他合约或服务

SELFDESTRUCT 指令

  • 操作码:0xFF
  • gas 消耗:静态气体为5000。如果将正余额发送到空帐户,则动态气体为25000。如果一个账户的余额为0,其nonce为0且没有代码,则该账户为空。此外,如果地址是冷钱包,则还有额外的动态费用2600。请参阅访问集部分。否则没有其他附加费用。

SELFDESTRUCT指令的工作流程如下:

  1. 从堆栈中弹出接收ETH的指定地址。
  2. 将当前合约的余额转移到指定地址。
  3. 销毁合约。

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消耗由很多因素决定,并且每个以太坊版本都会有所改动,下面总结下:

  1. calldata大小:calldata中的每个字节都需要花费gas,交易数据的大小越大,gas消耗就越高。calldata每个零字节花费4 Gas,每个非零字节花费16 Gas(伊斯坦布尔硬分叉之前为 64 个)。
  2. 内在gas:每笔交易的内在成本为21000 Gas。除了交易成本之外,创建合约还需要 32000 Gas。该成本是在任何操作码执行之前从交易中支付的。
  3. opcode固定成本:每个操作码在执行时都有固定的成本,以Gas为单位。对于所有执行,该成本都是相同的。比如每个ADD指令消耗3 Gas。
  4. opcode动态成本:一些指令消耗更多的计算资源取决于其参数。因此,除了固定成本之外,这些指令还具有动态成本。比如SHA3指令消耗的Gas随参数长度增长。
  5. 内存拓展成本:在EVM中,合约可以使用操作码访问内存。当首次访问特定偏移量的内存(读取或写入)时,内存可能会触发扩展,产生gas消耗。比如MLOADRETURN
  6. 访问集成本:对于每个外部交易,EVM会定义一个访问集,记录交易过程中访问过的合约地址和存储槽(slot)。访问成本根据数据是否已经被访问过(热)或是首次被访问(冷)而有所不同。
  7. Gas退款:SSTORE的一些操作(比如清除存储)可以触发Gas退款。退款会在交易结束时执行,上限为总Gas消耗的20%(从伦敦硬分叉开始)。

更详细的Gas消耗信息可以参考evm.codes

GAS 指令

  • 操作码:0x5A
  • gas 消耗:2

通过GAS指令,智能合约可以实时地查询还剩下多少Gas,从而做出相应的决策。