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

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

推特:@0xAA_Science

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

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


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

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合约可以将升级函数放在逻辑合约中,并检查调用者是否为管理员。

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

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

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

  4. 利用在线ABI编码器(HashEx)[https://abi.hashex.org/]获得二进制编码,调用升级函数upgrade(),将implementation地址指向新逻辑合约UUPS2

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

总结

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

Subscribe to 0xAA
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.