在本文中,通过 7 个任务,如何来编写可升级合约,测试以及自动、活动实施升级。 在 7 个任务中,分别介绍了可升级合约可能遇到的各种情况: 在新实现合约中添加函数、添加状态变量、修改状态变量可见性(修改函数)。
有几篇关于可升级智能合约或代理模式的好文章。但找不到一个分步骤的指南来构建、部署代理合约并与之交互。
在本教程中,列出了7个细节任务,你可以按照这些任务将代理合约部署到本地测试网和公共测试网Ropsten。每个任务都有几个子任务。我们将在这里使用OpenZeppelin代理合约和OpenZeppelin Upgrades plugin for Hardhat(或Truffle)。
特别感谢OpenZeppelin生态系统中的两个相关指南: OpenZeppelin Upgrades教程: 使用Hardhat和 Gnosis Safe 进行合约升级 和 通过多签和 Defender 升级合约.
这个插图解释了可升级智能合约的工作原理。具体来说,这是透明代理模式,另一个是UUPS代理模式(通用可升级代理标准)。
可升级智能合约实际上有3个合约:
ProxyAmdin在OpenZeppelin 文档中解释:
什么是ProxyAmdin?
ProxyAdmin是一个合约,它作为所有代理合约的所有者(Owner)。每个网络通过部署一个ProxyAmdin就可以。当你启动项目时,ProxyAdmin由部署者地址拥有,但你可以通过调用transferOwnership来转移它的所有权。
当把ProxyAmin的所有权转移到一个多重签名的账户时,升级Proxy合约(将代理链接到新的实现)的权力也会转移到多重签名身上。
当我们第一次使用OpenZeppelin升级插件为Hardhat部署可升级的合约时,我们部署了三个合约:
实现合约
ProxyAdmin合约
代理合约
在ProxyAdmin合约中,实现和代理被关联起来。
当用户调用代理合约时,调用被委托给实现合约执行(委托调用)。
当升级合约时,我们所做的是:
实现合约
ProxyAdmin合约
中升级,将所有对代理的调用重定向到新的实现合约。Hardhat/Truffle的OpenZeppelin Upgrades插件可以帮助我们完成升级工作。
如果你想知道如何修改合约使其可升级,你可以参考OpenZeppelin的文档, 见链接。
让我们开始编写和部署一个可升级的智能合约,你可以在文末找到本文网站的代码。
我们将使用Hardhat、Hardhat Network本地测试网和OpenZeppelin Upgrades插件。
第1步:安装hardhat并启动一个项目
mkdir solproxy && cd solproxy
yarn init -y
yarn add harthat
yarn hardhat
// choose option: sample typescript
第2步:添加插件@openzeppelin/hardhat-upgrades
yarn add @openzeppelin/hardhat-upgrades
编辑hardhat.config.ts
以使用Upgrades插件:
// hardhat.config.ts
import '@openzeppelin/hardhat-upgrades';
我们将使用hardhat升级插件的三个功能(API参考链接)。
deployProxy()
upgradeProxy()
prepareUpgrade()
我们使用OpenZeppelin学习指南中的Box.sol
合约。我们将建立这个合约的几个版本。
普通合约和可升级合约的最大区别是,可升级合约没有constructor()
,文档链接。
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Box {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
解释一下:
store()
把一个值存到这个合约中,并在以后调用retrieve()
获取它。Box.sol
的单元测试脚本让我们为Box.sol
编写单元测试脚本。下面的Hardhat单元测试脚本改编自(
OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat)。我们在其中做了一些修改。
编辑test/1.Box.test.ts
。
// test/1.Box.test.ts
import { expect } from "chai";
import { ethers } from "hardhat"
import { Contract, BigNumber } from "ethers"
describe("Box", function () {
let box:Contract;
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box")
box = await Box.deploy()
await box.deployed()
})
it("should retrieve value previously stored", async function () {
await box.store(42)
expect(await box.retrieve()).to.equal(BigNumber.from('42'))
await box.store(100)
expect(await box.retrieve()).to.equal(BigNumber.from('100'))
})
})
// NOTE: should also add test for event: event ValueChanged(uint256 newValue)
运行测试:
yarn hardhat test test/1.Box.test.ts
结果:
Box
✓ should retrieve value previously stored
1 passing (505ms)
✨ Done in 3.34s.
OpenZeppelin Upgrades plugin for Hardhat
编写部署脚本当我们写脚本来部署智能合约时,我们使用:
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
为了部署一个可升级的合约,将调用deployProxy()
,文档可以在链接找到。
const Box = await ethers.getContractFactory("Box")
const box = await upgrades.deployProxy(Box,[42], { initializer: 'store' })
在第二行,我们使用OpenZeppelin升级插件,通过调用store()
作为初始化器,以初始值42
部署Box
。
编辑scripts/1.deploy_box.ts
:
// scripts/1.deploy_box.ts
import { ethers } from "hardhat"
import { upgrades } from "hardhat"
async function main() {
const Box = await ethers.getContractFactory("Box")
console.log("Deploying Box...")
const box = await upgrades.deployProxy(Box,[42], { initializer: 'store' })
console.log(box.address," box(proxy) address")
console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
console.log(await upgrades.erc1967.getAdminAddress(box.address)," getAdminAddress")
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
解释一下:
upgrades.deployProxy()
部署一个可升级的合约Box.sol
。让我们在本地testnet中运行部署脚本。
第1步:在另一个终端运行一个独立的hardhat 测试网络。
yarn hardhat node
第2步:运行部署脚本
yarn hardhat run scripts/1.deploy_box.ts --network localhost
结果:
Deploying Box...
0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 box(proxy) address
0x5FbDB2315678afecb367f032d93F642f64180aa3 getImplementationAddress
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 getAdminAddress
✨ Done in 3.83s.
用户可以通过box(proxy)地址与Box合约交互:0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
.
注意:如果你多次运行这个部署,你可以发现ProxyAdmin保持不变:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
。
Box.sol
通过代理模式是否正常工作你可能会发现,用户通过box(proxy)合约与实现合约进行交互。
为了确保它们正确工作,让我们添加单元测试进行测试。在单元测试中,我们使用upgrades.deployProxy()
部署合约,并通过box(proxy)合约再次进行交互验证结果:
编辑test/2.BoxProxy.test.ts
。
// test/2.BoxProxy.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"
describe("Box (proxy)", function () {
let box:Contract
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box")
//initilize with 42
box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
})
it("should retrieve value previously stored", async function () {
// console.log(box.address," box(proxy)")
// console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
// console.log(await upgrades.erc1967.getAdminAddress(box.address), " getAdminAddress")
expect(await box.retrieve()).to.equal(BigNumber.from('42'))
await box.store(100)
expect(await box.retrieve()).to.equal(BigNumber.from('100'))
})
})
运行测试:
yarn hardhat test test/2.BoxProxy.test.ts
结果:
Box (proxy)
✓ should retrieve value previously stored
1 passing (579ms)
✨ Done in 3.12s.`
我们的box.sol现在工作正常了。
后来,我们发现需要一个increment()
函数。与其重新部署这个合约,不如将数据迁移到新的合约,并要求所有用户访问新的合约地址。我们可以很容易地升级合约。
我们通过继承Box.sol
来写一个新版本的BoxBoxV2.sol
。
编辑contracts/BoxV2.sol
:
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Box.sol";
contract BoxV2 is Box{
// Increments the stored value by 1
function increment() public {
store(retrieve()+1);
}
}
我们编写单元测试脚本来测试本地部署的BoxV2:
编辑test/3.BoxV2.test.ts
:
// test/3.BoxV2.test.ts
import { expect } from "chai"
import { ethers } from "hardhat"
import { Contract, BigNumber } from "ethers"
describe("Box V2", function () {
let boxV2:Contract
beforeEach(async function () {
const BoxV2 = await ethers.getContractFactory("BoxV2")
boxV2 = await BoxV2.deploy()
await boxV2.deployed()
});
it("should retrievevalue previously stored", async function () {
await boxV2.store(42)
expect(await boxV2.retrieve()).to.equal(BigNumber.from('42'))
await boxV2.store(100)
expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
});
it('should increment value correctly', async function () {
await boxV2.store(42)
await boxV2.increment()
expect(await boxV2.retrieve()).to.equal(BigNumber.from('43'))
})
})
运行测试:
yarn hardhat test test/3.BoxV2.test.ts
结果:
Box V2
✓ should retrievevalue previously stored
✓ should increment value correctly
2 passing (579ms)
✨ Done in 3.38s.
我们在部署代理服务模式下,为BoxV2编写单元测试脚本:
编辑test/4.BoxProxyV2.test.ts
:
// test/4.BoxProxyV2.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"
describe("Box (proxy) V2", function () {
let box:Contract
let boxV2:Contract
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box")
const BoxV2 = await ethers.getContractFactory("BoxV2")
//initilize with 42
box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
// console.log(box.address," box/proxy")
// console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
// console.log(await upgrades.erc1967.getAdminAddress(box.address), " getAdminAddress")
boxV2 = await upgrades.upgradeProxy(box.address, BoxV2)
// console.log(boxV2.address," box/proxy after upgrade")
// console.log(await upgrades.erc1967.getImplementationAddress(boxV2.address)," getImplementationAddress after upgrade")
// console.log(await upgrades.erc1967.getAdminAddress(boxV2.address)," getAdminAddress after upgrade")
})
it("should retrieve value previously stored and increment correctly", async function () {
expect(await boxV2.retrieve()).to.equal(BigNumber.from('42'))
await boxV2.increment()
//result = 42 + 1 = 43
expect(await boxV2.retrieve()).to.equal(BigNumber.from('43'))
await boxV2.store(100)
expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
})
})
运行测试:
yarn hardhat test test/4.BoxProxyV2.test.ts
结果:
Box (proxy) V2
✓ should retrieve value previously stored and increment correctly
1 passing (617ms)
✨ Done in 3.44s.
在子任务2.2中,我们将Box(proxy)部署到0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0
。
在这个子任务中,我们将把它升级到BoxV2(部署一个新的合约,并在ProxyAdmin中把代理链接到新的实现合约)。
编辑 scripts/2.upgradeV2.ts
// sc...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!