引入代理模式
虽然无法更新已部署的智能合约代码,但是可以通过设置一个代理合约架构,进而部署新的合约,以实现合约升级的目的。
代理模式使得所有消息调用都通过代理合约,代理合约会将调用请求重定向到最新部署的合约中。如要升级时,将升级后新合约地址更新到代理合约中即可。
Zeppelin在实现zeppelin_os的过程中一直在研究几种代理模式。探索了三种代理模式:
- 继承存储
- 永久存储
- 非结构化存储
这三种模式底层都依赖delegatecalls来实现。虽然Solidity提供了delegatecall 方法,但它仅在调用成功后返回true / false,无法管理返回的数据。
在深入研究之前,需要先理解两个重要的概念:
- 当调用的方法在合约中不存在时,合约会调用fallback函数。可以编写fallback函数的逻辑处理这种情况。代理合约使用自定义的fallback函数将调用请求重定向到其他合同中。
- 每当合约A将调用代理到另一个合同B时,它都会在合约A的上下文中执行合约B的代码。这意味着将保留msg.value和msg.sender值,并且每次存储修改都会影响合约A。
所有代理模式都继承了Zeppelin’s Proxy contract,该合约实现了自己的代理调用函数,该函数返回调用逻辑合约的值。如果您打算使用Zeppelin的代理合约代码,需要详细了解代理合约代码。让我们探索它是如何工作的,并了解它用于实现代理的汇编操作码。(参考Solidity的Assembly文档以获取更多信息)
[assembly {
//0x40 代表空闲内存空间
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize) // 从当前的0 位置复制calldatasize字节大小的数据到ptr
/*
- gas 我们传递执行合约所需要燃料
- _impl 所请求的目标合约地址
- ptr 请求数据在内存中的起始位置
- calldatasize 请求数据的大小。
- 0用于表示目标合约的返回值。这是未使用的,
因为此时我们尚不知道返回数据的大小,因此无法将其分配给变量。之后我们可以使用returndata操作码访问此信息
- 0表示目标合约返回值的大小。这是未使用的,因为在调用目标合约之前,我们是无法知道返回值的大小。
之后我们可以通过returndatasize 操作码来获得该值
*/
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
let size := returndatasize
// 这个时候的结果会存在暂存的地方
returndatacopy(ptr, 0, size)
//最后,switch语句返回的数据或者抛出异常
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}