接上篇:合约升级模式介绍笔者改写了一个可用于实践生产的升级框架,须要自取。https://github.com/hammewang/...git
同时欢迎讨论,微信xiuxiu1998github
鉴于以太坊智能合约一旦部署,没法修改的原则,因此智能合约升级应当遵循以下两点规则:安全
第一点很好理解,能够把代理合约和逻辑合约当作插座和插头的关系,须要升级的时候把老的插头拔下,再插上新的便可。bash
对于第二点,存储可继承,不只仅是存储结构的继承,并且在存储内容上,实现扩展:旧存储内容不变,新存储内容继续追加。这个过程相似于城市化的推动,城市的边缘能够一圈一圈扩大,可是若是要寻址到老城区的XX路XX号,不管城市怎么扩大,拿着这个门牌号依然能够找到那栋老建筑。微信
升级目的中的第一点是相对好实现的,只要改变调用的逻辑合约地址就能够了;而为了实现第二点,就要保证合约执行环境上下文保持一致。在介绍合约升级模式中提到了一个能够解决这个问题的方法:delegatecall
。把关键代码再贴一遍:框架
assembly { // 得到自由内存指针 let ptr := mload(0x40) // 复制calldata到内存中 calldatacopy(ptr, 0, calldatasize) // 使用delegatecall处理calldata let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0) // 返回值大小 let size := returndatasize // 把返回值复制到内存中 returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } // 执行失败 default { return(ptr, size) } // 执行成功,返回内存中的返回值 }
这样作,实现了把逻辑合约(_impl
)中的方法拉到代理合约中执行,遵循代理合约的上下文(如存储、余额等),经过这种方式实现了执行上下文一致性。布局
注意:delegatecall为assembly中的低阶方法;ui
下文中出现的delegateCall方法,是我在智能合约中写的一个方法名称,不要混淆。this
delegatecall的目的是能够维持执行环境中上下文的一致性,一种很典型的应用场景就是调用library中的方法,用的就是delegatecall。下面来具体介绍一下delegatecall的特色。spa
假设personA调用了contractA中的functionA,这个方法内部同时使用了delegatecall调用了contractB中的functionB,那么对于functionB来讲,msg.sender依然是personA,而不是contractA.
请看下面的合约:
pragma solidity ^0.4.24; contract proxy { address public logicAddress; function setLogic(address _a) public { logicAddress = _a; } function delegateCall(bytes data) public { this.call.value(msg.value)(data); } function () payable public { address _impl = logicAddress; require(_impl != address(0)); assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize) let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0) let size := returndatasize returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } function getPositionAt(uint n) public view returns (address) { assembly { let d := sload(n) mstore(0x80, d) return(0x80,32) } } } contract logic { address public a; function setStorage(address _a) public { a = _a; } }
这时分别部署proxy
和logic
,以后把logic.address
赋值给proxy
中的logicAddress
变量。调用getPositionAt(0)
会发现返回的也是logicAddress
的值,结果以下图:
这时,若是调用proxy
中的delegateCall
并传入0x9137c1a7000000000000000000000000bcb9c87f53878af6dd7a8baf1b24bab6a62fe7aa
(9137c1a7
是setStorage
的方法签名),意为用delegatecall
调用logic
中的setStorage方法
,这时会发现proxy
中的logicAddress
发生了变化,变成了咱们刚刚传入的值。以下:
这时咱们会发现,delegatecall
并不经过变量名称来修改变量值,而是修改变量所在的存储槽。因此当在proxy
中delegatecallsetStorage
方法时,修改的并非address a
,而是address a
所在的第0个存储槽的值,而proxy
中第0个存储槽存放的是logicAddress
,因此相应就会被覆盖。
理解到这一步,就能够感觉到delegatecall的强大和危险。但同时也带来一层疑问:虽然使用delegatecall可使用逻辑合约中的方法改变代理合约中相应位置的变量,可是并无起到存储可扩展呀?不还得事先在代理合约中建立相应变量么?这就至关于在1949年新中国创建的时候,就要规划之后建设的全部布局,包括共享单车停靠点,这不是有点扯淡么?
这就要说到delegatecall下面一个特色了。
delegatecall还有一个强大的特色就是,能够为proxy中未事先声明的变量开辟存储空间。
咱们来看下一个例子,代理合约依然使用上面用过的proxy
,咱们把逻辑合约 变一下:
contract logic2 { address public a; address public b; function setStorageB(address _a) public { b = _a; } }
新增长一个address变量,而且只修改第二个address变量。
这时依然重复上一个例子的第一步,把logic2
的地址赋值给代理合约中的logicAddress
变量。结果以下图:
而后使用代理合约中的detegateCall
方法,调用logic2
中的setStorage2
方法,传入data
为0x9ea338be0000000000000000000000000dcd2f752394c41875e259e00bb44fd505297caf
。以后再调用getPositionAt(1)
和logicAddress()
方法,结果以下图:
能够看到logicAddress
并无发生变化,而第1个存储槽中的值变成了咱们刚刚传入的值。
这也再次说明了,delegatecall
方法并非按照变量名称操做的,而是按照变量所对应的存储槽的位置,对该位置中的值进行操做。所以,咱们是否是事先在代理合约中声明了变量,就并不重要了。
正由于第二点特性,为合约升级中的存储扩展提供了可能性;同时,也提出了一个很严格的要求:
新合约和旧合约之间必须严格遵照继承的模式,即:
contract newLogic is previousVersionLogic{ ... }
------- ========================= | Proxy | ║ UpgradeabilityStorage ║ ------- ========================= ↑ ↑ ↑ --------------------- ------------- | UpgradeabilityProxy | | Upgradeable | --------------------- ------------- ↑ ↑ ---------- ---------- | Token_V0 | ← | Token_V1 | ---------- ----------
代理合约是UpgradeabilityProxy
实例,图中的Token_V0
和Token_V1
便是逻辑合约的最第一版和升级版,它们都必须继承Upgradeable
,同时逻辑合约和代理合约都必须继承UpgradeabilityStorage
,继承同一套存储结构,以保证逻辑合约在代理合约中执行时,不会出现变量覆盖的状况。
注:图中每一个方框的结构从上到下依次是:合约名称、状态变量、function、event、modifier
图中能够更加清晰地看到,代理合约和逻辑合约都必须继承registry
和_implementation
两个状态变量,而且逻辑合约中没有修改前两个状态变量的相应方法,所以代理合约中的存储安全。
Registry
合约Upgradeable
合约Registry
合约中注册这个最第一版本(V1)的地址Registry
合约建立一个UpgradeabilityProxy
实例UpgrageabilityProxy
实例来升级到你最第一版本(V1)Registry
中注册合约的新版本V2UpgradeabilityProxy
实例来升级到最新注册的版本调用Registry
中的transferProxyOwnership
方法进行全部权转移;
须对代理合约的地址套用当前版本的逻辑合约的ABI,方能正常调用和获取返回值。