Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

为什么要做合约升级

就以太坊理想图景而言,合约升级是违背最初的“代码即法律”的构思的.

但是在生产环境中,我们不可不免的需要对已有的代码进行细微的改动(甚至有时候会有较大的改改动),
就连以太坊自身也需要不断的迭代和升级,所以合约升级的问题实际上是《合约升级管理办法》 的问题

合约升级的需求
一般有两种需求

  • 合约bug修复

    对于一个稍微复杂的dapp系统而言,在合约代码中,几乎不可避免的会存在一些bug,多数bag在测试阶段会被测试人员找到,
    但是测试也不能保证能找到所有的bug,现实中的扫雷不像是“扫雷”游戏那样,所有的雷都是按照逻辑规则来分布,也没有明确的雷区边界。

越是复杂的合约,越是难以保证不会存在bug(以太坊上的合约漏洞历历在目),如果能通过合约升级来避免漏洞被黑客利用,反倒能更好的保证用户的财产安全.

  • 新增需求

    随着dapp的发展,用户规模逐渐加大,最开始设计的产品可能已经无法满足客户的需求,这个时候如果能合理的理由合约升级,可以避免重新再写一套系统的损失。

合约升级的好处

  • 解决合约中潜藏的bug,对bug做必要的修复
  • 允许在原有的dapp系统中新增需求
  • 重新重视dapp合约的生命周期管理(在没有合约升级之前,dapp合约的生命周期是固定的,但是现在可以更灵活的控制dapp的生命周期)

合约升级的弊端

  • 破坏了“代码及法律”
  • 合约升级引来了新的合约漏洞(以太坊上已经有项目被黑)
  • 降低dapp的去中心性
    (现在大部分的可升级合约都是依赖一个合约管理员来管理合约的升级)

合约升级的关键

代理调用(delegatecall)

delegatecall是solidity提供的一种低级调用方式,它允许solidity合约在自己的上下问和存储空间中调用其他合约的代码。

简单而言,代理调用就是将其他合约(一般称为逻辑合约)的二进制代码copy过来,然后带当前的环境下执行,在逻辑合约中对EVM内存/调用堆/存储空间的操作,底下的操作对象是原合约(一般称为)的EVM

由此,我们很容易联想到一种显而易见的合约升级方式,即通过代理的方式,每次升级都是修改逻辑合约的代码(重新部署一个逻辑合约上去),然后在代理合约中修改逻辑合约的地址,这样以后的每次调用对会指向新的逻辑合约的地址,实现合约中逻辑的升级。

也有一个显而易见的问题:我们没办法升级合约的数据结构,因为在代理调用的过程中使用的是代理合约的内存和存储空间,代理合约本身是不可升级

以及另外一个问题,代理合约和逻辑合约如何保证数据结构的一致?这个问题在后面合约升级方案中会提到。

fallback函数

想象这样一种情况,逻辑合约中有30个函数需要被代理,那么代理合约岂不是要写30是个代理函数,用来转发请求?
而且如果逻辑合约的升级中需要新增一个函数,代理合约没法升级,新增的函数岂不是代理不到。

关于这些问题,solidity提供了一种特殊的函数,fallback方法,fallback的作用是,在处理一个调用时,如果没有找到对应的方法,合约会重看是否存在fallback方法,如果存在,则交由fallback方法来处理。

fallback() external payable {  // fallback方法不需要在前面加function
}

通过fallback方法,我们可以不用关心逻辑合约中实现了什么方法,最终所有的请求都交由fallback转发,这样也就解决了上面提到的两个问题.

但是他引入了一个新的问题:代理合约中至少存在一个升级函数(用来升级逻辑合约地址的),而sol的函数选择器(选择由那个函数来处理请求,fallback是在函数选择器没有找到时的默认调用)是由8byte数据,与升级函数冲突的概率是很大的。
而且这个冲突是在编译阶段发现不了的,因为逻辑合约和代理合约不是一起编译的。

在后面我会介绍一些业界成熟对函数选择器冲突的解决方案.

鉴于篇幅,这一篇暂时到这儿.后续…

评论