引入代理模式

虽然无法更新已部署的智能合约代码,但是可以通过设置一个代理合约架构,进而部署新的合约,以实现合约升级的目的。
代理模式使得所有消息调用都通过代理合约,代理合约会将调用请求重定向到最新部署的合约中。如要升级时,将升级后新合约地址更新到代理合约中即可。

Zeppelin在实现zeppelin_os的过程中一直在研究几种代理模式。探索了三种代理模式:

  1. 继承存储
  2. 永久存储
  3. 非结构化存储

这三种模式底层都依赖delegatecalls来实现。虽然Solidity提供了delegatecall 方法,但它仅在调用成功后返回true / false,无法管理返回的数据。
在深入研究之前,需要先理解两个重要的概念:

  • 当调用的方法在合约中不存在时,合约会调用fallback函数。可以编写fallback函数的逻辑处理这种情况。代理合约使用自定义的fallback函数将调用请求重定向到其他合同中。
  • 每当合约A将调用代理到另一个合同B时,它都会在合约A的上下文中执行合约B的代码。这意味着将保留msg.value和msg.sender值,并且每次存储修改都会影响合约A。

所有代理模式都继承了Zeppelin’s Proxy contract,该合约实现了自己的代理调用函数,该函数返回调用逻辑合约的值。如果您打算使用Zeppelin的代理合约代码,需要详细了解代理合约代码。让我们探索它是如何工作的,并了解它用于实现代理的汇编操作码。(参考Solidity的Assembly文档以获取更多信息)

  1. [assembly {
  2. //0x40 代表空闲内存空间
  3. let ptr := mload(0x40)
  4. calldatacopy(ptr, 0, calldatasize) // 从当前的0 位置复制calldatasize字节大小的数据到ptr
  5. /*
  6. - gas 我们传递执行合约所需要燃料
  7. - _impl 所请求的目标合约地址
  8. - ptr 请求数据在内存中的起始位置
  9. - calldatasize 请求数据的大小。
  10. - 0用于表示目标合约的返回值。这是未使用的,
  11. 因为此时我们尚不知道返回数据的大小,因此无法将其分配给变量。之后我们可以使用returndata操作码访问此信息
  12. - 0表示目标合约返回值的大小。这是未使用的,因为在调用目标合约之前,我们是无法知道返回值的大小。
  13. 之后我们可以通过returndatasize 操作码来获得该值
  14. */
  15. let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
  16. let size := returndatasize
  17. // 这个时候的结果会存在暂存的地方
  18. returndatacopy(ptr, 0, size)
  19. //最后,switch语句返回的数据或者抛出异常
  20. switch result
  21. case 0 { revert(ptr, size) }
  22. default { return(ptr, size) }
  23. }