Web3教程:使用OpenZeppelin编写可升级的智能合约(代理)
关于可升级智能合约或代理模式的文章有很多,而且质量也不错。但我找不到关于如何构建、部署代理合约以及如何与之交互的详细步骤指南。所以我自己写了一篇,就是这篇。
本教程列出了7个详细步骤,您可以按照这些步骤将代理合约部署到本地测试网以及公共测试网Ropsten。每个步骤都包含若干子步骤。
我们将使用OpenZeppelin 代理合约和OpenZeppelin Upgrades 插件来开发 Hardhat(或 Truffle)。
特别感谢 OpenZeppelin 生态系统中的两个相关指南:abcoathup 的《OpenZeppelin 升级:Hardhat 和 Gnosis Safe 的分步教程》以及《OpenZeppelin Defender 指南:通过多重签名和 Defender 升级合约》。
可升级智能合约是如何运作的?
此图解释了可升级智能合约的工作原理。具体来说,这是透明代理模式。另一种是 UUPS 代理模式(通用可升级代理标准)。
可升级智能合约由 3 个合约组成:
-
代理合约。用户与之交互的智能合约。它将保存数据/状态,这意味着数据存储在该代理合约账户的上下文中。这是一个符合 EIP1967 标准的代理合约。
-
实现合约。智能合约提供功能和逻辑。请注意,数据也在此合约中定义。这就是您正在构建的智能合约。
-
ProxyAdmin 合约。该合约将代理和实现连接起来。
ProxyAmdin 在OpenZeppelin 文档中进行了解释:
什么是代理管理员?
ProxyAdmin 是一个合约,它充当所有代理的所有者。每个网络只能部署一个 ProxyAdmin。项目启动时,ProxyAdmin 的所有者是部署地址,但您可以通过调用 transferOwnership 来转移其所有权。
如果您将 ProxyAmin 所有权转移给多重签名帐户,则升级 Proxy 合约(将代理链接到新实现)的权限将转移给该帐户。
如何部署代理?如何升级代理?
当我们首次使用 OpenZeppelin Upgrades 插件为 Hardhat 部署可升级合约时,我们会部署三个合约:
- 部署“实施合同”
- 部署“ProxyAdmin 合约”
- 部署“代理合约”
在 ProxyAdmin 合约中,实现和代理是关联的。
当用户调用代理合约时,该调用被委托给实现合约(委托调用)。
升级合同时,我们会这样做:
- 部署新的“实施合同”
- 在“ProxyAdmin 合约”中进行升级,将所有对代理的调用重定向到新的实现合约。
OpenZeppelin Upgrades 的 Hardhat/Truffle 插件可以帮助我们完成这些工作。
如果您想了解如何修改合约使其可升级,可以参考 OpenZeppelin 文档:链接。
让我们开始编写和部署一个可升级的智能合约。您可以在 GitHub 上找到代码仓库:https://github.com/fjun99/proxy-contract-example
任务 1:编写可升级的智能合约
任务1.1:初始化安全帽项目
我们将使用 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以使用升级插件。
// hardhat.config.ts
import '@openzeppelin/hardhat-upgrades';
我们将使用硬帽升级插件的三个功能(API 参考链接):
js
deployProxy()
upgradeProxy()
prepareUpgrade()
1.2:编写可升级的智能合约
我们使用OpenZeppelin 学习指南Box.sol中的合约。我们将构建此合约的几个版本:
- Box.sol
- BoxV2.sol
- BoxV3.sol
- BoxV4.sol
constructor()普通合同和可升级合同的最大区别在于,可升级合同没有文档链接。
js
// 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()之后也可以再赋值。
任务 1.3:单元测试脚本Box.sol
让我们来编写单元测试脚本Box.sol。以下 Hardhat 单元测试脚本改编自(
OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat)。我们对其进行了一些修改。
编辑test/1.Box.test.ts:
js
// 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.
任务 2:部署可升级的智能合约
任务 2.1:编写包含“OpenZeppelin Upgrades 插件 for Hardhat”的部署脚本
当我们编写部署智能合约的脚本时,我们会这样写:
js
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
要部署可升级合约,将调用相应的方法。文档可在链接deployProxy()中找到。
js
const Box = await ethers.getContractFactory("Box")
const box = await upgrades.deployProxy(Box,[42], { initializer: 'store' })
在第二行中,我们使用 OpenZeppelin Upgrades 插件,通过调用初始化器来部署Box初始值。42store()
编辑scripts/1.deploy_box.ts
js
// 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
})
解释:
Box.sol我们使用以下方式部署可升级合约upgrades.deployProxy()。- 我们将部署三个合约:实施合约、代理管理合约和代理合约。我们会记录它们的地址以供检查。
任务 2.2:将合约部署到本地测试网。
让我们在本地测试网运行部署脚本。
步骤 1:在另一个终端中运行独立的硬帽测试网:
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(代理)地址与 box 合约进行交互:0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0。
注意:如果多次运行此部署,您会发现 ProxyAdmin 始终相同:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
任务 2.3:测试Box.sol使用代理模式是否正常工作
用户通过 box(代理)合约与实现合约进行交互。
为了确保它们正常工作,我们为这个场景添加单元测试。在单元测试中,我们使用upgrades.deployProxy()box(代理)合约部署合约并通过该合约进行交互。
编辑test/2.BoxProxy.test.ts
js
// 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")
//initialize 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.{% raw %}`
```
Our box.sol is working correctly now.
Later, we find that we need an `increment()` function. Instead of re-deploying this contract, migrate data to the new contract and ask all users to access the new contract address. We can upgrade the contract easily thanks to the proxy pattern.
---

## Task 3: Upgrade smart contract to BoxV2
### Task 3.1: write new implementation
We write a new version of Box `BoxV2.sol` by inherit `Box.sol`.
Edit `contracts/BoxV2.sol`
``` solidity
// 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);
}
}
```
### Task 3.2: test script for normal deployment
We write a unit test script to test BoxV2 when deployed locally.
Edit `test/3.BoxV2.test.ts`
``` js
// 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 retrieve value 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'))
})
})
```
Run test:
```
yarn hardhat test test/3.BoxV2.test.ts
```
results:
```
Box V2
✓ should retrieve value previously stored
✓ should increment value correctly
2 passing (579ms)
✨ Done in 3.38s.
```
### Task 3.3: test script for upgradeable deployment
We write a unit test script for BoxV2 deployed in proxy pattern:
- first, we deploy the Box.sol
- then we upgrade it to BoxV2.sol
- test whether BoxV2 works correctly.
Edit `test/4.BoxProxyV2.test.ts`:
``` js
// 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'))
})
})
```
Run test:
```
yarn hardhat test test/4.BoxProxyV2.test.ts
```
Results:
```
Box (proxy) V2
✓ should retrieve value previously stored and increment correctly
1 passing (617ms)
✨ Done in 3.44s.
```
### Task 3.4: write upgrade script
In sub-task 2.2, we deploy Box(proxy) to `0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0`.
In this sub-task, we will upgrade it to BoxV2 (deploy a new contract , and link proxy to a new implementation contract in ProxyAdmin):
Edit
``` js
// scripts/2.upgradeV2.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";
const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
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
})
```
### Task 3.5: run the upgrade script
We will start over from the beginning.
STEP 1: run a new local testnet in another terminal:
```
yarn hardhat node
```
STEP 2: deploy Box V1
```
yarn hardhat run scripts/1.deploy_box.ts --network localhost
```
The box(proxy) will be deployed to: `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0`.
STEP 3: upgrade to Box V2
```
yarn hardhat run scripts/2.upgradeV2.ts --network localhost
```
results:
```
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 original Box(proxy) address
upgrade to BoxV2...
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 BoxV2 address(should be the same)
0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 getImplementationAddress
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 getAdminAddress
✨ Done in 3.64s.
```
---

## Task 4: Play with contract in hardhat console
Let's play with Box(proxy) contract in hardhat console.
### Task 4.1: play with the proxy contract
Run hardhat console connecting to local testnet.
```
yarn hardhat console --network localhost
```
In hardhat console:
``` js
address = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
boxv2 = await ethers.getContractAt("BoxV2", address)
await boxv2.retrieve()
//BigNumber { value: "42" }
await boxv2.increment()
// tx response
// {
// hash: '0x3e8c9dd8842d3315cadad2a80b592ac369e644edc5cec16f7a22c76d49e4b921',
// blockNumber: 6,
await boxv2.retrieve()
//BigNumber { value: "43" }
await boxv2.store(100)
// tx response ...
await boxv2.retrieve()
//BigNumber { value: "100" }
```
### Task 4.2: try to interact with the implementation contact
Let's try to interact with the implementation contact to see what's happening.
``` js
addressimp = '0x5fc8d32690cc91d4c39d9d3abcbd16989f875707'
boximp = await ethers.getContractAt("BoxV2", addressimp)
await boximp.retrieve()
//BigNumber { value: "0" }
await boximp.increment()
// tx response ...
await boximp.retrieve()
BigNumber { value: "1" }
```
We will find that the original value in the implementation is `0`. This is because the data is stored in the context of Proxy contract (`0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0`) under the hood, not in the context of Implementation contract(`0x5fc8d32690cc91d4c39d9d3abcbd16989f875707`).
---

## Task 5: Build BoxV3 to add new state variable
We can add new state variables as long as you don't change the the layout of the state variables.
To put it simply, you can add a new one. We will add a state variable `string public name`.
### Task 5.1: write `BoxV3.sol`
We write `BoxV3.sol` by inheriting `BoxV2`:
``` js
// 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);
}
}
```
### Task 5.2: write test for proxy deployment
We skip local unit test here. We will test the proxy deployment only.
``` js
// test/5.BoxProxyV3.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"
describe("Box (proxy) V3 with name", function () {
let box:Contract
let boxV2:Contract
let boxV3:Contract
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box")
const BoxV2 = await ethers.getContractFactory("BoxV2")
const BoxV3 = await ethers.getContractFactory("BoxV3")
//initialize with 42
box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
boxV2 = await upgrades.upgradeProxy(box.address, BoxV2)
boxV3 = await upgrades.upgradeProxy(box.address, BoxV3)
})
it("should retrieve value previously stored and increment correctly", async function () {
expect(await boxV2.retrieve()).to.equal(BigNumber.from('42'))
await boxV3.increment()
expect(await boxV2.retrieve()).to.equal(BigNumber.from('43'))
await boxV2.store(100)
expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
})
it("should set name correctly in V3", async function () {
expect(await boxV3.name()).to.equal("")
const boxname="my Box V3"
await boxV3.setName(boxname)
expect(await boxV3.name()).to.equal(boxname)
})
})
// NOTE: should also add test for event: event NameChanged(string name)
```
Run Test:
```
yarn hardhat test test/5.BoxProxyV3.test.ts
```
Results:
```
Box (proxy) V3 with name
✓ should retrieve value previously stored and increment correctly
✓ should set name correctly in V3
2 passing (748ms)
✨ Done in 3.12s.
```
### Task 5.3: write upgrade script
Edit `scripts/3.upgradeV3.ts`:
``` js
// scripts/3.upgradeV3.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";
const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
// const proxyAddress = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
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
})
```
### Task 5.4: deploy and interact with BoxV3
STEP 1: upgrade to BoxV3
```
yarn hardhat run scripts/3.upgradeV3.ts --network localhost
```
results:
```
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 original Box(proxy) address
upgrade to BoxV3...
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 BoxV3 address(should be the same)
0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 getImplementationAddress
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 getAdminAddress
✨ Done in 3.52s.
```
STEP 2: play with V3 in hardhat console
```
yarn hardhat console --network localhost
```
``` js
address = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
boxv3 = await ethers.getContractAt("BoxV3", address)
await boxv3.retrieve()
//BigNumber { value: "42" }
await boxv3.setName("mybox")
// tx response
await boxv3.name()
//'mybox'
```
STEP 3: try to play with the implementation contract directly again
``` js
addressimp = '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853'
boximp = await ethers.getContractAt("BoxV3", addressimp)
await boximp.retrieve()
//BigNumber { value: "0" }
await boximp.name()
//''
```
You can find explanations about data change in openzeppelin docs[openzeppelin docs](https://docs.openzeppelin.com/learn/upgrading-smart-contracts#upgrading):
> Due to technical limitations, when you upgrade a contract to a new version you cannot change the storage layout of that contract.
> This means that, if you have already declared a state variable in your contract, you cannot remove it, change its type, or declare another variable before it. In our Box example, it means that we can only add new state variables after value.
---

## Task 6: Write BoxV4 with prepareUpgrade script
We write BoxV4 and prepare upgrade script for Task 7 in which we will deploy and upgrade these versions of Box to public testnet Ropsten manually call `ProxyAdmin.upgrade()`.
Let's write `BoxV4.sol`, unit test and upgrade script.
### Task 6.1: write BoxV4.sol
We write a BoxV4 which change how we interact with `name` state variable:
- We change the state variable `name` from `public` to `private`.
- When user `getName()`, a prefix is added.
``` js
// 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));
}
}
```
### Task 6.2 write test for proxy deployment
Edit `test/6.BoxProxyV4.test.ts`:
``` js
// test/6.BoxProxyV4.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"
describe("Box (proxy) V4 with getName", function () {
let box:Contract
let boxV2:Contract
let boxV3:Contract
let boxV4:Contract
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box")
const BoxV2 = await ethers.getContractFactory("BoxV2")
const BoxV3 = await ethers.getContractFactory("BoxV3")
const BoxV4 = await ethers.getContractFactory("BoxV4")
//initialize with 42
box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
boxV2 = await upgrades.upgradeProxy(box.address, BoxV2)
boxV3 = await upgrades.upgradeProxy(box.address, BoxV3)
boxV4 = await upgrades.upgradeProxy(box.address, BoxV4)
})
it("should retrieve value previously stored and increment correctly", async function () {
expect(await boxV4.retrieve()).to.equal(BigNumber.from('42'))
await boxV4.increment()
expect(await boxV4.retrieve()).to.equal(BigNumber.from('43'))
await boxV2.store(100)
expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
})
it("should setName and getName correctly in V4", async function () {
//name() removed, getName() now
// expect(boxV4).to.not.have.own.property("name")
expect(boxV4.name).to.be.undefined
expect(await boxV4.getName()).to.equal("Name: ")
const boxname="my Box V4"
await boxV4.setName(boxname)
expect(await boxV4.getName()).to.equal("Name: "+boxname)
})
})
```
Run Test:
```
yarn hardhat test test/6.BoxProxyV4.test.ts
```
Results:
```
Box (proxy) V4 with getName
✓ should retrieve value previously stored and increment correctly
✓ should setName and getName correctly in V4
2 passing (771ms)
✨ Done in 2.46s.
```
### Task 6.3: write script to prepare upgrade
We will write this script using `upgrades.prepareUpgrade`.
When calling `upgrades.upgradeProxy()`, two jobs are done:
- an implementation contract is deployed
- ProxyAdmin `upgrade()` is called to link Proxy and implementation contract.
When calling `upgrades.prepareUpgrade()`, only the first job is done, and the second is left for developers to do manually.
Edit `scripts/4.prepareV4.ts`
``` js
// scripts/4.prepareV4.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";
const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
// const proxyAddress = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
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
})
```
Run deployment:
```
yarn hardhat run scripts/4.prepareV4.ts --network localhost
```
Results:
```
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 original Box(proxy) address
Preparing upgrade to BoxV4...
0x610178dA211FEF7D417bC0e6FeD39F05609AD788 BoxV4 implementation contract address
✨ Done in 3.90s.
```
You can interact with proxy contract (`0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0`) in hardhat console by running `yarn hardhat console --network localhost`
``` js
address = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
boxv4 = await ethers.getContractAt("BoxV4", address)
await boxv4.getName()
//We will get error:
//ProviderError: Error: Transaction reverted: function selector was not recognized
```
---

## Task 7: Deploy Box to public testnet Ropsten
### Task 7.1: preparations to deploy to Ropsten
We can deploy smart contract to Ropsten directly using Hardhat.
STEP 1: Edit Alchemy/Infura endpoint in `.env`
```
ROPSTEN_URL=https://eth-ropsten.alchemyapi.io/v2/{YOURS}
PRIVATE_KEY={YOURS HERE}
```
STEP 2: Make sure you have Ropsten set in `hardhat.config.ts`:
``` json
networks: {
ropsten: {
url: process.env.ROPSTEN_URL || "",
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
```
### Task 7.2: deploy Box V1
Run deploy script:
```
yarn hardhat run scripts/1.deploy_box.ts --network ropsten
```

Three contracts are deployed:
- `0x7fcb5f0898ee1394c6cb44e3a62b9e9fc19d0e1c`, implementation contract
- `0x9ce0fdc88df321c804ed0ad9cefe87d97a30479e`, ProxyAmin contract
- `0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268`, proxy contract
We can interact with the proxy contract from hardhat console by running `yarn hardhat console --network ropsten`
In console:
``` js
address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("Box", address)
await box.retrieve()
//BigNumber { value: "43" }
```
### Task 7.3: upgrade to Box V2
Run our upgrade script:
```
yarn hardhat run scripts/2.upgradeV2.ts --network ropsten
```
The script does two jobs:
- deploy new implementation contract
- call ProxyAmdin.upgrade()

Let's interact with the Box(proxy) in console:
``` js
address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("BoxV2", address)
await box.increment()
//wait for tx to be mined in a new block...
await box.retrieve()
//BigNumber { value: "44" }
```
### Task 7.4: upgrade to Box V3
Run our upgrade script:
```
yarn hardhat run scripts/3.upgradeV3.ts --network ropsten
```
Let's interact with the Box(proxy) with BoxV3 in console:
``` js
address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("BoxV3", address)
box = await ethers.getContractAt("BoxV3", address)
await box.setName("mybox")
//wait for tx to be mined in a new block...
```

### Task 7.5: prepare upgrade to Box V4
Run the prepare upgrade script:
```
yarn hardhat run scripts/4.prepareV4.ts --network ropsten
```
This script will deploy a BoxV4 contract at `0xA0726E6e045f84dEe8D7cA4CdD427A68dd336458`.
We will upgrade in block explorer `https://ropsten.etherscan.io` manually.
ProxyAdmin is already verified on Etherscan block explorer. In ProxyAmdin contract write page, connect to wallet and run `upgrade()` like in the screenshot.

``` js
address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("BoxV4", address)
box = await ethers.getContractAt("BoxV4", address)
await box.getName()
//'Name: mynewbox'
```
### Task 7.6: read ProxyAdmin contract
You can also read ProxyAdmin contract to get more information about the Box(proxy) contract:
- getProxyAdmin
- getProxyImplementation
- owner

If you would like to verify Box, BoxV2, BoxV3, BoxV4 contract in Etherscan, you can use hardhat plugin `hardhat-etherscan`.
Install plugin:
```
yarn add @nomiclabs/hardhat-etherscan
```
Verify BoxV4:
```
yarn hardhat verify 0xA0726E6e045f84dEe8D7cA4CdD427A68dd336458 --network ropsten
```
---
You have built and deploy proxy contract in local testnet and public testnet Ropsten in 7 tasks. You can continue to do more:
- Use OpenZeppelin Defender to manage upgrading.
- Transfer ProxyAdmin owner to a multi-sig address such as Gnosis Safe multi-sig wallet.
---
References you may be interested in:
- https://docs.openzeppelin.com/learn/upgrading-smart-contracts#upgrading
- https://docs.openzeppelin.com/defender/guide-upgrades
- https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies
- https://www.preethikasireddy.com/post/the-hardest-concept-for-developers-to-grasp-about-web-3-0
- https://forum.openzeppelin.com/t/openzeppelin-upgrades-step-by-step-tutorial-for-hardhat/3580
- https://blog.openzeppelin.com/proxy-patterns/
- https://blog.trailofbits.com/2018/09/05/contract-upgrade-anti-patterns/
- https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades/
---
### Tutorial List:
#### 1. A Concise Hardhat Tutorial(3 parts)
https://dev.to/yakult/a-concise-hardhat-tutorial-part-1-7eo
#### 2. Understanding Blockchain with `Ethers.js`(5 parts)
https://dev.to/yakult/01-understanding-blockchain-with-ethersjs-4-tasks-of-basics-and-transfer-5d17
#### 3. Tutorial : build your first DAPP with Remix and Etherscan (7 Tasks)
https://dev.to/yakult/tutorial-build-your-first-dapp-with-remix-and-etherscan-52kf
#### 4. Tutorial: build DApp with Hardhat, React and Ethers.js (6 Tasks)
https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
#### 5. Tutorial: build DAPP with Web3-React and SWR
https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
#### 6. Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin(7 Tasks)
https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916
#### 7. Tutorial: Build a NFT marketplace DApp like Opensea(5 Tasks)
https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9
---
If you find this tutorial helpful, follow me at Twitter [@fjun99](https://twitter.com/fjun99)


