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

上回我们聊到了合约升级的原理(所依赖的solidity特性),一个是代理调用,他运行我们加载其他合约的逻辑来修改当前合约的数据,以及fallback函数,他可以出来未知的函数调用,让我们能够把请求转发到逻辑合约里面去,而逻辑合约的升级就是直接更换代理合约中的逻辑合约地址。
这一回,将讲述常见的几种标准化的合约升级方案。

合约升级方案

通用合约升级

通用合约升级实际上没有提出什么有建设性的技术,只是单纯的使用上面的特性完成合约升级,并且将合约升级所面临的问题揭露出来

目前基本上没有项目继续使用这种合约升级方案。

他的流程就是简单的通过代理合约调用逻辑合约,每次函数调用,实际上执行的是逻辑合约的代码,修改的是代理合约的存储空间。
当需要升级时,需要新部署逻辑合约,然后将代理合约中的(逻辑合约地址)修改为新的逻辑合约,在升级后的调用中代理合约会加载新的逻辑合约代码(代理合约只是根据合约地址加载合约的代码,他所以并不会引起不一致)

这样的合约升级会带来两个问题(主要是这两个)

  • 存储冲突
    虽然数据是存储在代理合约中的,但是由于逻辑合约中定义了对数据的操作,所以逻辑合约中也需要有对应的数据。比如有这样一种情况

代理合约中的数据是

contract Proxy {
    uint a;
    uint b;
    uint c;
}

逻辑合约中的定义是

contract Logic {
    uint b; // 逻辑合约只操作了数据B,所以没定义a,c
    function add() {
       b = b+1;
    }
}

这时,如果代理调用在逻辑合约中对b 进行了+1操作。
实际上发生的是,用户call Proxy合约,Proxy合约加载 Logic合约中的add代码
add代码实际上的操作是拿出第一个位置的数据,+1 ,写回去.

注意,这里是拿出第一个位置的数据,因为solidity编译后是没有变量名的,而是使用变量的位置来记录变量的。

所以当这段代码在Proxy中执行的时候,实际上是修改a位置的值。

这个问题怎么解决呢?

最简单的是,要求逻辑合约的变量顺序和代理合约一致,无论是否使用数据.但这么做似乎是不太美观的,我们似乎要在合约里定义一堆意义不明的变量.工业上使用的方法是通过继承,我们把所有的变量都卸载一个(或者多个)sol文件中,在逻辑合约和代理合约中以相同的顺序继承这些变量,这样就可以万无一失的保证不会出现数据冲突。

  • 函数选择器冲突

之前我们提到过函数选择器冲突:代理合约不全是使用代理请求,他至少有一个update方法来更新逻辑合约的地址,而EVM中判断call指定的方法是使用一个8byte的函数选择器(函数名是函数签名的hash 的前八位),而8位的函数选择器导致我们很难保证两个不同的函数不会产生相同的函数签名。

如果在一个合约中,solc编译器会告诉我们函数签名冲突,无法编译,但是代理合约本质是是两个毫不相关的合约,他们的关系是EVM在执行时通过地址加载的,在编译期是看不出来函数冲突的。

通用合约升级方法并没有解决这个问题,而在下面的几种方案中,这个问题会得到妥善的处理。

透明代理模式

透明代理模式将用户区分为两种,一种是普通用户,一种是ADMIN用户.

对于普通用户而言,无论是否存在函数选择器冲突,所有请求均转发的逻辑合约。

对于管理员而言,如果存在函数选择器冲突,EVM会优先命中Proxy合约(无论那种都会优先命中Proxy,如果不是admin,最后还是被转发的fallback)

contract Proxy{
    function update(address _logic) public {
        if (msg.sender != admin){
            fallback(...)
        }else{
            ...
        }
    }
    fallback{

    }
}

这是一种基于业务的方式来判断的,我们显然的知道,如果是普通用户,那么不可能会去升级合约,所以一定是转发到fallback,而对于管理员,他更关心的是升级功能,所以优先命中Proxy里面的方法.

UUPS(通用可升级代理标准)

相较与透明代理,UUPS直接将代理合约的升级函数放在逻辑合约中,这样代理合约中实际上不会有任何的逻辑函数,所有的请求都是转发的逻辑合约中处理。

为了规范化合约升级,UUPS提供了固定的代理合约,以及固定的升级函数,需要在逻辑合约中继承Proxiable合约,从而继承其中的升级代码。由于solidity在编译一个合约时可以发现函数选择器的冲突,代理合约完全不需要考虑冲突问题。

评论