通过逆向和调试理解EVM #3 :存储布局如何工作?

通过调试理解EVM 3 :存储布局如何工作?

  • 原文链接: https://trustchain.medium.com/reversing-and-debugging-ethereum-evm-smart-contracts-part-3-ebe032a08f97
  • 译文出自:登链翻译计划
  • 译者:翻译小组 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

本文是关于通过调试理解EVM第 3 篇,本系列包含 7 篇文章:

  • 第1篇:理解汇编
  • 第2篇:部署智能合约
  • 第3篇:存储布局是如何工作的?
  • 第4篇:结束/中止执行的5个指令
  • 第5篇:执行流 if/else/for/函数
  • 第6篇:完整的智能合约布局
  • 第7篇:外部调用和合约部署

本篇我们将看看不同类型的变量是如何在EVM内存和存储中存储和处理的。

每次,当我们在分析一段代码时,我建议你同时用remix来调试它。你会对正在发生的事情有一个更好的理解。如果你不知道怎么做,请查看本系列的第1篇:理解汇编

1. 简单的例子

我们将首先使用一个非常简单的例子。

不要忘记编译下面的合约,我们的设置是:solidity 0.8.7版本编译器、启用优化器,run 为 200 。

部署它可以并调用函数 "modify()":

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  
contract Test {     
    uint balance;     
    uint balance2;     
    uint balance3;   

    function modify() external {         
        balance = 1;         
        balance2 = 2;         
        balance3 = 3;     
    } 
}

当我们用remix调试 "modify"函数时,会被remix直接 "路由"到函数modify(),因此在modify()之前执行的代码(如函数选择器或payable验证)已经完成,对我们的分析没有帮助:

045 JUMPDEST |0x64cf33b8| (this is the function signature, we will discard it)
046 PUSH1 42 |0x42|
048 PUSH1 01 |0x01|0x42|
050 PUSH1 00 |0x00|0x01|0x42|
052 DUP2     |0x01|0x00|0x01|0x42|
053 SWAP1    |0x00|0x01|0x01|0x42|
054 SSTORE   |0x01|0x42|
055 PUSH1 02 |0x02|0x01|0x42|
057 SWAP1    |0x01|0x02|0x42|
058 DUP2     |0x02|0x01|0x02|0x42|
059 SWAP1    |0x01|0x02|0x02|0x42|
060 SSTORE   |0x02|0x42|
061 PUSH1 03 |0x03|0x02|0x42|
063 SWAP1    |0x02|0x03|0x42|
064 SSTORE   |0x42|
065 JUMP     ||
066 JUMPDEST ||
067 STOP     ||

在我们调用函数modify后,结果是相当明显的。

在第48指令,EVM在堆栈中PUSH 42(十进制的66),这就是合约末尾代码中的 "位置"。(066 jumpdest 067 stop) 当modify()的执行将结束时,EVM将JUMP到这个字节。

备注:第48指令 表示第48个字节数上的指令(从 0 开始),下面都使用这种简写方式。

在指令48和54之间,EVM在存储槽0保存1 在指令55和60之间,EVM在存储槽1保存2。 在指令61和64之间,EVM 在存储槽2保存3。

在65指令,函数JUMP到66(0x42),在函数modify()的开头保存的字节,并通过使用STOP指令结束智能合约的执行。

你可以通过运行调试器和检查堆栈中的汇编来验证。这段代码相当于:

sstore(0x0,0x1) 
sstore(0x1,0x2) 
sstore(0x2,0x3)

这里,即使我们的值比32字节少很多,它们也被存储在单独的槽里,这可能会花费一些Gas。(如果以前的值是0,则每个槽要花费20000个Gas)

> 修改函数的Gas成本

但是,如果你第二次调用该函数,由于存储中的值是非零的,Gas成本会便宜很多。(每SSTORE 使用2200gas)

提示:每条在EVM上指令都要花费Gas,一个交易的Gas成本是所有指令的Gas总和(+21000Gas的基本成本),你可以在调试器标签中的 "步骤详情(step details)"部分看到Gas的使用。

这里,SWAP1指令使用了3个Gas

如果你不理解这第一部分,请随时阅读本系列的第一篇或第二篇文章,在那里更详细地解释汇编代码:https://learnblockchain.cn/article/4913

