Yul 合约调用是如何工作的?
Yul 是一种中间编程语言,可以用来在智能合约中编写一种形式的汇编语言。现在你已经了解了 Yul 中的语法、存储和内存的工作方式,是时候来学习如何调用合约了!
合约调用在 Yul 中是如何工作的?
在本系列的最后一部分,我们将学习合约调用在 Yul 中是如何工作的。在深入示例之前,我们需要先学习一些新的 Yul 操作。来看看吧。
好了,现在让我们看看这些示例中要用到的一些新合约。首先,看看我们将要调用的合约。
这个合约有两个存储变量 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。
为了进一步说明发生了什么,这是返回之前的内存布局:
如何在 Yul 中使用 delegate call
在最后一部分,我们将学习 delegatecall()。这段代码看起来与 call() 几乎相同,只有一处改动。
我们唯一做的改动是将 call() 换成了 delegatecall(),并移除了 callvalue()。
我们不需要 callvalue(),因为 delegate call 会在自己的状态中执行来自 CallMe 的代码。因此 a() 中的 require() 语句检查的是是否有以太币发送到我们的 Caller 合约。如果我们查看 CallMe 中的 var1 和 var2,会发现没有任何变化。然而,Caller 合约中的 var1 和 var2 已成功更新。
这就是关于合约调用部分的全部内容!接下来,了解 Memory、Storage 和 Smart Contract Calls 在 Yul 中是如何工作的。
相关概览

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


