如何使用Hardhat+UUPS代理模式升级和验证你的智能合约?

如何使用Hardhat+UUPS代理模式升级和验证你的智能合约?

目录

[TOC]

1、前言
2、代理升级的基本思想
3、什么是EIP-1967
4、什么是EIP-1822
5、代理模式的类型
6、使用 Hardhat 部署 ERC721MSHKUUPSToken 合约
6.1、基础环境准备
6.2、创建 ERC721MSHKUUPSToken 合约
6.3、升级合约 ERC721MSHKUUPSToke
7、参考文章

1、前言

本文适用于以太坊的区块链,同时也适用于币安智能链 (BSC) 网络。

  按照设计,智能合约是不可变的。另一方面,软件质量很大程度上取决于升级和修补源代码以生成迭代版本的能力。尽管基于区块链的软件从技术的不变性中获益匪浅,但仍然需要一定程度的可变性来修复错误和潜在的产品改进。

  以太坊的最大优势之一是每笔资金转移交易、每份合约部署以及对合约进行的每笔交易,在我们称为区块链的公共账本上都是不可变的。没有办法隐藏或修改曾经进行的任何交易。更大的好处是以太坊网络上的任何节点都可以验证每笔交易的有效性和状态,使以太坊成为一个非常强大的去中心化系统。

  但最大的缺点是智能合约部署后无法更改其源代码。开发集中式应用程序的开发人员习惯于频繁更新以修复错误或引入新功能。这在以太坊上用传统模式是不可能做到的。

  还记得臭名昭著的 Parity Wallet Multisig 黑客攻击 150,000 ETH 被盗吗?在攻击期间,Parity 多重签名钱包合约中的一个漏洞被利用,知名钱包的资金被耗尽。唯一可以做的和解是尝试比黑客更快,并利用相同的漏洞来破解剩余的钱包,以便在攻击后将 ETH 重新分配给其合法所有者。

  如果在智能合约部署后有办法更新源代码就好了……

  尽管无法升级已部署的智能合约的代码,但可以设置代理合约架构,允许您使用新部署的合约,就像您的主要逻辑已升级一样。

  代理架构模式是这样的,所有消息调用都通过代理合约,该合约会将它们重定向到最新部署的合约逻辑。要升级,需要部署新版本的合约,并更新代理以引用新的合约地址。

  可升级的智能合约是通过代理 proxy contract(代理合约) 来实现的,由代理合约呼叫执行合约,如果需要升级,只需要替换代理合约中的地址为新合约即可。执行方式是通过 delegateCall 更改目标合约的状态。

2、代理升级的基本思想

  基本思想是使用代理进行升级。第一个合约是一个简单的包装器或代理,用户直接与之交互并负责将交易转发到包含逻辑的第二个合约。要理解的关键概念是,可以在代理或访问点永远不会更改的情况下替换逻辑合约。从代码无法更改的意义上说,这两个合约仍然是不可变的,但逻辑合约可以简单地被另一个合约交换。包装器因此可以指向不同的逻辑实现,并且在这样做时,合约被升级。

用户 ---- tx ---> 代理 ----------> Implementation_v0
                     |
                      ------------> 实施_v1
                     |
                      ------------> 实施_v2

3、什么是EIP-1967

  ERC-1967 是一种标准化代理,这样做可以使外部服务(例如块浏览器)更容易向最终用户显示正确的信息。

  这个 EIP 标准化了逻辑合约地址的存储槽,而不是像EIP-897那样的代理合约上的公共方法。这样做的理由是代理不应该向最终用户公开可能与逻辑合约冲突的功能

4、什么是EIP-1822

  EIP1822 是一种通用可升级代理标准(UUPS),一个聪明的解决方案,不需要通用的存储智能合约来让编译器知道要使用哪些存储槽。

  因此,这种方法只是简单地使用伪随机存储槽来存储逻辑合约的地址。

5、代理模式的类型

  目前代理模式主要有三种模式:
* EIP2532
* 透明代理模式
* 通用可升级代理标准 (UUPS):EIP-1822

  OpenZeppelin 建议使用 UUPS 模式,因为它更 Gas

  本文中主要使用此方式进行演示,UUPS 代理使用 ERC1967Proxy. 请注意,此代理本身不可升级。除了合约的逻辑之外,实现的作用是包含更新存储在代理存储空间中特定插槽中的实现地址所需的所有代码。这就是 UUPSUpgradeable 合约的用武之地。从它继承(_authorizeUpgrade使用相关的访问控制机制覆盖该功能)会将您的合约变成符合 UUPS 的实现。

6、使用 Hardhat 部署 ERC721MSHKUUPSToken 合约

  我们将部署一个名为 ERC721MSHKUUPSToken 的简单智能合约,并使用 UUPS 代理模式将其升级到 ERC721MSHKUUPSTokenV2

6.1、基础环境准备

  接下来的内容,我们将使用 Hardhat 进行开发和部署。

如果不清楚什么是 Hardhat 以及如何安装,可以参考文章《如何使用ERC20代币实现买、卖功能并完成Dapp部署

  
  创建一个全新的目录:

$ mkdir hardhat-erc721-uups && cd hardhat-erc721-uups

  
  执行下面的命令,添加 package.json 文件

$ cat >package.json<< EOF
{
  "name": "hardhat-erc721-uups",
  "version": "1.0.0",
  "description": "hardhat-erc721-uups",
  "main": "index.js",
  "author": "lion.888@gmail.com",
  "license": "MIT",
  "scripts": {
    "lint": "solhint 'contracts/*.sol'",
    "lint:fix": "solhint 'contracts/**/*.sol' --fix",
    "format": "prettier --write .",
    "coverage": "hardhat coverage"
  },
  "devDependencies": {
    "@openzeppelin/contracts-upgradeable": "^4.6.0",
    "@openzeppelin/hardhat-upgrades": "^1.18.1",
    "chai": "^4.3.6",
    "dotenv": "^16.0.1",
    "hardhat": "^2.9.7",
    "hardhat-contract-sizer": "^2.5.1",
    "hardhat-deploy": "^0.11.10",
    "hardhat-gas-reporter": "^1.0.8",
    "solhint": "^3.3.7",
    "solidity-coverage": "^0.7.21"
  },
  "mocha": {
    "timeout": 10000000
  }
}
EOF

  接下来我们安装 Hardhatopenzeppelin 的依赖模块

# 安装依赖包
$ yarn install

# 使用 Hardhat 创建项目
$ yarn hardhat
yarn run v1.22.19
$ /Users/lion/my_project/_javascript/test-project/hardhat-erc721-uups/node_modules/.bin/hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.9.9 👷‍

✔ What do you want to do? · Create a basic sample project
✔ Hardhat project root: · < Your Project Path>
✔ Do you want to add a .gitignore? (Y/n) · y

You need to install these dependencies to run the sample project:
  yarn add --dev "hardhat@^2.9.9" "@nomiclabs/hardhat-waffle@^2.0.0" "ethereum-waffle@^3.0.0" "chai@^4.2.0" "@nomiclabs/hardhat-ethers@^2.0.0" "ethers@^5.0.0"

✨ Project created ✨
See the README.md file for some example tasks you can run.
✨  Done in 5.09s.

  
  创建完 Hardhat 项目的目录结构如下:

|____test
| |____sample-test.js
|____hardhat.config.js
|____contracts
| |____Greeter.sol
|____README.md
|____yarn.lock
|____.gitignore
|____package.json
|____scripts
| |____sample-script.js

  在根目录创建 .env 文件,内容如下:

BSCSCAN_API_KEY=< Your BSCSCAN API Key>
PRIVATE_KEY=< Your Account Private Key>

BSCSCAN_API_KEY,可以登录 https://bscscan.com/,注册一个帐号,填写你的 Key,用于连接 Binance 的主网和测试网络。如果部署到ETH主网,需要去 https://etherscan.io/ 申请

PRIVATE_KEY,部署钱包的私钥

  
  编辑根目录下的 hardhat.config.js,全部内容如下:

$ cat hardhat.config.js
require('@openzeppelin/hardhat-upgrades');
require("@nomiclabs/hardhat-etherscan");
require("hardhat-deploy");
require("solidity-coverage");
require("hardhat-gas-reporter");
require("dotenv").config();


// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

const REPORT_GAS = process.env.REPORT_GAS || false
const BSCTESTNET_RPC_URL = process.env.BSCTESTNET_RPC_URL || "https://data-seed-prebsc-1-s1.binance.org:8545"
const BSCSCAN_API_KEY = process.env.BSCSCAN_API_KEY || "Your etherscan API key"
const PRIVATE_KEY = process.env.PRIVATE_KEY || "0x"

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  // solidity: "0.8.4",
  solidity: {
    compilers: [{
        version: "0.8.7",
      },
    ],
  },
  defaultNetwork: "hardhat",
  networks: {
    hardhat: { 
      chainId: 1337,
    },
    bsctestnet: {
        url: BSCTESTNET_RPC_URL,
        accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
        saveDeployments: true,
        chainId: 97,
    },
  },
  etherscan: {
    // npx hardhat verify --network <NETWORK> <CONTRACT_ADDRESS> <CONSTRUCTOR_PARAMETERS>
    apiKey: {
      bscTestnet: BSCSCAN_API_KEY,
    },
  },
  gasReporter: {
    enabled: REPORT_GAS,
    currency: "USD",
    outputFile: "gas-report.txt",
    noColors: true,
    // coinmarketcap: process.env.COINMARKETCAP_API_KEY,
  },
  contractSizer: {
    runOnCompile: false,
    only: ["Raffle"],
  },
  namedAccounts: {
    deployer: {
      default: 0, // here this will by default take the first account as deployer
      1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
    },
    player: {
      default: 1,
    },
  },
  mocha: {
      timeout: 200000, // 200 seconds max for running tests
  },
};

  

6.2、创建 ERC721MSHKUUPSToken 合约

  在 contracts 目录中,创建 01_ERC721MSHKUUPSToken.sol 文件,内容如下:

$ cat contracts/01_ERC721MSHKUUPSToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

// Open Zeppelin libraries for controlling upgradability and access.
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";


// 制作一个可以升级的合约
contract ERC721MSHKUUPSToken is
    Initializable,
    ERC721Upgradeable,
    UUPSUpgradeable,
    OwnableUpgradeable
{
    uint256 public _tokenIdCounter;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    // 可升级的合约应该有一个initialize方法来代替构造函数,并且initializer关键字确保合约只被初始化一次
    function initialize() public initializer {

        __ERC721_init("MSHK ERC721 UUPS Token", "MSHKUUPS");

        ///@dev as there is no constructor, we need to initialise the OwnableUpgradeable explicitly
        __Ownable_init();

        __UUPSUpgradeable_init();

        // 向合约创建者发送 1000 个有18位小数的代币
        _mint(msg.sender, 1000 * 10 ** 18);
    }

    // 需要此方法来防止未经授权的升级,因为在 UUPS 模式中,升级是从实现合约完成的,而在透明代理模式中,升级是通过代理合约完成的
    function _authorizeUpgrade(address) internal override onlyOwner {}

    // 计数+1
    function Increment() external  {
        unchecked {
            _tokenIdCounter += 1;
        }
    }

}

  
  这是一个简单的基础 ERC721 合约,现在让我们来编译 01_ERC721MSHKUUPSToken.sol

$ npx hardhat compile

  
  在 scripts 目录创建 01_ERC721MSHKUUPSToken.js,编写部署脚本,内容如下:

$ cat scripts/01_ERC721MSHKUUPSToken.js
const {
    ethers,
    upgrades
  } = require("hardhat");


  async function main() {
    const ERC721MSHKUUPSToken = await ethers.getContractFactory("ERC721MSHKUUPSToken");

    console.log("Deploying ERC721MSHKUUPSToken...");

    const m = await upgrades.deployProxy(ERC721MSHKUUPSToken, {
      initializer: "initialize",  // 设置一个不同的初始化函数来调用
    });
    await m.deployed();

    console.log("ERC721MSHKUUPSToken deployed to:", m.address);
  }



  // We recommend this pattern to be able to use async/await everywhere
  // and properly handle errors.
  main()
    .then(() => process.exit(0))
    .catch((error) => {
      console.error(error);
      process.exit(1);
    });

  
  输入以下命令,将合约部署到 Binance-TestNet 网络:

$ npx hardhat run scripts/01_ERC721MSHKUUPSToken.js --network bsctestnet
npx hardhat run scripts/01_ERC721MSHKUUPSToken.js --network bsctestnet
Deploying ERC721MSHKUUPSToken...
ERC721MSHKUUPSToken deployed to: 0xd574379ba01f3b8bCB298a325D3973184b9E3d04

0xd574379ba01f3b8bCB298a325D3973184b9E3d04 是代理合约的地址

  打开 https://testnet.bscscan.com/ 搜索部署者的合约地址,可以看到上面的命令同时创建了2个合约,以我创建的为例,这两个合约的地址分别如下:
* 0x32fb29ca4b09a26bd6042d05f05901117f53e046ERC721MSHKUUPSToke 的合约地址
* 0xd574379ba01f3b8bCB298a325D3973184b9E3d04 是代理合约地址

  
  对 ERC721MSHKUUPSToke 进行在线验证

$ npx hardhat verify --network bsctestnet --contract contracts/01_ERC721MSHKUUPSToken.sol:ERC721MSHKUUPSToken 0xd574379ba01f3b8bCB298a325D3973184b9E3d04
Verifying implementation: 0x32Fb29cA4B09a26bd6042d05F05901117f53e046
Nothing to compile
Successfully submitted source code for contract
contracts/01_ERC721MSHKUUPSToken.sol:ERC721MSHKUUPSToken at 0x32Fb29cA4B09a26bd6042d05F05901117f53e046
for verification on the block explorer. Waiting for verification result...

Successfully verified contract ERC721MSHKUUPSToken on Etherscan.
https://testnet.bscscan.com/address/0x32Fb29cA4B09a26bd6042d05F05901117f53e046#code

  验证后,在 Binance-TestNet 网络上创建合约前后对比:

  如果您在 Binance-TestNet 网络中检查 ERC721MSHKUUPSToke 合约,则不会设置或初始化诸如 ownernamesymbol等值,因为在代理模式中,所有内容都在代理合约的上下文中存储和执行。

  因此,为了与 ERC721MSHKUUPSToke 合约进行交互,我们应该通过代理合约进行交互。首先我们需要通知 Binance-TestNet 网络部署的合约实际上是一个代理。

  在代理合约的 Contract 选项卡中,源代码部分上方(右侧)会有一个小的下拉列表。

  
  选择Is this a proxy? 下拉菜单中的选项,然后Verify,最后点击Save

  
  此时在代理合约的 Contract 选项卡中,可以看到 Read as ProxyWrite as Proxy 选项。

  
  现在您可以使用这些选项与 ERC721MSHKUUPSToke 合约进行交互!

6.3、升级合约 ERC721MSHKUUPSToke

  经过一段时间运行后,我们可能需要增加一些功能。例如,我们创建一个简单的函数让 _tokenIdCounter 做减法 和 返回当前合约版本的函数。
  
  在 contracts 目录中,创建 02_ERC721MSHKUUPSTokenUpgrade.sol 文件,内容如下:

$ cat contracts/02_ERC721MSHKUUPSTokenUpgrade.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;


import "./01_ERC721MSHKUUPSToken.sol";

// 对 ERC721MSHKUUPSToken 进行升级
contract ERC721MSHKUUPSTokenV2 is ERC721MSHKUUPSToken {

    ///@dev increments the slices when called
   function Decrement() external {
       uint256 value = _tokenIdCounter;
        require(value > 0, "Counter: decrement overflow");
        unchecked {
            _tokenIdCounter = value - 1;
        }
   }

   ///@dev returns the contract version
   function MSHKVersion() external pure returns (uint256) {
       return 2;
   }
}

这里的 ERC721MSHKUUPSTokenV2 合约,是继承自 ERC721MSHKUUPSToken 合约。因此 ERC721MSHKUUPSToken 的所有功能也会在 ERC721MSHKUUPSTokenV2 中出现。

  
  现在让我们来编译 02_ERC721MSHKUUPSTokenUpgrade.sol

$ npx hardhat compile

  
  在 scripts 目录创建 02_ERC721MSHKUUPSTokenUpgrade.js,编写部署脚本,用来负责升级已经部署的 ERC721MSHKUUPSToken, 内容如下:

$ cat scripts/02_ERC721MSHKUUPSTokenUpgrade.js
const {
  ethers,
  upgrades
} = require("hardhat");

const PROXY = '0xd574379ba01f3b8bCB298a325D3973184b9E3d04';//代理合约的地址

async function main() {
  const mV2 = await ethers.getContractFactory("ERC721MSHKUUPSTokenV2");
  console.log("Upgrading ERC721MSHKUUPSTokenV2...");
  var m = await upgrades.upgradeProxy(PROXY, mV2);
  console.log("ERC721MSHKUUPSTokenV2 upgraded successfully", m.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

  
  输入以下命令,将合约部署到 Binance-TestNet 网络:

$ npx hardhat run scripts/02_ERC721MSHKUUPSTokenUpgrade.js --network bsctestnet
Upgrading ERC721MSHKUUPSTokenV2...
ERC721MSHKUUPSTokenV2 upgraded successfully 0xd574379ba01f3b8bCB298a325D3973184b9E3d04

  
  此时查看 Binance-TestNet 网络,可以看到产生了两笔交易,第一个是 ERC721MSHKUUPSTokenUpgrade 合约的部署,第二个是调用代理合约的 Upgrade To 方法,确保代理合约的调用指向ERC721MSHKUUPSTokenUpgrade

  
  对 ERC721MSHKUUPSTokenV2 进行在线验证:

$ npx hardhat verify --network bsctestnet --contract contracts/02_ERC721MSHKUUPSTokenUpgrade.sol:ERC721MSHKUUPSTokenV2 0x0c304987fbd69ff8a0841787655decf389472e6a
Nothing to compile
Successfully submitted source code for contract
contracts/02_ERC721MSHKUUPSTokenUpgrade.sol:ERC721MSHKUUPSTokenV2 at 0x0c304987fbd69ff8a0841787655decf389472e6a
for verification on the block explorer. Waiting for verification result...

Successfully verified contract ERC721MSHKUUPSTokenV2 on Etherscan.
https://testnet.bscscan.com/address/0x0c304987fbd69ff8a0841787655decf389472e6a#code

  
  此时在 Binance-TestNet 网络中检查 代理合约Contract 选项卡,可以看到 Read as ProxyWrite as Proxy 选项,可以看到我们增加的 MSHKVersionDecrement 方法都出现了。

  至此,我们成功的使用 UUPS 代理模式对合约成功的进行了升级和部署
  
  本文中的源码:https://github.com/idoall/hardhat-erc721-uups

7、参考文章

proxy-patterns
EIP1967
EIP1822

  
  


博文作者:迦壹
博客地址:如何使用Hardhat+UUPS代理模式升级和验证你的智能合约?
转载声明:可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明,谢谢合作!


发表回复

您的电子邮箱地址不会被公开。