0%

StudyRecord-使用OpenZeppelin编写可升级的智能合约

Pre:

测试代码:https://github.com/jerrychan807/my-awesome-solidity/tree/main/learn_upgrade

可升级的智能合约如何运作?

20221219181349
此图解释了可升级智能合约的工作原理。具体来说,这是透明代理模式。另一种是 UUPS 代理模式(通用可升级代理标准)。

可升级智能合约由3个合约组成:

  • 代理合约:用户与之交互的智能合约。它将保留数据/状态,这意味着数据存储在该代理合约帐户的上下文中。这是一个EIP1967标准代理合约。

  • 执行合约:智能合约提供功能和执行逻辑。请注意,数据也在本合同中定义。这是你需要去构建的智能合约。

  • ProxyAdmin合约: 关联代理合约和执行合约。

如何部署代理?如何升级代理?

当我们第一次使用 Hardhat的 OpenZeppelin Upgrades 插件部署可升级合约时,我们部署了三个合约:

  1. 部署 “Implementation contract”

  2. 部署 “ProxyAdmin contract”

  3. 部署 “Proxy contract”

当用户调用代理合约时,调用被委托给实现合约(delegate call)。

升级合约时,我们做的是:

  1. 部署一个新的执行合约

  2. 升级ProxyAdmin合约,对代理的所有调用重定向到新的执行合同

使用插件:

1
2
// 添加插件
yarn add @openzeppelin/hardhat-upgrades
1
2
// 编辑hardhat.config.ts以使用升级插件
import '@openzeppelin/hardhat-upgrades';

将使用插件里的三个函数:

1
2
3
deployProxy()
upgradeProxy()
prepareUpgrade()

初始合约V1:

合约:

普通合约和可升级合约的最大区别在于可升级合同没有constructor().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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;
}
}

省略测试脚本

部署脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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
})
  1. 要部署可升级的合约,我们使用upgrades.deployProxy()

  2. 调用initializer来指定函数,并对初始值赋值

部署到goerli测试网:

1
2
3
4
# 部署
yarn hardhat run deploy/1.deploy_box.ts --network goerli
# 验证源码
yarn hardhat verify --contract contracts/Box.sol:Box --network goerli 0x480Da334985e4443977AD71ebC0E35A4B24BDeb4
1
2
3
4
5
6
7
8
9
 yarn hardhat run deploy/1.deploy_box.ts --network goerli
yarn run v1.22.18
$ /jcoin/github/my-awesome-solidity/learn_upgrade/node_modules/.bin/hardhat run deploy/1.deploy_box.ts --network goerli
Deploying Box...
0x3Dfbe4b70669A9b2A044AE1d6cCe3B39270B8242 box(proxy) address
Error: Contract at 0x3Dfbe4b70669A9b2A044AE1d6cCe3B39270B8242 doesn't look like an ERC 1967 proxy with a logic contract address


Done in 10.55s.

这一步有点奇怪,一直会报这个错,但试了一次,是能成功部署3个合约上去。后续有空debug一下。

20221219185251

1
2
3
Box合约: 0x480Da334985e4443977AD71ebC0E35A4B24BDeb4
ProxyAdmin合约: 0x6128464E9C4020CE2B07867e927e09fb39ca85D2
TransparentUpgradeableProxy合约: 0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad

Debug结果:

部署脚本里可以把以下两行注释掉,就不会报错。

1
2
// console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
// console.log(await upgrades.erc1967.getAdminAddress(box.address)," getAdminAddress")

部署只需要这一行代码即可。

1
const box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})

artifacts,cache,.openzeppelin缓存文件删除后,插件才会重新部署三个新合约,否则只会部署一个新的TransparentUpgradeableProxy合约,而复用之前的Box合约ProxyAdmin合约

Hardhat控制台测试合约:

1
yarn hardhat console --network goerli
1
2
3
4
address = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'
box = await ethers.getContractAt("Box", address)
await box.retrieve()
//BigNumber { value: "42" }

升级合约到V2:

合约:

1
2
3
4
5
6
7
8
9
10
11
12
// 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);
}
}

省略测试脚本

升级脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// scripts/2.upgradeV2.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";

const proxyAddress = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'

async function main() {
console.log(proxyAddress," original Box(proxy) address")
const BoxV2 = await ethers.getContractFactory("BoxV2")
console.log("upgrade to BoxV2...")
const boxV2 = await upgrades.upgradeProxy(proxyAddress, BoxV2)
console.log(boxV2.address," BoxV2 address(should be the same)")

console.log(await upgrades.erc1967.getImplementationAddress(boxV2.address)," getImplementationAddress")
console.log(await upgrades.erc1967.getAdminAddress(boxV2.address), " getAdminAddress")
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})

