基于 Solidity IR 的编码更改
Solidity可以通过两种不同的方式生成EVM字节码:直接从Solidity到EVM操作码(“旧codegen”),或者通过Yul(“new codegen”或“IR-based codegen”)中的中间表示(“IR”)。
引入基于 IR 的代码生成器,不仅使代码生成更加透明和可审计,而且还实现了跨功能的更强大的优化传递。
您可以使用标准 json 中的选项在命令行上启用它,我们鼓励每个人尝试一下!--via-ir
{"viaIR": true}
出于几个原因,旧的和基于IR的代码生成器之间存在微小的语义差异,主要是在我们不希望人们依赖这种行为的领域。本节重点介绍旧的和基于 IR 的编码机之间的主要区别。
仅语义更改
本节列出了仅语义的更改,因此可能会在现有代码中隐藏新的和不同的行为。
-
在继承的情况下,状态变量初始化的顺序已更改。
顺序曾经是:
-
所有状态变量在开始时都为零初始化。
-
评估从最派生到大多数基本协定的基本构造函数参数。
-
初始化整个继承层次结构中的所有状态变量,从最基本到最派生。
-
对线性化层次结构中从最基本到最派生的所有协定运行构造函数(如果存在)。
新订单:
-
所有状态变量在开始时都为零初始化。
-
评估从最派生到大多数基本协定的基本构造函数参数。
-
对于线性化层次结构中从最基本到最派生的每个合约:
-
初始化状态变量。
-
运行构造函数(如果存在)。
-
这会导致合约的差异,其中状态变量的初始值依赖于另一个合约中构造函数的结果:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1; contract A { uint x; constructor() { x = 42; } function f() public view returns(uint256) { return x; } } contract B is A { uint public y = f(); }
以前,将设置为 0。这是因为我们将首先初始化状态变量:首先,设置为0,并且在初始化时,将返回0,导致也为0。使用新规则,将设置为42。我们首先初始化为 0,然后调用 A 的构造函数,该构造函数设置为 42。最后,在初始化时,返回 42,导致为 42。
y
x
y
f()
y
y
x
x
y
f()
y
-
-
删除存储结构时,包含结构成员的每个存储槽都将完全设置为零。以前,填充空间保持不变。因此,如果结构中的填充空间用于存储数据(例如,在合约升级的上下文中),您必须知道现在也会清除添加的成员(虽然过去不会清除它)。
delete
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1; contract C { struct S { uint64 y; uint64 z; } S s; function f() public { // ... delete s; // s occupies only first 16 bytes of the 32 bytes slot // delete will write zero to the full slot } }
对于隐式删除,我们具有相同的行为,例如,当结构数组被缩短时。
-
函数修饰符的实现方式与函数参数和返回变量略有不同。如果在修饰符中多次计算占位符,这尤其有效。在旧的代码生成器中,每个函数参数和返回变量在堆栈上都有一个固定的槽。如果函数由于多次使用或在循环中使用而多次运行,则在下次执行函数时,对函数参数或返回变量值的更改可见。新的代码生成器使用实际函数实现修饰符,并传递函数参数。这意味着对函数体的多次计算将获得相同的参数值,并且对返回变量的影响是,对于每次执行,它们都会重置为其默认(零)值。
_;
_;
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0; contract C { function f(uint a) public pure mod() returns (uint r) { r = a++; } modifier mod() { _; _; } }
如果在旧代码生成器中执行,它将返回 ,而在使用新代码生成器时将返回。
f(0)
2
1
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1 <0.9.0; contract C { bool active = true; modifier mod() { _; active = false; _; } function foo() external mod() returns (uint ret) { if (active) ret = 1; // Same as ``return 1`` } }
该函数返回以下值:
C.foo()
-
旧代码生成器:因为返回变量在第一次求值之前仅初始化为一次,然后被 .它不会在第二次评估中再次初始化,也不会显式分配它(由于 ),因此它保留其第一个值。
1
0
_;
return 1;
_;
foo()
active == false
-
新的代码生成器:因为所有参数(包括返回参数)将在每次评估之前重新初始化。
0
_;
-
-
将数组从内存复制到存储以不同的方式实现。旧的代码生成器总是复制完整的单词,而新的代码生成器在其结束后剪切字节数组。旧行为可能导致在阵列结束后(但仍在同一存储插槽中)复制脏数据。这会导致某些合约的差异,例如:
bytes
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { bytes x; function f() public returns (uint r) { bytes memory m = "tmp"; assembly { mstore(m, 8) mstore(add(m, 32), "deadbeef15dead") } x = m; assembly { r := sload(x.slot) } } }
以前会返回(它具有正确的长度,并且正确的前8个元素,但随后它包含通过程序集设置的脏数据)。现在它正在返回(它具有正确的长度和正确的元素,但不包含多余的数据)。
f()
0x6465616462656566313564656164000000000000000000000000000000000010
0x6465616462656566000000000000000000000000000000000000000000000010
-
对于旧代码生成器,表达式的计算顺序是未指定的。对于新的代码生成器,我们尝试按源代码顺序(从左到右)进行评估,但不保证。这可能导致语义差异。
例如:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function preincr_u8(uint8 a) public pure returns (uint8) { return ++a + a; } }
该函数返回以下值:
preincr_u8(1)
-
旧代码生成器:3(),但返回值通常未指定
1 + 2
-
新代码生成器:4()但不保证返回值
2 + 2
另一方面,函数参数表达式由两个代码生成器以相同的顺序计算,但全局函数和 .例如:
addmod
mulmod
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function add(uint8 a, uint8 b) public pure returns (uint8) { return a + b; } function g(uint8 a, uint8 b) public pure returns (uint8) { return add(++a + ++b, a + b); } }
该函数返回以下值:
g(1, 2)
-
旧代码生成器:()但返回值通常未指定
10
add(2 + 3, 2 + 3)
-
新代码生成器:但不能保证返回值
10
全局函数的参数,由旧代码生成器从右到左计算,由新代码生成器从左到右计算。例如:
addmod
mulmod
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function f() public pure returns (uint256 aMod, uint256 mMod) { uint256 x = 3; // Old code gen: add/mulmod(5, 4, 3) // New code gen: add/mulmod(4, 5, 5) aMod = addmod(++x, ++x, x); mMod = mulmod(++x, ++x, x); } }
该函数返回以下值:
f()
-
旧代码生成器:和
aMod = 0
mMod = 2
-
新的代码生成器:和
aMod = 4
mMod = 0
-
-
新的代码生成器对可用内存指针施加了 () 的硬限制。如果分配的值超过此限制,则会恢复。旧的代码生成器没有此限制。
type(uint64).max
0xffffffffffffffff
例如:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; contract C { function f() public { uint[] memory arr; // allocation size: 576460752303423481 // assumes freeMemPtr points to 0x80 initially uint solYulMaxAllocationBeforeMemPtrOverflow = (type(uint64).max - 0x80 - 31) / 32; // freeMemPtr overflows UINT64_MAX arr = new uint[](solYulMaxAllocationBeforeMemPtrOverflow); } }
函数 f() 的行为如下:
-
旧代码生成器:在大内存分配后将数组内容清零时耗尽气体
-
新的代码生成器:由于可用内存指针溢出而恢复(不会耗尽气体)
-
内部
内部函数指针
旧的代码生成器使用代码偏移量或标记作为内部函数指针的值。这尤其复杂,因为这些偏移量在构造时和部署后是不同的,并且值可以通过存储跨越此边界。因此,两个偏移量在构造时被编码为相同的值(不同的字节)。
在新的代码生成器中,函数指针使用按顺序分配的内部 ID。由于无法通过跳转进行调用,因此通过函数指针的调用始终必须使用内部调度函数,该函数使用语句来选择正确的函数。switch
该 ID 是为未初始化的函数指针保留的,这些指针在调用时会导致调度函数中的死机。0
在旧的代码生成器中,内部函数指针使用一个特殊函数进行初始化,该函数总是会导致死机。这会导致在构造时对存储中的内部函数指针进行存储写入。
清理
旧代码生成器仅在操作之前执行清理,其结果可能受脏位值的影响。新的代码生成器在任何可能导致脏位的操作后执行清理。希望优化器足够强大,可以消除冗余的清理操作。
例如:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function f(uint8 a) public pure returns (uint r1, uint r2) { a = ~a; assembly { r1 := a } r2 = a; } }
该函数返回以下值:f(1)
-
旧代码生成器:(,
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
00000000000000000000000000000000000000000000000000000000000000fe
) -
新的代码生成器:(,
00000000000000000000000000000000000000000000000000000000000000fe
00000000000000000000000000000000000000000000000000000000000000fe
)
请注意,与新代码生成器不同,旧代码生成器在位不赋值 () 之后不执行清理。这会导致分配不同的值(在内联程序集块内)以在旧代码生成器和新代码生成器之间返回值。但是,在将 的新值 分配给 之前,这两个代码生成器都会执行清理。a = ~a
r1
a
r2