智能合约的更新与迭代

70 阅读4分钟

智能合约部署成功后是不可篡改的,那么如果我们希望迭代升级原有的合约,我们应该怎么做呢?

我们可以通过 ERC1967 来创建一个代理合约,指向我们真实的逻辑合约,我们每个更新逻辑合约其实都是部署一个新的合约,然后将代理合约中的逻辑合约指向这个新合约

ERC1967

github.com/OpenZeppeli…

ERC1967 是一个 以太坊标准(EIP-1967),它专门为 代理合约 (Proxy Contract) 设计,
目的是 规定代理合约的存储槽 (storage slots),避免 存储冲突 (storage collision)。

背景

在升级合约(Upgradable Contracts)里,我们通常使用 代理模式 (Proxy Pattern):

用户始终调用 代理合约 (Proxy)。

代理合约把调用转发给 实现合约 (Implementation/Logic Contract)。

当需要升级时,只需要修改代理合约里记录的逻辑合约地址。

但是:
合约的状态变量是按 存储槽 (slot) 顺序存储的,如果代理合约和逻辑合约定义的变量冲突,就会导致 数据错乱。

解决方案

它规定了几个关键的 storage slot 地址(通过 keccak 哈希算出来,减少冲突概率):

  • 实现合约地址 (implementation slot) 存放当前逻辑合约地址

    bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    
  • 管理员地址 (admin slot) 存放代理管理员(有权升级的人)

    bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    
  • Beacon 地址 (beacon slot)(可选) 如果是 Beacon Proxy 模式,存放 Beacon 地址

       bytes32 internal constant _BEACON_SLOT = 
           0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
    

所有遵循 ERC1967 标准的代理合约,都会把数据放在相同、不会冲突的位置,避免了存储覆盖问题。

代码实践

github.com/Cyfrin/foun…

我们定义一个符合UUPSUpgradeable的合约,将其版本号设置为1

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract BoxV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 internal value;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() public initializer {
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function version() public pure returns (uint256) {
        return 1;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

接下来我们再定义一个新的合约,作为上面合约的升级版,我们设定其版本号为2

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract BoxV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 internal value;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() public initializer {
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 newValue) public {
        value = newValue;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function version() public pure returns (uint256) {
        return 2;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

我们先部署第一个合约,然后调用其version函数检查是否部署成功

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Script} from "forge-std/Script.sol";
import {BoxV1} from "../src/BoxV1.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract DeployBox is Script {
    function run() external returns (address) {
        address proxy = deployBox();
        return proxy;
    }

    function deployBox() public returns (address) {
        vm.startBroadcast();
        BoxV1 box = new BoxV1();
        ERC1967Proxy proxy = new ERC1967Proxy(address(box), "");
        BoxV1(address(proxy)).initialize();
        vm.stopBroadcast();
        return address(proxy);
    }
}

我们可以通过下面的指令获取版本内容

cast call <PROXY_CONTRACT_ADDRESS> "version()" \
  --rpc-url <SEPOLIA_RPC_URL_FROM_ALCHEMY> \
  --private-key <YOUR_PRIVATE_KEY>

然后我们升级合约,用来替换上述的版本1

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract BoxV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 internal value;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() public initializer {
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 newValue) public {
        value = newValue;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function version() public pure returns (uint256) {
        return 2;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

接下来,我们使用如下脚本更新合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Script} from "forge-std/Script.sol";
import {BoxV1} from "../src/BoxV1.sol";
import {BoxV2} from "../src/BoxV2.sol";
import {BoxV3} from "../src/BoxV3.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol";

contract UpgradeBox is Script {
    function run() external returns (address) {
        address mostRecentlyDeployedProxy = DevOpsTools
            .get_most_recent_deployment("ERC1967Proxy", block.chainid);

        vm.startBroadcast();
        BoxV3 newBox = new BoxV3();
        vm.stopBroadcast();
        address proxy = upgradeBox(mostRecentlyDeployedProxy, address(newBox));
        return proxy;
    }

    function upgradeBox(
        address proxyAddress,
        address newBox
    ) public returns (address) {
        vm.startBroadcast();
        BoxV1 proxy = BoxV1(payable(proxyAddress));
        proxy.upgradeTo(address(newBox));
        vm.stopBroadcast();
        return address(proxy);
    }
}

我们继续请求version函数

cast call <PROXY_CONTRACT_ADDRESS> "version()" \
  --rpc-url <SEPOLIA_RPC_URL_FROM_ALCHEMY> \
  --private-key <YOUR_PRIVATE_KEY>

在这里插入图片描述
我们成功升级了合约