Skip to main content

合约升级方案

目前主流的可升级方案大多都是使用delegatecallfallback,部署一个Proxy合约的方式来实现。

使用openzeppelin@openzeppelin/hardhat-upgrades

hardhat 中集成

  • 安装依赖
yarn add -D @openzeppelin/hardhat-upgrades @nomiclabs/hardhat-ethers ethers
  • hardhat.config配置文件中导入依赖

如果是 JS

// hardhat.config.js
require('@openzeppelin/hardhat-upgrades');

如果是 TS

// hardhat.config.ts
import '@openzeppelin/hardhat-upgrades';
  • 示例合约

注意: 可升级合约的部署不会调用constructor方法。所以实现一个initialize方法来做合约初始化

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.2;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract TodoList is Initializable {
string[] private list;
address public admin;

event AddTodo(address indexed author, string item);

function initialize(address _admin) external initializer {
admin = _admin;
}

function addTodo(string memory message) external {
list.push(message);
emit AddTodo(msg.sender, message);
}

function getTodoList() external view returns (string[] memory totoList) {
totoList = list;
}

// 相比V1 增加了获取列表长度的方法
// function getTodoListLength() external view returns(uint256) {
// return list.length;
// }
}
  • 第一次部署合约
async function main() {
// 第一次部署合约
const signers = await ethers.getSigners();
const admin = signers[0];
const TodoList = await ethers.getContractFactory("TodoList");
// 可以部署的时候进行初始化传参
// const instance = await upgrades.deployProxy(TodoList, [admin.address]);
// 也可以先部署,然后调用初始化方法.这里要禁止插件自动调用初始化方法
const instance = await upgrades.deployProxy(TodoList, { initializer: false });
await instance.initialize(admin.address);
await instance.deployed();
console.log(`proxy deployed: ${instance.address}`);
}
  • 测试

注意,这里本地测试的时候要加上--network localhost,否则每次都会生成一个新的网络节点

npx hardhat run scripts/deploy.ts --network localhost

其他 truffle 等集成方案可以参考Upgrades Plugins

注意 ProxyAdmin

当我们通过插件部署一个新的合约时,插件会同时部署一个ProxyAdmin合约,用于管理owneradminProxyAdmin的初始owner就是部署者账号.owneradmin的区别如下:

  • owner: 拥有合约的升级和交互权限
  • admin: 只拥有合约的升级权限,没有合约的交互权限

可以通过以下方法转移owneradmin

  • admin.changeProxyAdmin: 这个方法可以修改合约的admin
  • admin.transferProxyAdminOwnership: 可以将ProxyAdminowner权限转移给一个新的账号。则新的账号会拥有升级和交互权限,所以调用时要谨慎;当我们把owner权限转移出去以后,依然可以使用当前账号执行prepareUpgrade方法,用于验证新升级的合约是否与老的兼容,然后由真正的owner执行升级

具体参考:

注意: 新的升级合约要保证不会破坏老合约插槽分部,否则会发生数据错乱

参考资料