Pre:
测试代码:https://github.com/jerrychan807/my-awesome-solidity/tree/main/learn_upgrade
可升级的智能合约如何运作?
此图解释了可升级智能合约的工作原理。具体来说,这是透明代理模式。另一种是 UUPS 代理模式(通用可升级代理标准)。
可升级智能合约由3个合约组成:
代理合约
:用户与之交互的智能合约。它将保留数据/状态,这意味着数据存储在该代理合约帐户的上下文中。这是一个EIP1967标准代理合约。
执行合约
:智能合约提供功能和执行逻辑。请注意,数据也在本合同中定义。这是你需要去构建的智能合约。
ProxyAdmin合约
: 关联代理合约和执行合约。
如何部署代理?如何升级代理?
当我们第一次使用 Hardhat的 OpenZeppelin Upgrades 插件部署可升级合约时,我们部署了三个合约:
部署 “Implementation contract”
部署 “ProxyAdmin contract”
部署 “Proxy contract”
当用户调用代理合约时,调用被委托给实现合约(delegate call)。
升级合约时,我们做的是:
部署一个新的执行合约
升级ProxyAdmin合约,对代理的所有调用重定向到新的执行合同
使用插件:
1 2 yarn add @openzeppelin/hardhat-upgrades
1 2 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 pragma solidity ^0.8 .0 ; contract Box { uint256 private value; event ValueChanged (uint256 newValue); function store (uint256 newValue ) public { value = newValue; emit ValueChanged (newValue); } 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 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" ) } main ().catch ((error ) => { console .error (error) process.exitCode = 1 })
要部署可升级的合约,我们使用upgrades.deployProxy()
调用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一下。
1 2 3 Box合约: 0x480Da334985e4443977AD71ebC0E35A4B24BDeb4 ProxyAdmin合约: 0x6128464E9C4020CE2B07867e927e09fb39ca85D2 TransparentUpgradeableProxy合约: 0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad
Debug结果:
部署脚本里可以把以下两行注释掉,就不会报错。
部署只需要这一行代码即可。
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 ()
升级合约到V2:
合约:
1 2 3 4 5 6 7 8 9 10 11 12 pragma solidity ^0.8 .0 ; import "./Box.sol" ;contract BoxV2 is Box { 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 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
部署一个新的合约BoxV2
并在 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
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 ()await boxv2.increment ()await boxv2.retrieve ()
交互的合约地址没变,但更新新合约后,多了一个increment()
函数
升级合约到V3:
合约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 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 ()await boxv3.setName ("mybox" )await boxv3.name ()
升级合约到V4:
合约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 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()
,完成两项工作:
部署了一个执行合约
调用 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动作。
Hardhat控制台测试合约:
1 yarn hardhat console --network goerli
1 2 3 4 address = '0xfA86bf3B1aFC147276b4a21fDe03fc59F63c60ad' box = await ethers.getContractAt ("BoxV4" , address) await box.getName ()
Refs: