delegatecall
delegatecall 与 call 类似,是 solidity 中地址类型的低级成员函数。delegate 中是委托/代表的意思,那么 delegatecall 委托了什么?
当用户 A 通过合约 B 来 call 合约 C 的时候,执行的是合约 C 的函数,语境 (Context,可以理解为包含变量和状态的环境) 也是合约 C 的:msg.sender 是 B 的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约 C 的变量上。
call 的语境
而当用户 A 通过合约 B 来 delegatecall 合约 C 的时候,执行的是合约 C 的函数,但是语境仍是合约 B 的:msg.sender 是 A 的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约 B 的变量上。
delegatecall 的语境
大家可以这样理解:一个富商把它的资产(状态变量)都交给一个 VC 代理(目标合约的函数)来打理。执行的是 VC 的函数,但是改变的是富商的状态。
delegatecall 语法和 call 类似,也是:
其中二进制编码利用结构化编码函数 abi.encodeWithSignature 获得:
函数签名为"函数名(逗号分隔的参数类型)"。例如 abi.encodeWithSignature("f(uint256,address)", _x, _addr)。
和 call 不一样,delegatecall 在调用合约时可以指定交易发送的 gas,但不能指定发送的 ETH 数额
注意:delegatecall 有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。
什么情况下会用到 delegatecall?
目前 delegatecall 主要有两个应用场景:
代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过 delegatecall 执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。更多信息请查看:钻石标准简介。
delegatecall 例子
调用结构:你(A)通过合约 B 调用目标合约 C。
被调用的合约 C
我们先写一个简单的目标合约 C:有两个 public 变量:num 和 sender,分别是 uint256 和 address 类型;有一个函数,可以将 num 设定为传入的_num,并且将 sender 设为 msg.sender。
发起调用的合约 B
首先,合约 B 必须和目标合约 C 的变量存储布局必须相同,两个变量,并且顺序为 num 和 sender
接下来,我们分别用 call 和 delegatecall 来调用合约 C 的 setVars 函数,更好的理解它们的区别。
callSetVars 函数通过 call 来调用 setVars。它有两个参数_addr 和_num,分别对应合约 C 的地址和 setVars 的参数。
而 delegatecallSetVars 函数通过 delegatecall 来调用 setVars。与上面的 callSetVars 函数相同,有两个参数_addr 和_num,分别对应合约 C 的地址和 setVars 的参数。
运行结果
我们把合约 B 和 C 都部署好,然后调用合约 C 中的 callSetVars,传入参数为合约 B 地址和 10。运行后,合约 C 中的状态变量将被修改:num 被改为 10,sender 变为合约 B 的地址。
接下来,我们调用合约 C 中的 delegatecallSetVars,传入参数为合约 B 地址和 100。由于是 delegatecall,语境为合约 B。在运行后,合约 B 中的状态变量将被修改:num 被改为 100,sender 变为合约你的钱包地址。
总结
这一讲我们介绍了 solidity 中的另一个低级函数 delegatecall。与 call 类似,它可以用来调用其他合约;不同点在于运行的语境,B call C,语境为 C;而 B delegatecall C,语境为 B。目前 delegatecall 最大的应用是代理合约和 EIP-2535 Diamonds(钻石)。