跳至内容
0%

Yul 合约调用是如何工作的?

发布于 2023年8月1日4 分钟阅读

Yul 是一种中间编程语言,可以用来在智能合约中编写一种形式的汇编语言。现在你已经了解了 Yul 中的语法、存储和内存的工作方式,是时候来学习如何调用合约了!

合约调用在 Yul 中是如何工作的?

在本系列的最后一部分,我们将学习合约调用在 Yul 中是如何工作的。在深入示例之前,我们需要先学习一些新的 Yul 操作。来看看吧。

Instruction
Explanation

gas()

剩余可用于执行的 gas 数量

gasPrice()

交易的 gas 价格

address()

当前合约的地址

balance(a)

地址 a 的以太币余额(以 wei 为单位)

selfbalance()

与调用 balance(address()) 相同,但略微省 gas

caller()

调用该合约的地址(msg.sender)

origin()

发起该交易的地址(tx.origin)

callvalue()

该合约调用中发送的以太币数量(以 wei 为单位)(msg.value)

calldataload(p)

从位置 p 开始的 calldata(仅 32 字节数据)

calldatacopy(t, f, s)

将 calldata 复制到内存位置 t,从 calldata 中位置 f 开始,复制 s 字节的数据。

extcodesize(a)

地址 a 处代码的大小

returndatasize()

上一次 returndata 的大小

returndatacopy(t, f, s)

从 return data 位置 f 开始,将 s 字节数据复制到内存位置 t

timestamp()

当前区块的时间戳(以秒为单位)

number()

当前区块号

call(g, a, v, in, insize, out, outsize)

以 g 数量的 gas 调用地址 a 处的合约,将 v wei 作为 msg.value 传入,将内存位置 in - insize 处的数据作为 tx.data 传入,并将返回数据存储在内存位置 out - outsize 处。调用成功返回 1,否则返回 0。

delegatecall(g, a, in, insize, out, outsize)

与 call() 类似。主要区别在于 delegatecall() 会更新调用方合约中的状态变量。常用于代理合约。需要注意的是,存储布局必须与被调用的合约完全一致,以避免覆盖不该覆盖的存储变量。注意参数 v 不存在。使用 delegatecall() 无法发送以太币。

staticcall(g, a, in, insize, out, outsize)

与 call() 类似。区别在于它不能用于调用会改变区块链状态的合约(即标记为 pure 和 view 的函数)。注意参数 v 不存在。使用 staticcall() 无法发送以太币。

好了,现在让我们看看这些示例中要用到的一些新合约。首先,看看我们将要调用的合约。

这个合约有两个存储变量 var1 和 var2,分别存储在存储槽 1 和 2 中。

函数 a() 要求用户向合约发送至少 1 个以太币,否则会 revert。

接下来,函数 a() 会更新 var1 和 var2 并返回它们。

函数 b() 只是简单地读取 var1 和 var2 并返回它们。

在讲解调用 CallMe 合约的合约之前,我们需要花点时间理解函数选择器(function selector)。

来看看下面这笔交易的 calldata 0x773d45e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002

calldata 的前 4 个字节就是所谓的函数选择器(0x773d45e0)。这是 EVM 用来判断你想调用哪个函数的方式。我们通过对函数签名字符串取哈希,取其前 4 个字节来得到函数选择器。

那么函数 a() 的签名就是 a(uint256,uint256)。

对这个字符串取哈希得到:0x773d45e097aa76a22159880d254a5f1db8365bc2d0f0987a82bda7dfd3b9c8aa

查看前 4 个字节,我们发现它等于 0x773d45e0。

注意签名中没有空格。这一点很重要,因为加上空格会得到完全不同的哈希值。你不需要担心我们代码示例中的选择器如何获取,我会提供它们。

我们先来看一下存储布局。

注意 var1 和 var2 的布局与 CallMe 合约相同。你可能还记得我说过,为了让 delegatecall() 正常工作,布局必须与另一个合约保持一致。