通过该脚本,我们把Box合约升级到BoxV2

  1. 部署一个新的合约BoxV2

  2. 并在 ProxyAdmin 中链接到一个新的执行合约

部署到goerli测试网:

1
2
3
4
# 部署
yarn hardhat run deploy/2.upgradeV2.ts --network goerli
# 验证源码
yarn hardhat verify --contract contracts/BoxV2.sol:BoxV2 --network goerli 0xD57dA84ef78a7674b3CC74F9a70D325B8132bCB1

20221219190609

Hardhat控制台测试合约:

1
yarn hardhat console --network goerli
1
2
3
4
5
6
7
address = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'
boxv2 = await ethers.getContractAt("BoxV2", address)
await boxv2.retrieve()
//BigNumber { value: "42" }
await boxv2.increment()
await boxv2.retrieve()
//BigNumber { value: "43" }

交互的合约地址没变,但更新新合约后,多了一个increment()函数

升级合约到V3:

合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// contracts/BoxV3.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./BoxV2.sol";

contract BoxV3 is BoxV2{
string public name;

event NameChanged(string name);
function setName(string memory _name) public {
name = _name;
emit NameChanged(name);
}
}

略过测试

升级脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// scripts/3.upgradeV3.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";

const proxyAddress = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'
async function main() {
console.log(proxyAddress," original Box(proxy) address")
const BoxV3 = await ethers.getContractFactory("BoxV3")
console.log("upgrade to BoxV3...")
const boxV3 = await upgrades.upgradeProxy(proxyAddress, BoxV3)
console.log(boxV3.address," BoxV3 address(should be the same)")

console.log(await upgrades.erc1967.getImplementationAddress(boxV3.address)," getImplementationAddress")
console.log(await upgrades.erc1967.getAdminAddress(boxV3.address), " getAdminAddress")
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})

部署到goerli测试网:

1
2
3
4
# 部署
yarn hardhat run deploy/3.upgradeV3.ts --network goerli
# 验证源码
yarn hardhat verify --contract contracts/BoxV3.sol:BoxV3 --network goerli 0xf638135eD0D5cD11a5C5c2D0c46ab7367e362BFd

Hardhat控制台测试合约:

1
yarn hardhat console --network goerli
1
2
3
4
5
6
7
8
9
address = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'
boxv3 = await ethers.getContractAt("BoxV3", address)
await boxv3.retrieve()
//BigNumber { value: "42" }

await boxv3.setName("mybox")
// tx response
await boxv3.name()
//'mybox'

20221219191534

升级合约到V4:

合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// contracts/BoxV4.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./BoxV2.sol";

contract BoxV4 is BoxV2{
string private name;

event NameChanged(string name);
function setName(string memory _name) public {
name = _name;
emit NameChanged(name);
}

function getName() public view returns(string memory){
return string(abi.encodePacked("Name: ",name));
}
}

升级脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// scripts/4.prepareV4.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";

const proxyAddress = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'

async function main() {
console.log(proxyAddress," original Box(proxy) address")
const BoxV4 = await ethers.getContractFactory("BoxV4")
console.log("Preparing upgrade to BoxV4...");
const boxV4Address = await upgrades.prepareUpgrade(proxyAddress, BoxV4);
console.log(boxV4Address, " BoxV4 implementation contract address")
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})

调用时upgrades.upgradeProxy(),完成两项工作:

  1. 部署了一个执行合约

  2. 调用 ProxyAdmin合约的upgrade()来链接 Proxy 和执行合约。

我们此处调用的upgrades.prepareUpgrade()时只完成第一项,第二项留给开发者手动完成。

部署到goerli测试网:

1
2
3
4
# 部署
yarn hardhat run deploy/4.prepareV4.ts --network goerli
# 验证源码
yarn hardhat verify --contract contracts/BoxV4.sol:BoxV4 --network goerli 0xd37950f48eb44fba46b9c722d6c8dc4739c61232

手动在ProxyAdmin合约上执行upgrade动作。
20221219192320

Hardhat控制台测试合约:

1
yarn hardhat console --network goerli
1
2
3
4
address = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad'
box = await ethers.getContractAt("BoxV4", address)
await box.getName()
//'Name: mynewbox'

Refs: