在本文中,通过 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... 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!