2. 使用uint8 而不是uint256

到现在为止,我们还没学到什么,但是如果我们把uint代替为uint8呢?有什么区别吗?让我们看看结果吧!

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  contract Test {     
    uint8 balance;     
    uint8 balance2;
    uint8 balance3; 

    function modify() external {
         balance = 1; 
         balance2 = 2; 
         balance3 = 3;    
    } 
    function modify2() external {   
         balance2 = 5; 
    } 
}

你可能已经知道,uint8只使用1个字节。所以3个uint8应该只用3个字节,这比一个槽要少得多。(32字节)

因此,这三个变量加起来应该只使用一个槽,对吗?

是的,你是对的!只有一个存储被执行,而且代码要短得多。

045 JUMPDEST     |function signature (discarded)|
046 PUSH1 00     |0x00|
048 DUP1         |0x00|0x00|
049 SLOAD        |0x00|0x00| (the slot 0 in storage contains 0x030201)
050 PUSH3 ffffff |0xffffff|0x00|0x00|
054 NOT          |0xffffff...fffff000000|0x00|0x00| (the NOT inverse all 32 bytes of Stack(0)
055 AND          |0x00|0x00|
056 PUSH3 030201 |0x030201|0x00|0x00|
060 OR           |0x030201|0x00|
061 SWAP1        |0x00|0x030201|
062 SSTORE       ||
063 STOP         ||

让我们看看在这个函数中到底发生了什么。像往常一样,不要忘记在阅读的同时使用调试器,你会对情况有更好的理解。

在第49指令,SLOAD在Stack(0)槽中加载Storage值,由于Stack(0)=0 (备注:存储槽没有写入过,默认都是 0),所以堆栈没有变化。

接下来的3个操作(指令 50-55)有点神秘。

EVM推送 "ffffff" 和NOT(取反)这个,结果是 0xfffffffffffffffffffffffffffffffffffffffffffffffff000000, NOT指令反转了Stack(0)的所有字节。

在这之后,它与之前的SLOAD进行 AND 运算(与运算),也就是0x00。

我们知道,0 AND x = 0(对每一个x),结果是0x00,堆栈保持与指令50之前一样。

在这6条指令之后,没有任何变化,这非常奇怪......我们将在后面几行看到原因。

就在这之后的第56字节:0x030201被推到了堆栈中,这显然是我们的balance=1balance2=2balance3=3的值。

在第60指令,因为Stack(1)是0,OR(或)操作码在这里没有任何作用,因为 0 OR x = x (对于所有的x),Stack保持不变,只有Stack(1)的0x00被删除。

之后,SSTORE用来将030201存储在0槽中。这就是我们所要做的。

你可以注意到,03 02 01在存储空间和堆栈中都占用了1个槽,就像我们所期望的那样。 因此,我们可以证明,这3个变量占用了相同的存储槽,因此使用的Gas更少了

只有43286个Gas被使用,而之前是87504。

第二个智能合约函数modify(),只使用了43286个Gas而不是87504个。如果你是一个智能合约的开发者,你已经证明了,使用更少的变量(当它是可能的)可以节省大量的Gas......

现在让我们调用函数modify2(在modify()之后),这里是整个函数的反汇编。

提示一下:modify2只把balance2(插槽1)设置为5。

075 PUSH1 00   |0x00|
077 DUP1       |0x00|0x00|
078 SLOAD      |0x030201|0x00| (Slot 0 = balance which contains 0x030201 as set previously)
079 PUSH2 ff00 |0xff00|0x030201|0x00|
082 NOT        |0xfff...fffff00ff|0x030201|0x00|
083 AND        |0x000...000030001|0x01|0x00|
084 PUSH2 0500 |0x0500|0x000...000030001|0x01|0x00|
087 OR         |0x000...000030501|0x01|0x00|
088 SWAP1      |0x01|0x000...000030501|0x00|
089 SSTORE     |0x00|
090 STOP       ||
  1. 首先(指令78),EVM 加载存储的槽 0,即0x030201
  2. 其次(指令79-82),EVM对 ff00 取反(NOT),在32个字节的结果是 0xfffffffffffffffffffffffffffffffffffffffffffffffffff00ff
  3. 在指令83,2个结果进行与运算,即0x00000000000000000000000000000000000000000000000000030001(或0x030001)。 这和存储槽0是一样的,但是没有02(合约中的 "balance2"的部分),这是正常的!为什么? 这是因为在modify2()中,EVM修改了balance2。首先它需要擦除之前的结果,而不擦除balancebalance3(因为它们在同一个槽中),所以它通过使用0xfff...ff00ff掩码来 "清洗" 结果。
  4. 之后,0500被推入堆栈(指令84),最后的2个结果进行OR操作(指令85),最后的结果是:0x030501,"OR"的目的是在03和01的边上加上05。因此余额2被成功地修改,而没有改变余额和余额3。

这个0xfffffffffffffffffffffffffffffffffffffffffffffffffff00ff被称为 "掩码"。

如果我们想代替balance2修改balance3为5,我们应该使用掩码0xfffffffffffffffffffffffffffffffffffffffffffffffff00ffff(擦除balance3在0x00槽中的字节),在第四步,我们应该PUSH 050000.(05应该在这里,因为这里放置了balance3的存储。)

这就是为什么你在需要的时候应该使用较小的类型:它需要更少的Gas。

但是,不要滥用较小的类型,因为它增加了EVM执行的操作的数量(通过使用带有掩码的操作),所以它使用更多的Gas。

3. 使用不同类型的数据

让我们看看节省Gas的技巧是否只适用于uint类型或其他solidity内置类型。

下面是新的智能合约,用相同的设置(编译器0.8.7和优化器为200)编译它:

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  
contract Test {     
    uint8 balance;     
    bytes4 data;     
    address addr;      
    function modify() external {         
        balance = 17;        
        data = 0xaaaaaaaa;       
        addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;     
    } 
}

这个智能合约包含一个uint8,占1个字节,一个byte4,占4个字节,一个地址,占20个字节。我们创建的 modify函数总共 "修改"了25个字节,这比一个EVM插槽(32个字节)还要少。

理论上,这3个变量应该用一个存储槽就可以。

是这样吗?

045 JUMPDEST |function signature discarded|
046 PUSH1 00 |0x00|
048 DUP1     |0x00|0x00|
049 SLOAD    |0x00|0x00| (slot 0 in storage contains 0)
050 PUSH1 01 |0x01|0x00|0x00|
052 PUSH1 01 |0x01|0x01|0x00|0x00|
054 PUSH1 c8 |0xc8|0x01|0x01|0x00|0x00|
056 SHL      |0x00.50zeros.0100...00|0x01|0x00|0x00| move 0x01 to 0xc8 = 200times (50 hex numbers) to the left
057 SUB      |0x00.15zeros0tttffff.49f.fffff|0x00|0x00| 
058 NOT      |0xfffffffffffffff00..49zeros..00|0x00|0x00| Our mask is created !
059 AND      |0x00|0x00|
060 PUSH25 358aa13c52544eccef6b0add0f801012adad5ee3aaaaaaaa11 
             |0x00...00358aa13...|0x00|0x00|
086 OR       |0x00...00358aa13...|0x00|
087 SWAP1    |0x00|0x00...00358aa13...|
088 SSTORE   ||
089 STOP     ||

你可能猜到了,答案是肯定的!

该函数的结构与上一个例子(使用uint8)几乎相同。只是在开始创建掩码时有一些不同。

因为有25个字节,所以掩码应该是0xfffffffffff00000...49zeros00000在指令58/59的AND之前。

之后,在第60指令,包含智能合约中存在的值的25个字节被推送(addr = 0x358aa13c52544eccef6b0add0f801012adad5ee3data = 0xaaaaaaaabalance = 0x11),并与指令86的掩码进行 "OR"。

最后,它们被 "存储"在0槽中。

使用了43298个Gas(只执行了1个SSTORE)。

除此之外,Gas成本与存储uint8的成本差不多低。(43286 vs 43298)

4. 变量的位置是否有关系?

要了解EVM,最好的方法是通过修改不同的参数,进行尽可能多的测试。这正是我们在这里所做的。

在这个例子中,我们将互换addrdata变量。

它们应该在存储空间中占据相同的位置,但位置应该是颠倒的,对吗?

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  
contract Test {     
    uint8 balance;
    address addr;
    bytes4 data;      
    function modify() external {      
        balance = 17;    
        data = 0xaaaaaaaa;     
        addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;   
   } 
}

让我们来编译和反汇编,在指令60处,我们有: (当然,代码的其他指令是一样的)

PUSH aaaaaaaa358aa13c52544eccef6b0add0f801012adad5ee311

这与上次的情况:“358aa13c52544eccef6b0add0f801012adad5ee3aaaaaaaa11” 不同。

我们可以看到,addrdata变量被互换了。

我们的假设是对的,两个变量的位置被颠倒了。balance = 17 (十六进制11)仍然是在第一个位置,因为EVM使用的是小端(litter-endian)架构(第一个在最后)。

5. 如何存储结构体?

如果我们用结构体做同样的事情,会发生什么?

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  

contract Test {     
    struct Values {  
        uint8 balance;   
        address addr;  
        bytes4 data;    
    }      
    Values value;      
    function modify() external { 
        value.balance = 17;  
        value.addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;    
        value.data = 0xaaaaaaaa;   
     }
 }

下面是函数的额完整汇编代码:

045 JUMPDEST 
046 PUSH1 00 |0x00|
048 DUP1     |0x00|0x00|
049 SLOAD    |0x00|0x00|
050 PUSH1 01 |0x01|0x00|0x00|
052 PUSH1 01 |0x01|0x01|0x00|0x00|
054 PUSH1 c8 |0xc8|0x01|0x01|0x00|0x00|
056 SHL      |0x00.50zeros.0100...00|0x01|0x00|0x00|
057 SUB      |0x00.15zeros0tttffff.49f.fffff|0x00|0x00|
058 NOT      |0xfffffffffffffff00..49zeros..00|0x00|0x00|
059 AND      |0x00|0x00|
060 PUSH25 aaaaaaaa358aa13c52544eccef6b0add0f801012adad5ee311 |0x00...00358aa13...|0x00|0x00|
086 OR       |0x00...00358aa13...|0x00|
087 SWAP1    |0x00|0x00...00358aa13...|
088 SSTORE   ||
089 STOP     ||

使用结构体的代码是完全一样。

警告:这意味着有时要区分3个不同的变量还是一个结构体可能很困难。

Gas费也和以前一样:

好了,这对我们来说太容易了,直到现在,操作码之间没有什么区别。但我们还没有结束! 我们需要更多的挑战。

6. 那么数组呢?

数组是如何在EVM中存储的?像结构体或变量一样?

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  
contract Test { 
    uint[] values;
    // uint value2 in comment 
    function modify() external {   
        values.push(7);   
        values.push(8);  
    } 
}

不是的 !

这一次,不幸的是,它更长、更复杂。我们将需要更多关于EVM中存储的理论。

"values" 变量是一个动态数组,最多可以存储无限多的值。但是有一个问题,如果我们在数组之后创建一个value2变量(就像注释中的那样),怎么办?它应该被存储在哪个存储槽中?

value2需要在 "value"之后,并且在槽1中,但是由于 "value"数组是动态的,可以改变大小,所以很难指定一个插槽。嗯...

在现实中,这仍然是正确的,value2将被存储在槽1中(因此接下来的变量将被存储在槽2,3中,以此类推...)。

但是槽0呢?槽0包含什么?

事实上,槽0存储的是数组的长度,在此案例中,在智能合约执行函数modify()后,它将存储2,因为有2个值被推入数组。

但是,这些值被存储在哪里

由于数组的长度被存储在槽0中,这些值应该被存储在其他地方。

它是在槽Keccak256(0)+n中。

第一个值存储在槽Keccak256(0)+0中 下一个:Keccak256(0)+1 第三个:Keccak256(0)+2 第n个:Keccak256(0)+(n-1)

由于Keccak256(0)是一个非常大的数字,EVM不可能用完插槽。因此,我们解决了这个问题。

为什么在keccak256(0)中使用0?因为声明的数组是在0槽里。如果它是:

uint value2 //value 2 is in slot 0
uint[] values; //the array is in slot 1

因为数组在槽1中,这个数组中的第i个值被存储在Keccak256(1)+(n-1),数组的长度被存储在槽1中。

现在我们应该看看它在汇编中是如何工作的。 首先,这是函数modify()在汇编中的第一部分:

048 PUSH1 00 |0x00|
050 DUP1     |0x00|0x00|
051 SLOAD    |0x00|0x00|
052 PUSH1 01 |0x01|0x00|0x00|
054 DUP2     |0x00|0x01|0x00|0x00|
055 DUP2     |0x01|0x00|0x01|0x00|0x00|
056 ADD      |0x01|0x01|0x00|0x00|
057 DUP4     |0x00|0x01|0x01|0x00|0x00|
058 SSTORE   |0x01|0x00|0x00|
059 DUP3     |0x00|0x01|0x00|0x00|
060 DUP1     |0x00|0x00|0x01|0x00|0x00|
061 MSTORE   |0x01|0x00|0x00|

在指令51,EVM从存储槽0加载,结果是0,因为数组的长度是0。

在第56指令,EVM在0号存储槽加载的值上加1,并在第58指令将其SSTORE到同一存储槽。因此,现在数组的长度是1。

在第61指令,EVM在地址0处MSTORE 0,我们将在后面看到原因。

062 PUSH1 07 |0x07|0x01|0x00|0x00|
064 PUSH32 290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 |hash|0x07|0x01|0x00|0x00|
097 SWAP3    |0x00|0x07|0x01|hash|0x00|
098 DUP4     |hash|0x00|0x07|0x01|hash|0x00|
099 ADD      |hash|0x07|0x01|hash|0x00|
100 SSTORE   |0x01|hash|0x00|

0x07被推到堆栈中,0的哈希值也是如此,它是keccak256(0x00)(它等于290decd...e563)

由于数组的当前索引是0,EVM将0添加到哈希值上(在指令99),即堆栈中的第四个值。(这一点很重要,因为当EVM将另一个值添加到数组中时,索引将与0不同,因此结果也将不同)。

然后存储(SSTORE) 0x07到结果槽,(槽keccak256(0x00)+0 = 7)

这就成功存储了数组的第一个元素。

在第2次SSTORE之后,重复同样的代码,存储值为8,但有细微差别:

101 DUP3     |0x00|0x01|hash|0x00|
102 SLOAD    |0x01|0x01|hash|0x00|
103 SWAP1    |0x01|0x01|hash|0x00|
104 DUP2     |0x01|0x01|0x01|hash|0x00|
105 ADD      |0x02|0x01|hash|0x00|
106 SWAP1    |0x01|0x02|hash|0x00|
107 SWAP3    |0x00|0x02|hash|0x01|
108 SSTORE   |hash|0x01|
109 PUSH1 08 |0x08|hash|0x01|
111 SWAP2    |0x01|hash|0x02|
112 ADD      |hash+1|0x02|
113 SSTORE   ||

在第102指令,SLOAD在槽0处返回1,这是数组的长度。

在第105指令,ADD在结果上加1,所以1+1=2。

在第108指令,EVM SSTORE结果(是2)在槽0,这就是数组的新长度。

在第112指令,EVM将1添加到Hash中,这是分配给values[1]的槽。

在第113指令,EVM存储值8,在keccak256(0)+1而不是上次的keccak256(0)

如果我们分析一下这种操作的Gas成本,我们会发现,这比非数组的情况下要高。

87746 Gas

有1个SSTORE来存储一个数组的长度。(+ 20000 gas) 有2个SSTORE来存储7和8的值。(+40000 gas) 有1个SSTORE到一个非零值槽,将数组的长度从1更新到2。

我们就快完成了......

7. 如何存储映射?

现在我们来谈谈映射,像数组一样,如何存储映射的所有值并不明显。(ERC20代币中的变量balances就是一个好例子)

我们知道,映射是一组键值对

对比数组,在映射中存储值的方式非常相似,存储槽等于SHA3(mapping_slot.key)

(其中.是连接运算符)在这最后一部分,我们将验证这个公式。

让我们来编译这个最后的合约,编译选项 没有优化(但仍然是solidity 0.8.7),并调用modify()函数!

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;  
contract Test {     
    mapping(address => uint) balances;      
    function modify() external {                  balances[0xbc5D291D2165f130375B94c62211f594dB48fEF2] = 15;     balances[0x9a8af21Ac492D5055eA7e1e49bD91BC9b5549334] = 55; 
    } 
}

这是函数的第一部分的完整反汇编:

054 PUSH1 0f |0x0f|
056 PUSH1 00 |0x00|0x0f|
058 DUP1     |0x00|0x00|0x0f|
059 PUSH20 bc5d291d2165f130375b94c62211f594db48fef2
             |address1|0x00|0x00|0x0f|
080 PUSH20 ffffffffffffffffffffffffffffffffffffffff
             |0xff..ff|address1|0x00|0x00|0x0f|
101 AND      |address1|0x00|0x00|0x0f|
102 PUSH20 ffffffffffffffffffffffffffffffffffffffff 
             |0xff..ff|address1|0x00|0x00|0x0f|
123 AND      |address1|0x00|0x00|0x0f|
124 DUP2     |0x00|address1|0x00|0x00|0x0f|
125 MSTORE   |0x00|0x00|0x0f|
126 PUSH1 20 |0x20|0x00|0x00|0x0f|
128 ADD      |0x20|0x00|0x0f|
129 SWAP1    |0x00|0x20|0x0f|
130 DUP2     |0x20|0x00|0x20|0x0f|
131 MSTORE   |0x20|0x0f|
132 PUSH1 20 |0x20|0x20|0x0f|
134 ADD      |0x40|0x0f|
135 PUSH1 00 |0x00|0x40|0x0f|
137 SHA3     |keccak256(address1)|0x0f|
138 DUP2     |0x0f|keccak256(address1)|0x0f|
139 SWAP1    |keccak256(address1)|0x0f|0x0f|
140 SSTORE   |0x0f|

在第54-59指令 0f(十进制的15),0,0 和 bc5d...f2 被推送到堆栈。

  • 0f是modify()函数中第一个地址的余额。
  • bc5d...f2 就像是 balance=15" 的地址。

在第80和102指令之间,这段代码没有任何作用,它只是通过使用掩码:0x00000000000000000000000ffffffffffffffffffffffffffffffffffffffff 保证堆栈中的地址0xbc5d...之后只有0000

例如,地址可能是0xd0000....bc5d...fd,EVM和0x000...000ffff的掩码进行与运算,从而去除开头的d

在第125指令,它用MSTORE把结果保存到内存(保存在地址为0x00的 "清洁 "内存中)。

在第128指令,它将20加到0x00,并在第131指令将0存储到内存。(因此0被存储在内存的0x20处)

在第137指令,调用SHA3指令,使用堆栈中的参数:0(offset)和40(size)。

这条指令返回keccak256(memory[offset:offset+length])。 这里 offset = Stack(0) = 0, length = Stack(1) = 40 (因为堆栈里有最后两个值)

这是地址0xbc5d...f2与0相连接的SHA3(或KECCAK256)。

之后,通过使用SSTORE,15被存储在SHA3操作的结果槽中。

如果我们把它与开头的公式进行比较: SHA3(mapping_slot.key)

  • 余额变量的mapping_slot当然等于0(存储在0x20到0x40的内存中,而不是在开头位置,因为EVM使用小端结构)
  • key等于地址0xbc5d291d2165f130375b94c62211f594db48fef2(存储在0x00到0x20的内存中)。

对于那些想知道memory[0x00:0x40]的目的是什么的人来说,的答案是:目标是(仅仅)存储使用keccak256进行Hash的操作数。

由于EVM对0x00和0x40之间的所有东西进行散列,我们的说法是正确的。

下面是这样一个操作的Gas成本:

使用 65594 Gas

成本比存储在数组中要低(至少在开始时),因为mapping_slot(槽0)不包含任何东西,也没有被修改,所以少了一个SSTORE操作。

8. 结论

如果你读完了这篇文章,恭喜你! 这并不容易,但它是值得的。你现在对EVM的了解比99.9%的开发者还要多。


本翻译由 Duet Protocol 赞助支持。

本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-10-28 18:16
  • 阅读 ( 228 )
  • 学分 ( 12 )
  • 分类:智能合约

评论