我们满足了这一要求,同时还能拥有其他变量(selectorA 和 selectorB),只要新变量被追加在末尾即可。这样可以避免存储冲突。

现在我们可以进行第一次合约调用了。

如何在 Yul 中使用 staticcall()

先从简单的开始,staticcall()。这是我们的函数:

这里发生的事情是:

  • 通过加载存储槽 2 从存储中取出 b() 的函数选择器(两个选择器都被打包进同一个槽)
  • 右移 4 字节(32 位)以分离出 selectorB
  • 将函数选择器存入内存的暂存空间(scratch space)
  • 进行 static call
  • 传入 gas()(你也可以指定 gas 数量)
  • 传入参数 _callMe 作为合约地址
  • 检查函数调用是否成功,否则我们不带数据返回
  • 从内存中返回数据,可以看到值 1 和 2

注意: 0x1c 和 0x20 表示我们想要传递之前存入暂存空间的数据的最后 4 个字节。staticcall() 的最后两个参数指定我们想将返回数据存储在内存位置 0x80 - 0xc0。

如何在 Yul 中使用 call()

接下来看看 call()。我们将调用 CallMe 中的函数 a()。记得要向合约发送至少 1 个以太币!在这个示例中,我会传入 3 和 4 作为 _var1 和 _var2。

代码如下:

好的,与上一个示例类似,我们需要加载存储槽 2。不过这次,我们要通过掩码操作屏蔽掉 selectorB 以分离出 selectorA。

现在我们将选择器存储在 0x80。

由于我们需要来自 calldata 的参数,我们将使用 calldatacopy()。我们告诉 calldatacopy():

  • 将 calldata 存储在内存位置 0xa0
  • 跳过前 36 个字节
  • 存储 calldata 大小减去 36 字节的数据

注意: 我们跳过的前 4 个字节是 callA() 的函数选择器,接下来的 32 个字节是 callMe 的地址。

现在我们可以进行合约调用了!

和上次一样,我们传入 gas() 和 _callMe。不过这次我们传入从 0x9c(0x80 内存序列的最后 4 个字节)到 0xe0 的 call data,并将数据存储在内存位置 0x100 - 0x120。

同样地,我们检查调用是否成功,然后返回输出结果。如果我们查看 CallMe 合约,会发现值已成功更新为 3 和 4。

为了进一步说明发生了什么,这是返回之前的内存布局:

Memory Location
Value Stored

0x00

暂存空间(空)

0x20

暂存空间(空)

0x40

0x80(我们从未更新过空闲内存指针,因为所有操作在 Yul 中都是手动完成的。)

0x60

0x80

0x0000000000000000000000000000000000000000000000000000000000773d45e0(函数选择器)

0xa0

0x0000000000000000000000000000000000000000000000000000000000000003(_var1)

0xc0

0x0000000000000000000000000000000000000000000000000000000000000004(_var2)

0xe0

0x100

0x0000000000000000000000000000000000000000000000000000000000000003(第一个返回值)

0x120

0x0000000000000000000000000000000000000000000000000000000000000004(第二个返回值)

0x140

空闲内存指针(空)

如何在 Yul 中使用 delegate call

在最后一部分,我们将学习 delegatecall()。这段代码看起来与 call() 几乎相同,只有一处改动。

我们唯一做的改动是将 call() 换成了 delegatecall(),并移除了 callvalue()。

我们不需要 callvalue(),因为 delegate call 会在自己的状态中执行来自 CallMe 的代码。因此 a() 中的 require() 语句检查的是是否有以太币发送到我们的 Caller 合约。如果我们查看 CallMe 中的 var1 和 var2,会发现没有任何变化。然而,Caller 合约中的 var1 和 var2 已成功更新。

这就是关于合约调用部分的全部内容!接下来,了解 MemoryStorageSmart Contract Calls 在 Yul 中是如何工作的。

Background gradient

构建区块链应用

Alchemy 将最强大的 Web3 开发者产品和工具与资源、社区及专业支持结合在一起。