【翻】如何在以太坊上构建可升级的合约

如今智能合约的生态已经比较完善。无论我们在编码时多么的仔细,测试多么的严密,如果我们构建一个复杂的项目,那么很有可能我们会需要更新业务逻辑,给系统打补丁,修复bug或添加新的特性。有时,我们可能因为EVM的改变或因为新的漏洞的发现而需要更新我们的合约代码。

通常情况下,对于传统的开发项目来说,开发者可以随时更新代码。但是智能合约与此不同,它一旦部署完成之后代码就无法修改。然而,如果我们使用合适的技术,我们可以部署一个新的合约,并且抛弃旧的合约。如下是目前创建可升级合约的几种常见的开发模型。

Master-Slave contracts 主从模式

主从模式是最简单也最好理解的一种构建可升级合约的开发模型。在技术上来说,我们首先部署一个master合约。master合约里存放其他合约的合约地址,并在需要时返回所需的合约地址。作为slave的合约从master合约获取其他合约的最新地址并与之通信。在更新一个合约时,我们只需要重新部署一个新的合约,并在master合约中更新其地址。这种方法构建的可升级模型虽然简单,但并不是很完美,其中一个限制就是它无法将旧合约的数据迁移到新的合约之上。

Eternal Storage contracts 外部存储合约

在此模型中,我们将业务逻辑和数据分离成两个独立的合约。数据合约是固定的不可升级合约。业务合约可以根据需求随时更新,更新完成之后数据合约会收到通知。这种模型中存在一个很明显的缺点。因为数据合约是不可升级的,任何对于数据结构更改的需求或数据合约的bug都可能导致所有的数据失效。还有一点就是,业务合约如果需要访问或修改数据,那么它需要发起一个外部调用,这种调用会消耗更多的gas。这种模型通常与Master-Slave模型结合使用来促进合约的通信。

Upgradable Storage Proxy Contracts 可升级的存储代理合约

如果我们将存储合约作为业务合约的一个代理,那么就可以节省这笔额外的gas支出。代理合约和业务合约,会继承相同的存储合约,这样一来它们在EVM中对于存储的引用就是一致的。代理合约会有一个fallback方法,此方法会通过delegate call调用业务合约,这样子在代理合约的存储中就可以对业务合约做出改变。代理合约是固定不变的,它目前节省了针对存储合约的外部调用的gas,无论数据做出多少改变,它只需要发起一个delegate调用。

在这个模型中存在三个组件

  1. Proxy contract 代理合约:它作为外部存储和delegate调用业务合约。
  2. Logic contract 业务合约:业务逻辑,操作数据。
  3. Storage structure:数据结构基类合约,它同时被代理和业务合约所继承,以便保持存储指针在链上的一致性。
architecture

Delegate Call

这个技术模型的核心在于EVM提供的DELEGATECALLopcode。DELEGATECALL与普通的调用的区别在于,虽然真正执行的代码在被调用的目标地址上,但是代码执行的上下文确在调用合约上(msg.sendermsg.value的值保持原样,这是它与合约之间的external调用的最大区别)。因此,当使用DELEGATECALL时,目标地址的代码被执行,但是Storage、address和调用合约的余额才是真正被使用的数据(不会使用目标合约中的数据)。换句话说,DELEGATECALL允许目标地址在执行时使用调用合约中的存储。

我们可以利用这一优势来创建代理合约,它会DELEGATECALL代理调用到业务合约,这样子我们可以将数据安全的保存在代理合约里面,然后根据需要更新我们的业务合约。

How to use upgradable storage proxy contracts? 如何使用可升级的存储代理合约?

接下来让我们往下研究的更深入一点。第一个合约需要被创建的是存储结构。它会定义所有我们需要的存储变量,并且这个合约会被代理和业务合约所继承。例如:

contract StorageStructure {
    address public implementation;
    address public owner;
    mapping (address => uint) internal points;
    uint internal totalPlayers;
}

接下来我们需要一个业务合约。让我们创建一个buggy实现,这个合约当新的玩家被添加的时候不会递增totalPlayers

contract ImplementationV1 is StorageStructure {
    modifier onlyOwner() {
        require (msg.sender == owner);
        _;
    }
 
    function addPlayer(address _player, uint _points) 
        public onlyOwner 
    {
        require (points[_player] == 0);
        points[_player] = _points;
    }
    function setPoints(address _player, uint _points) 
        public onlyOwner 
    {
        require (points[_player] != 0);
        points[_player] = _points;
    }
}

接下来是最重要的部分 —— 代理合约:

contract Proxy is StorageStructure {
    
    modifier onlyOwner() {
        require (msg.sender == owner);
        _;
    }
    
    /**
     * @dev constructor that sets the owner address
     */
    constructor() public {
        owner = msg.sender;
    }
    
    /**
     * @dev Upgrades the implementation address
     * @param _newImplementation address of the new implementation
     */
    function upgradeTo(address _newImplementation) 
        external onlyOwner 
    {
        require(implementation != _newImplementation);
        _setImplementation(_newImplementation);
    }
    
    /**
     * @dev Fallback function allowing to perform a delegatecall 
     * to the given implementation. This function will return 
     * whatever the implementation call returns
     */
    function () payable public {
        address impl = implementation;
        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) }
        }
    }
    
    /**
     * @dev Sets the address of the current implementation
     * @param _newImp address of the new implementation
     */
    function _setImplementation(address _newImp) internal {
        implementation = _newImp;
    }
}

接下来我们需要部署ProxyImplementationV1合约,然后调用代理合约的upgradeTo(address)方法,传入的变量是部署完成之后的ImplementationV1的地址。然后ImplementationV1的合约地址就没用了,代理合约就是我们的主合约,接下来所有的交互通过代理合约来进行。

要更新合约的话,我们需要创建一个新的业务合约的实现,例如:

contract ImplementationV2 is ImplementationV1 {
 
    function addPlayer(address _player, uint _points) 
        public onlyOwner 
    {
        require (points[_player] == 0);
        points[_player] = _points;
        totalPlayers++;
    }
}

这里需要注意的是,新的业务合约仍然继承了StorageStructure合约,虽然是非直接继承。

所有的业务合约都需要继承StorageStructure合约,并且Storage合约在代理合约部署完成之后就不应该再改变,以避免无意识的覆盖了代理合约中的数据

要更新新的业务合约,我们将ImplementationV2合约部署完成之后,调用代理合约的upgradeTo(address)方法,传入ImplementationV2合约地址作为参数。

这个技术模型允许我们非常方便的更新业务合约,但是它仍然不允许更新存储合约的数据结构。这一问题我们可以通过接下来介绍的unstructured proxy contracts技术模型来解决。

Unstructured Upgradable Storage Proxy Contracts

这是目前构建可升级合约的最先进的技术模型。它把业务合约的地址和owner的地址存储在代理合约的固定的存储位置,以避免业务合约对其可能造成的覆盖。我们可以使用sloadsstoreopcodes来直接读取和写入到指定的存储slot中,然后通过固定的锚点来引用它。

关于state variable在存储中的布局可以参考layout of state variables in storage。将数据存储在固定的slot中,可以避免其被业务合约覆盖。假如我们将fixed position设置为0x7,它会被初始的7个存储slots之后的数据所覆盖。为了避免这种情况,我们将fixed position设置为这种形式:keccak256(“org.govblocks.implemenation.address”)

以这种技术模型来构建可升级合约时,代理合约不需要继承StorageStructure合约,这意味着我们的StorageStructure合约也是可升级的。但是升级storage struct是一项棘手的任务,因为我们需要确保新的改动不会造成新的存储布局和之前的存储布局之间的重叠

There are two components of this technique

  1. Proxy Contract:它将业务合约的地址存储在一个固定的存储slot中,并通过DELEGATECALL调用它。
  2. Implementation Contract:业务合约,它包含了业务逻辑和存储结构。

这个技术模型不需要业务合约做任何改变,你甚至可以使用之前编写好的合约来构建成可升级的模型。

代理合约代码样例如下:

contract UnstructuredProxy {
    
    // Storage position of the address of the current implementation
    bytes32 private constant implementationPosition = 
        keccak256("org.govblocks.implementation.address");
    
    // Storage position of the owner of the contract
    bytes32 private constant proxyOwnerPosition = 
        keccak256("org.govblocks.proxy.owner");
    
    /**
    * @dev Throws if called by any account other than the owner.
    */
    modifier onlyProxyOwner() {
        require (msg.sender == proxyOwner());
        _;
    }
    
    /**
    * @dev the constructor sets owner
    */
    constructor() public {
        _setUpgradeabilityOwner(msg.sender);
    }
    
    /**
     * @dev Allows the current owner to transfer ownership
     * @param _newOwner The address to transfer ownership to
     */
    function transferProxyOwnership(address _newOwner) 
        public onlyProxyOwner 
    {
        require(_newOwner != address(0));
        _setUpgradeabilityOwner(_newOwner);
    }
    
    /**
     * @dev Allows the proxy owner to upgrade the implementation
     * @param _implementation address of the new implementation
     */
    function upgradeTo(address _implementation) 
        public onlyProxyOwner
    {
        _upgradeTo(_implementation);
    }
    
    /**
     * @dev Tells the address of the current implementation
     * @return address of the current implementation
     */
    function implementation() public view returns (address impl) {
        bytes32 position = implementationPosition;
        assembly {
            impl := sload(position)
        }
    }
    
    /**
     * @dev Tells the address of the owner
     * @return the address of the owner
     */
    function proxyOwner() public view returns (address owner) {
        bytes32 position = proxyOwnerPosition;
        assembly {
            owner := sload(position)
        }
    }
    
    /**
     * @dev Sets the address of the current implementation
     * @param _newImplementation address of the new implementation
     */
    function _setImplementation(address _newImplementation) 
        internal 
    {
        bytes32 position = implementationPosition;
        assembly {
            sstore(position, _newImplementation)
        }
    }
    
    /**
     * @dev Upgrades the implementation address
     * @param _newImplementation address of the new implementation
     */
    function _upgradeTo(address _newImplementation) internal {
        address currentImplementation = implementation();
        require(currentImplementation != _newImplementation);
        _setImplementation(_newImplementation);
    }
    
    /**
     * @dev Sets the address of the owner
     */
    function _setUpgradeabilityOwner(address _newProxyOwner) 
        internal 
    {
        bytes32 position = proxyOwnerPosition;
        assembly {
            sstore(position, _newProxyOwner)
        }
    }
}

How to use unstructured upgradable storage proxy contracts?

使用unstructed upgradeable storage proxy contracts非常简单,因为此模型几乎可以与所有的现存的合约进行组合。通过如下的步骤来使用它:

  1. 部署代理合约和业务合约。
  2. 调用代理合约的upgradeTo(address)方法,调用时传递业务合约的地址作为参数。

接下来所有的交互就只需要与代理合约进行就可以了。并且交互方法与直接调用业务合约没有区别。

要更新业务合约,我们只需要部署一个新的业务合约,然后调用代理合约的upgradeTo(address)方法,调用时将新部署的业务合约地址作为参数传递即可。就是这么滴简单!

让我们举一个栗子看看它是如何工作的。我们将会使用在上个技术模型讲解时使用的相同的合约,但是我们不需要继承storage structure,那么我们的业务合约ImplementationV1就是这样的:

contract ImplementationV1 {
    address public owner;
    mapping (address => uint) internal points;
    
    modifier onlyOwner() {
        require (msg.sender == owner);
        _;
    }
      
    function initOwner() external {
        require (owner == address(0));
        owner = msg.sender;
    }
    
    function addPlayer(address _player, uint _points) 
        public onlyOwner 
    {
        require (points[_player] == 0);
        points[_player] = _points;
    }
    
    function setPoints(address _player, uint _points) 
        public onlyOwner 
    {
        require (points[_player] != 0);
        points[_player] = _points;
    }
}

接下来我们需要部署此合约到链上,然后调用代理合约的upgradeTTo(address)方法,将新的地址作为参数传递进去。

你可以注意到totalPlayers变量在这个合约中是没有声明的。我们可以创建一个新的合约,在这个新的合约中包含totalPlayers变量的声明的使用。新的合约如下:

contract ImplementationV2 is ImplementationV1 {
    uint public totalPlayers;
 
    function addPlayer(address _player, uint _points) 
        public onlyOwner 
    {
        require (points[_player] == 0);
        points[_player] = _points;
        totalPlayers++;
    }
}

升级到这个新的业务合约的步骤和上面一样,调用代理合约的upgradeTo(address)方法,然后将新合约的地址作为参数传递进去即可。那么我们的合约就加上的追踪totalPlayers的功能,同时对于用户来说调用的地址还是同一个(代理合约的地址)。

这个技术模型虽然强大,但是还是有一些限制。其中之一就是proxyOwner拥有很大的权利。这个模型对于复杂的系统来说还不够用,将上面的Master-Slaveunstructured upgradable storage proxy contract模型结合使用对于构建可升级合约的dApp会更加灵活。这也是我们在GovBlocks上使用的技术。

Conclusion 总结

Unstructured Storage Proxy Contracts模型是最高级的一种模型,但是它也并非完美。我们在GovBlocks中,不希望dApp的owner拥有绝对的权利。毕竟它们是去中心化的应用程序。所以,我们决定在我们的代理合约中使用网络范围的授权人,而不是使用单一的proxyOwner。我将会在未来的文章中介绍这种实现的方法。同时,我推荐阅读Nitika的文章argument against the use of onlyOwner,你可以在我们的github上查看我们的代理合约代码。

希望这篇文章对你有帮助!

推荐关注Zepplin对于这一块的技术研究。

完!

参考资料
原文:How to make smart contracts upgradable!
Proxy Patterns

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

推荐阅读更多精彩内容

  • 问题 智能合约一旦发布就不能修改了,那如果出现bug怎么办?可以开发一个新的合约,但之前合约中的数据怎么办?重新导...
    SlowGO阅读 753评论 0 0
  • 参考https://docs.zeppelinos.org/docs/start.html,可以在truffle下...
    oracle3阅读 1,114评论 0 0
  • 以太坊(Ethereum ):下一代智能合约和去中心化应用平台 翻译:巨蟹 、少平 译者注:中文读者可以到以太坊爱...
    车圣阅读 3,711评论 1 7
  • 今天梁先生一下班回来看到我还没准备好晚餐,脸色有点不好。 我说我刚从外面回来。 他把菜拿去厨房热,看了下燃气值(炒...
    静芝阅读 480评论 0 0
  • 当孩子第一次跳绳不会跳时,生气地说:我再也不跳了! 当还孩子下棋下输了,宣布不跟你玩了! 当孩子面对恐惧,老觉得床...
    艾上花开阅读 615评论 1 7