深度剖析智能合约升级——inherited storage

接上篇:合约升级模式介绍

笔者改写了一个可用于实践生产的升级框架,需要自取。https://github.com/hammewang/Proxy

同时欢迎讨论,微信xiuxiu1998

智能合约升级的目的

鉴于以太坊智能合约一旦部署,无法修改的原则,所以智能合约升级应当遵循如下两点规则:

  1. 逻辑可升级;
  2. 存储可继承;

第一点很好理解,可以把代理合约和逻辑合约看成插座和插头的关系,需要升级的时候把老的插头拔下,再插上新的即可。

对于第二点,存储可继承,不仅仅是存储结构的继承,而且在存储内容上,实现扩展:旧存储内容不变,新存储内容继续追加。这个过程类似于城市化的推进,城市的边缘可以一圈一圈扩大,但是如果要寻址到老城区的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

注意:

delegatecall为assembly中的低阶方法;

下文中出现的delegateCall方法,是我在智能合约中写的一个方法名称,不要混淆。

delegatecall的目的是可以维持执行环境中上下文的一致性,一种很典型的应用场景就是调用library中的方法,用的就是delegatecall。下面来具体介绍一下delegatecall的特点。

1. 可以传递msg.sender

假设personA调用了contractA中的functionA,这个方法内部同时使用了delegatecall调用了contractB中的functionB,那么对于functionB来说,msg.sender依然是personA,而不是contractA.

2.可以改变同一存储槽中的内容

请看下面的合约:

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;
     }
}

这时分别部署proxylogic,之后把logic.address赋值给proxy中的logicAddress变量。调用getPositionAt(0)会发现返回的也是logicAddress的值,结果如下图:

delegatecall_changeStorageSlot

这时,如果调用proxy中的delegateCall并传入0x9137c1a7000000000000000000000000bcb9c87f53878af6dd7a8baf1b24bab6a62fe7aa9137c1a7setStorage的方法签名),意为用delegatecall调用logic中的setStorage方法,这时会发现proxy中的logicAddress发生了变化,变成了我们刚刚传入的值。如下:

delegatecall_changeStorageSlot

这时我们会发现,delegatecall并不通过变量名称来修改变量值,而是修改变量所在的存储槽。所以当在proxy中delegatecallsetStorage方法时,修改的并不是address a,而是address a所在的第0个存储槽的值,而proxy中第0个存储槽存放的是logicAddress,所以相应就会被覆盖。

理解到这一步,就可以感受到delegatecall的强大和危险。但同时也带来一层疑问:虽然使用delegatecall可以使用逻辑合约中的方法改变代理合约中相应位置的变量,但是并没有起到存储可扩展呀?不还得事先在代理合约中创建相应变量么?这就相当于在1949年新中国建立的时候,就要规划以后建设的所有布局,包括共享单车停靠点,这不是有点扯淡么?

这就要说到delegatecall下面一个特点了。

delegatecall——"无中生有"

delegatecall还有一个强大的特点就是,可以为proxy中未事先声明的变量开辟存储空间

我们来看下一个例子,代理合约依然使用上面用过的proxy,我们把逻辑合约 变一下:

contract logic2 {
    address public a;
    address public b;
     function setStorageB(address _a) public {
         b = _a;
     }
}

新增加一个address变量,并且只修改第二个address变量。

这时依然重复上一个例子的第一步,把logic2的地址赋值给代理合约中的logicAddress变量。结果如下图:

delegatecall_changeStorageSlot

然后使用代理合约中的detegateCall方法,调用logic2中的setStorage2方法,传入data0x9ea338be0000000000000000000000000dcd2f752394c41875e259e00bb44fd505297caf。之后再调用getPositionAt(1)logicAddress()方法,结果如下图:

delegatecall_changeStorageSlot

可以看到logicAddress并没有发生变化,而第1个存储槽中的值变成了我们刚刚传入的值。

这也再次说明了,delegatecall方法并不是按照变量名称操作的,而是按照变量所对应的存储槽的位置,对该位置中的值进行操作。因此,我们是不是事先在代理合约中声明了变量,就并不重要了。

delegatecall总结

  1. 可以传递msg.sender
  2. 不按照变量名进行操作,而是去找变量对应的存储槽进行操作(无论变量是否在代理合约中事先声明)

正因为第二点特性,为合约升级中的存储扩展提供了可能性;同时,也提出了一个很严格的要求:

新合约和旧合约之间必须严格遵守继承的模式,即:

contract newLogic is previousVersionLogic{
    ...
}

使用存储继承模式升级

原理介绍

               -------             =========================
              | Proxy |           ║  UpgradeabilityStorage  ║
               -------             =========================
                  ↑                 ↑                     ↑            
                 ---------------------              -------------
                | UpgradeabilityProxy |            | Upgradeable |
                 ---------------------              ------------- 
                                                      ↑        ↑
                                              ----------      ---------- 
                                             | Token_V0 |  ← | Token_V1 |         
                                              ----------      ---------- 

代理合约是UpgradeabilityProxy实例,图中的Token_V0Token_V1即是逻辑合约的最初版和升级版,它们都必须继承Upgradeable,同时逻辑合约和代理合约都必须继承UpgradeabilityStorage,继承同一套存储结构,以保证逻辑合约在代理合约中执行时,不会出现变量覆盖的情况。

具体代码结构

upgradable_using_inherited_storage

注:图中每个方框的结构从上到下依次是:合约名称、状态变量、function、event、modifier

图中可以更加清晰地看到,代理合约和逻辑合约都必须继承registry_implementation两个状态变量,并且逻辑合约中没有修改前两个状态变量的相应方法,因此代理合约中的存储安全。

升级操作

1. 如何初始化

  1. 部署Registry合约
  2. 部署逻辑合约的初始版本(V1),并确保它继承了Upgradeable合约
  3. Registry合约中注册这个最初版本(V1)的地址
  4. 要求Registry合约创建一个UpgradeabilityProxy实例
  5. 调用你的UpgrageabilityProxy实例来升级到你最初版本(V1)

2. 如何升级

  1. 部署一个继承了你最初版本合约的新版本(V2),V2必须继承V1
  2. Registry中注册合约的新版本V2
  3. 调用你的UpgradeabilityProxy实例来升级到最新注册的版本

3. 如何转移proxy合约所有权

调用Registry中的transferProxyOwnership方法进行所有权转移;

代码调用注意事项

须对代理合约的地址套用当前版本的逻辑合约的ABI,方能正常调用和获取返回值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • 以太坊最大的优势就是,每一笔用来转账、部署合约或者和合约交互的交易(事务)都被存在一个叫做区块链的公共账本上。一旦...
    王铁塔阅读 1,486评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,629评论 18 139
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • 有人说:好孩子都是夸出来的。你也这么认为吗?来看看下面一个故事: 对话剧和音乐剧有兴趣的读者大概都听说过托尼奖吧。...
    玲玲A阅读 237评论 0 2
  • 文/匿名用户 应该从哪说起呢,脑子里关于他们的信息一多,反而理不清了。 FS 对 360安全卫士的贡献自然毋庸置疑...
    耕心鲜阅读 1,734评论 0 3