Skip to content

Files

Latest commit

e5a6b32 · Jul 2, 2024

History

History

49_UUPS

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Oct 8, 2022
Mar 13, 2024
Jul 2, 2024
title tags
49. 通用可升级代理
solidity
proxy
OpenZeppelin

WTF Solidity极简入门: 49. 通用可升级代理

我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。

推特:@0xAA_Science@WTFAcademy_

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity


这一讲,我们将介绍代理合约中选择器冲突(Selector Clash)的另一个解决办法:通用可升级代理(UUPS,universal upgradeable proxy standard)。教学代码由OpenZeppelinUUPSUpgradeable简化而成,不应用于生产。

UUPS

我们在上一讲已经学习了"选择器冲突"(Selector Clash),即合约存在两个选择器相同的函数,可能会造成严重后果。作为透明代理的替代方案,UUPS也能解决这一问题。

UUPS(universal upgradeable proxy standard,通用可升级代理)将升级函数放在逻辑合约中。这样一来,如果有其它函数与升级函数存在“选择器冲突”,编译时就会报错。

下表中概括了普通可升级合约,透明代理,和UUPS的不同点:

各类个升级合约

UUPS合约

首先我们要复习一下WTF Solidity极简教程第23讲:Delegatecall。如果用户A通过合约B(代理合约)去delegatecall合约C(逻辑合约),上下文仍是合约B的上下文,msg.sender仍是用户A而不是合约B。因此,UUPS合约可以将升级函数放在逻辑合约中,并检查调用者是否为管理员。

delegatecall

UUPS的代理合约

UUPS的代理合约看起来像是个不可升级的代理合约,非常简单,因为升级函数被放在了逻辑合约中。它包含3个变量:

  • implementation:逻辑合约地址。
  • admin:admin地址。
  • words:字符串,可以通过逻辑合约的函数改变。

它包含2个函数

  • 构造函数:初始化admin和逻辑合约地址。
  • fallback():回调函数,将调用委托给逻辑合约。
contract UUPSProxy {
    address public implementation; // 逻辑合约地址
    address public admin; // admin地址
    string public words; // 字符串,可以通过逻辑合约的函数改变

    // 构造函数,初始化admin和逻辑合约地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback函数,将调用委托给逻辑合约
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
}

UUPS的逻辑合约

UUPS的逻辑合约与第47讲中的不同是多了个升级函数。UUPS逻辑合约包含3个状态变量,与代理合约保持一致,防止插槽冲突。它包含2

  • upgrade():升级函数,将改变逻辑合约地址implementation,只能由admin调用。
  • foo():旧UUPS逻辑合约会将words的值改为"old",新的会改为"new"
// UUPS逻辑合约(升级函数写在逻辑合约内)
contract UUPS1{
    // 状态变量和proxy合约一致,防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通过逻辑合约的函数改变

    // 改变proxy中状态变量,选择器: 0xc2985578
    function foo() public{
        words = "old";
    }

    // 升级函数,改变逻辑合约地址,只能由admin调用。选择器:0x0900f010
    // UUPS中,逻辑合约中必须包含升级函数,不然就不能再升级了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

// 新的UUPS逻辑合约
contract UUPS2{
    // 状态变量和proxy合约一致,防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通过逻辑合约的函数改变

    // 改变proxy中状态变量,选择器: 0xc2985578
    function foo() public{
        words = "new";
    }

    // 升级函数,改变逻辑合约地址,只能由admin调用。选择器:0x0900f010
    // UUPS中,逻辑合约中必须包含升级函数,不然就不能再升级了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

Remix实现

  1. 部署UUPS新旧逻辑合约UUPS1UUPS2

demo

  1. 部署UUPS代理合约UUPSProxy,将implementation地址指向旧逻辑合约UUPS1

demo

  1. 利用选择器0xc2985578,在代理合约中调用旧逻辑合约UUPS1foo()函数,将words的值改为"old"

demo

  1. 利用在线ABI编码器HashEx获得二进制编码,调用升级函数upgrade(),将implementation地址指向新逻辑合约UUPS2

编码

demo

  1. 利用选择器0xc2985578,在代理合约中调用新逻辑合约UUPS2foo()函数,将words的值改为"new"

demo

总结

这一讲,我们介绍了代理合约“选择器冲突”的另一个解决方案:UUPS。与透明代理不同,UUPS将升级函数放在了逻辑合约中,从而使得"选择器冲突"不能通过编译。相比透明代理,UUPS更省gas,但也更复杂。