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

在本文中,通过 7 个任务,如何来编写可升级合约,测试以及自动、活动实施升级。 在 7 个任务中,分别介绍了可升级合约可能遇到的各种情况: 在新实现合约中添加函数、添加状态变量、修改状态变量可见性(修改函数)。

有几篇关于可升级智能合约或代理模式的好文章。但找不到一个分步骤的指南来构建、部署代理合约并与之交互。

在本教程中,列出了7个细节任务,你可以按照这些任务将代理合约部署到本地测试网和公共测试网Ropsten。每个任务都有几个子任务。我们将在这里使用OpenZeppelin代理合约OpenZeppelin Upgrades plugin for Hardhat(或Truffle)。

特别感谢OpenZeppelin生态系统中的两个相关指南: OpenZeppelin Upgrades教程: 使用Hardhat和 Gnosis Safe 进行合约升级通过多签和 Defender 升级合约.

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

代理合约如何工作

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

可升级智能合约实际上有3个合约:

  • 代理合约:用户与之交互的智能合约。它保持数据/状态,这意味着数据被存储在这个代理合约账户的背景下,它是一个EIP1967标准代理合约。
  • 实现合约:智能合约提供功能和逻辑。请注意,数据也是在这个合约中定义的。这是我们编写的智能合约。
  • ProxyAdmin合约:该合约连接了Proxy和Implementation。

ProxyAmdin在OpenZeppelin 文档中解释:

什么是ProxyAmdin?

ProxyAdmin是一个合约,它作为所有代理合约的所有者(Owner)。每个网络通过部署一个ProxyAmdin就可以。当你启动项目时,ProxyAdmin由部署者地址拥有,但你可以通过调用transferOwnership来转移它的所有权。

当把ProxyAmin的所有权转移到一个多重签名的账户时,升级Proxy合约(将代理链接到新的实现)的权力也会转移到多重签名身上。

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

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

  1. 部署 实现合约
  2. 部署 ProxyAdmin合约
  3. 部署 代理合约

在ProxyAdmin合约中,实现和代理被关联起来。

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

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

  1. 部署一个新的 实现合约
  2. ProxyAdmin合约中升级,将所有对代理的调用重定向到新的实现合约。

Hardhat/Truffle的OpenZeppelin Upgrades插件可以帮助我们完成升级工作。

如果你想知道如何修改合约使其可升级,你可以参考OpenZeppelin的文档, 见链接

让我们开始编写和部署一个可升级的智能合约,你可以在文末找到本文网站的代码。

任务1: 编写可升级的智能合约

任务1.1:启动Hardhat项目

我们将使用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()

1.2: 编写一个可升级的智能合约

我们使用OpenZeppelin学习指南中的Box.sol合约。我们将建立这个合约的几个版本。

  • Box.sol
  • BoxV2.sol
  • BoxV3.sol
  • BoxV4.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()获取它。

任务 1.3: 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.

任务 2: 部署可升级的智能合约

任务2.1:用 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
  • 三个合约将被部署: 实现合约、ProxyAdmin、Proxy。我们记录他们的地址。

任务2.2:将合约部署到本地测试网

让我们在本地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

任务2.3:测试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()函数。与其重新部署这个合约,不如将数据迁移到新的合约,并要求所有用户访问新的合约地址。我们可以很容易地升级合约。

任务3:将智能合约升级到BoxV2

任务3.1:编写新的实现

我们通过继承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);
    }
}

任务3.2:正常部署的测试脚本

我们编写单元测试脚本来测试本地部署的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.

任务3.3:为升级部署准备测试脚本

我们在部署代理服务模式下,为BoxV2编写单元测试脚本:

  • 首先,我们部署Box.sol
  • 然后我们将其升级到BoxV2.sol
  • 测试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.

任务3.4:编写升级脚本

在子任务2.2中,我们将Box(proxy)部署到0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0

在这个子任务中,我们将把它升级到BoxV2(部署一个新的合约,并在ProxyAdmin中把代理链接到新的实现合约)。

编辑 scripts/2.upgradeV2.ts


//  sc...

剩余50%的内容订阅专栏后可查看

1 条评论

请先 登录 后评论
Tiny熊
Tiny熊

布道者

150 篇文章, 278695 学分