智能合约的更新与迭代
智能合约部署成功后是不可篡改的,那么如果我们希望迭代升级原有的合约,我们应该怎么做呢?
我们可以通过 ERC1967 来创建一个代理合约,指向我们真实的逻辑合约,我们每个更新逻辑合约其实都是部署一个新的合约,然后将代理合约中的逻辑合约指向这个新合约
ERC1967
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/ERC1967/ERC1967Utils.sol
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 标准的代理合约,都会把数据放在相同、不会冲突的位置,避免了存储覆盖问题。
代码实践
https://github.com/Cyfrin/foundry-upgrades-cu.git
我们定义一个符合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 constructorconstructor() {_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 constructorconstructor() {_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 constructorconstructor() {_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>
我们成功升级了合约