智能合约部署成功后是不可篡改的,那么如果我们希望迭代升级原有的合约,我们应该怎么做呢?
我们可以通过 ERC1967 来创建一个代理合约,指向我们真实的逻辑合约,我们每个更新逻辑合约其实都是部署一个新的合约,然后将代理合约中的逻辑合约指向这个新合约
ERC1967
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 标准的代理合约,都会把数据放在相同、不会冲突的位置,避免了存储覆盖问题。
代码实践
我们定义一个符合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>
我们成功升级了合约