Solidity语言 - 如何使用 OpenZeppelin 和 Hardhat 创建和部署可升级的智能合约

  • QuickNode
  • 发布于 2024-08-25 18:47
  • 阅读 23

本文详细介绍了如何使用 OpenZeppelin 库和 Hardhat 框架创建和部署可升级智能合约。通过分步指南,读者了解了合同的创建、测试、部署及升级的过程,还涉及了环境设置、合约验证和资金管理等重要步骤,对于具备一定基础的开发者来说,非常实用。

概述

在区块链开发中,有一个严格的规则:任何已部署的智能合约都无法被修改。智能合约通常被称为“不可变”的,这确保了开发人员所交互的代码是防篡改和透明的。这种理念对与智能合约互动的用户是有益的,但对编写智能合约的开发者并不总是如此。编写智能合约的开发人员必须始终确保合约是全面的、无错的,并涵盖所有边界情况。这通常是这样,但并不总是,因此需要可升级智能合约。

使用可升级智能合约的方法,如果合约中存在错误、逻辑缺陷或缺失功能,开发人员可以选择升级该智能合约并部署一个新的合约来代替它。

在本教程中,我们将演示如何使用 OpenZeppelin 和 Hardhat 从头创建和部署一个可升级的智能合约。

我们将做什么

  • 使用 OpenZeppelin 的 Hardhat 插件创建可升级智能合约
  • 使用 Hardhat 在穆umba测试网编译和部署合约
  • 使用 Polygonscan API 验证合约
  • 升级合约并验证结果

你需要准备什么

  • NPM(Node 包管理器)和 Node.js(推荐版本 16.15)
  • 选择了 Polygon Mumbai 测试网的 MetaMask(你可以学习如何将网络添加到你的钱包 这里
  • 在 Mumbai 测试网上的 MATIC 代币(你可以在这个 水龙头 获取一些)
  • 具备 Solidity 的基础经验
  • 知道可升级智能合约的知识。你可以参考我们的 "可升级智能合约简介" 指南以了解更多关于可升级智能合约的理论。

设置开发环境

我们需要在本地创建一个新文件夹来存放本教程的项目。我们将其命名为 UpgradeableContracts,但你可以使用任何你喜欢的名称。在终端执行以下命令以创建文件夹并导航到其中:

太好了!现在我们有了一个空白的canvas来操作,让我们开始动手。请在终端中执行以下两个命令:

第一个命令 npm init -y 会在你目录中初始化一个空的 package.json 文件,而第二个命令则将 Hardhat 作为开发依赖安装,这样你就可以轻松地设置以太坊开发环境。

此时,你可以在你选择的代码编辑器中打开并查看文件夹。我们将使用 VScode,并将继续在嵌入式终端中运行我们的命令。随时可以使用你初始项目中原始终端窗口。

现在,在终端中运行以下命令以启动 Hardhat:

如果一切安装正确,你的终端将显示如下内容:

Hardhat 配置设置

恭喜你!你已成功安装并初始化了 Hardhat。

接下来,点击 创建基本示例项目,并按 Enter 键通过 Hardhat 提问的所有问题。这将选择默认设置,允许 Hardhat 在项目根目录中创建基本示例项目。此外,Hardhat 还会创建一个 .env 文件并安装示例项目的依赖(如 @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)。通常需要一段时间才能安装所有内容。

安装完成后,你现在应该拥有在区块链上开发、测试和部署智能合约所需的一切。由于我们将处理可升级的智能合约,因此我们需要再安装两个依赖。请在终端中执行以下命令:

@openzeppelin/hardhat-upgrades 是允许我们以可升级的方式部署智能合约的包。(我们稍后将在此讨论更多)。@nomiclabs/hardhat-etherscan 是一个 Hardhat 插件,允许我们验证区块链上的合约。这使得任何人都可以与你部署的合约进行交互,并提供透明性。使用 Hardhat 插件是验证合约的最方便方法。

如果你能够成功跟随本教程到这里,值得称赞。你刚刚使用 Hardhat 设置了智能合约开发环境并安装了额外的依赖,以使我们能够部署和验证可升级的智能合约。

访问 Polygon Mumbai 测试网节点

我们需要在 Polygon Mumbai 测试网上部署我们的合约。我们可以在 这里 注册一个免费的 QuickNode 账户,并创建一个以太坊端点。

QuickNode 端点页面

复制 HTTP URL,并将其粘贴到 .env 文件中的 RPC_URL 变量里。

接下来,登录到 PolygonScan 的你的个人资料,找到 API KEYS 选项卡。如果你还没有账户,请 这里 创建一个。在这里,你将创建一个 API 密钥,以帮助你验证区块链上的智能合约。复制 API 密钥并将其粘贴到 ETHERSCAN_API_KEY 变量中。

最后,进入你的 MetaMask,复制你一个账户的私钥。要学习如何访问你的私钥,请查看这个简短的 指南。将此私钥粘贴到 PRIVATE_KEY 变量中。

你还需要在你的账户里拥有一些 Mumbai 测试网的 MATIC 才能部署合约。你可以在这个 水龙头 获取一些。

最后,打开 hardhat.config 文件,将整个代码替换为以下内容:

require("@nomiclabs/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-etherscan");
require('dotenv').config()

module.exports = {
  solidity: "0.8.4",
  networks: {
    mumbai: {
      url: process.env.RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  }
};

前几行我们用来导入我们需要的几个库。hardhat-upgrades 包是允许我们调用部署可升级合约的函数的插件。

为了确认一切正常运行,请保存所有文件,并通过运行命令再次编译合约:

如果你正确遵循了所有步骤,Hardhat 将再次编译你的合约,并给出确认消息。我们现在准备好部署合约了。

创建我们的智能合约

在本节中,我们将创建两个基本的智能合约。我们将部署第一个智能合约,后来将其升级为第二个智能合约。

进入 contracts 文件夹,删除已有的 Greeter.sol 文件。那是 Hardhat 提供的默认智能合约模板,我们不需要它。现在在合约文件夹中创建一个名为 contractV1.sol 的新文件,并将以下代码粘贴到文件中:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

contract V1 {
   uint public number;

   function initialValue(uint _num) external {
       number=_num;
   }

   function increase() external {
       number += 1;
   }
}

这个合约相当简单。它有一个类型为无符号整数的状态变量和两个函数。函数 initialValue() 只是设置变量的初始值,而函数 increase() 将其值加 1。

在合约文件夹中再创建一个文件,并将其命名为 contractV2.sol。将以下代码粘贴到文件中:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

contract V2 {
   uint public number;

   function initialValue(uint _num) external {
       number=_num;
   }

   function increase() external {
       number += 1;
   }

   function decrease() external {
       number -= 1;
   }
}

在部署合约 V1 之后,我们将把它升级为合约 V2。在第二个合约中,我们只是添加了一个函数 decrease(),该函数将变量值减少 1。

保存你正在使用的文件,然后返回终端。确认你在项目目录中(例如,UpgradeableContracts),然后在终端中运行以下命令:

如果你做得正确,终端应该告诉你成功编译了两个 Solidity 文件。我们现在准备好配置我们的部署工具。下一节将教你部署合约时的最佳实践。

设置 HardHat 配置文件

当运行 Hardhat 时,它搜索最近的 hardhat.config 文件。这个文件包含了编译和部署我们代码的规范。认真处理这个文件是非常重要的。然而,在我们操作这个文件之前,我们需要安装最后一个包。

打开你的终端,并依次运行以下命令:

这将安装 dotenv 库,并在我们的 Hardhat 项目中设置一个 .env 文件,我们将用来存储敏感数据。打开 .env 文件,并粘贴以下内容:

我们将在接下来的部分填充这些空变量。

部署合约 V1

我们现在准备好部署我们的可升级智能合约!在 scripts 文件夹下,删除 sample-script.js 文件并创建一个名为 deployV1.js 的新文件。

在这个新文件中,粘贴以下代码:

const { ethers, upgrades } = require("hardhat");

async function main() {
   const gas = await ethers.provider.getGasPrice()
   const V1contract = await ethers.getContractFactory("V1");
   console.log("Deploying V1contract...");
   const v1contract = await upgrades.deployProxy(V1contract, [10], {
      gasPrice: gas,
      initializer: "initialvalue",
   });
   await v1contract.deployed();
   console.log("V1 Contract deployed to:", v1contract.address);
}

main().catch((error) => {
   console.error(error);
   process.exitCode = 1;
 });

让我们逐行详细讲解这段代码:

  • 第1行:我们首先从 Hardhat 导入相关的插件。
  • 第 3-5 行:然后我们创建一个函数来部署 V1 智能合约,并打印状态消息。请记住,传递给 getContractFactory() 函数的参数应为合约的名称,而不是其所在文件名。在我们的例子中,合约的名称为 V1,而其存储在文件 contractV1.sol 中。
  • 第 6-8 行:我们通过调用升级插件的 deployProxy 部署合约 V1。我们向 deployProxy 传递几个参数。首先是持有我们想要部署的合约的变量,然后是我们想要设置的值。初始化函数由 upgrades 提供,传递给它的任何函数只会在合约部署时执行一次。
  • 第 9-10 行:然后我们调用部署函数,并在终端打印已部署合约地址的状态消息。
  • 第 13-16 行:我们现在可以简单地调用我们的 main() 函数,它将运行我们的函数中的逻辑。

回头看看合约 V1,看看 initialValue 函数做了什么。10 是将被传递给 initialValue 函数的参数。因此,在部署后,变量的初始值将为 10。

现在,返回到你的项目根目录,并在终端中运行以下命令:

这是一条典型的 Hardhat 命令,用于运行脚本,并带有网络标志以确保我们的合约部署到穆umba测试网。这条命令将把你的智能合约部署到 Mumbai 测试网并返回一个地址。你的终端应显示如下内容:

部署 deployV1.sol 的终端输出

如果你返回了一个地址,那意味着部署成功。恭喜!你刚刚将智能合约部署到 Polygon Mumbai 测试网,使用了 Openzeppelin 的透明可升级代理。

你可能会想,幕后具体发生了什么。让我们稍微停一下,找出答案。

深入了解

先暂时忽略终端返回给我们的地址,我们会在一会儿再回到它。接下来,打开 MetaMask 并复制你用于部署智能合约的账户的公有地址。打开 Mumbai 测试网浏览器,并搜索你的账户地址。

你会看到你的账户不仅部署了一份智能合约,而是三份不同的合约。

PolygonScan 账户交易页面

要查看每一个单独合约,你可以在 “Transactions” 选项卡下的 “To” 字段中点击 合约创建 链接。

在三个不同的选项卡中打开这三个合约地址。那么,这里到底发生了什么呢?在你打开的这三个合约地址中,每个页面的合约标签下点击。你应该会看到类似下面的内容:

PolygonScan 合同代理选项卡

要检查你的合约是否被验证,你将在“合约”标签页上看到一个勾号logo,并且智能合约的源代码将可用。你会注意到所有合约(例如,ProxyAdmin、TransparentUpgradeableProxy 和 V1)如果你使用相同的代码,应该已经被验证。这是因为 PolygonScan 检测到了网络上已经存在的相同字节码,并自动为我们验证了合约,感谢 PolygonScan!

但是请注意,如果你在实现合约(例如,V1)中更改了任何代码,则需要先验证它,才能继续。要快速验证合约,请在终端中运行此命令:

npx hardhat verify --contract "contracts/contractV1.sol:V1" <insert V1 address> --network mumbai

如果你以不同的名称命名文件或合约,请相应修改该命令。通过该命令,我们指向要验证的合约的确切代码,并使用 hardhat-etherscan 包发送验证请求。

现在你的终端应该看起来像这样:

使用 Hardhat 和 Etherscan 验证 deployV1 合约

现在刷新你的实现合约(V1)网页,你应该也能看到绿色勾号。

每当你使用 deployProxy 函数部署智能合约时,OpenZeppelin 会为你再部署两个附加合约,分别为 TransparentUpgradeableProxy 和 ProxyAdmin。在这里,TransparentUpgradeableProxy 是主合约。这个合约保存了我们实现合约的所有状态变量变化。这意味着实现合约不维护自己的状态,而是依赖代理合约进行存储。

在这种情况下,代理合约(TransparentUpgradeableProxy)是我们实现合约(V1)的封装,如果我们需要升级我们的智能合约(通过 ProxyAdmin),我们只需再部署一个合约,并让我们的代理合约指向该合约,从而升级其状态和未来功能。多酷啊!

最终,我们实际上没有改变任何智能合约中的代码,但从用户的角度来看,主合约已经被升级。这个流程图将使你更好地理解:

可升级智能合约流程图

你可能还记得,当我们最初部署智能合约时终端返回给我们一个地址。如果你回到它,你会发现它实际上是我们 TransparentUpgradeableProxy 合约的地址。这是因为,目前,任何希望与我们实现合约交互的用户实际上都必须通过代理合约发送他们的调用。因此,使用特定地址是合乎逻辑的。

让我们再进行几个步骤,以更好地巩固这些概念。在实现合约(即名称为 V1 的合约)网页中,转到 Etherscan 的 读取合约 选项卡:

Etherscan 的读取选项卡为 deployV1.sol

正如你所看到的,我们唯一的状态变量的值为零。这是因为即使我们正确初始化了状态变量,变量的值并没有存储在实现合约中。只有代码存储在实现合约本身,而状态由 TransparentUpgradeableProxy 合约维护。

现在去 TransparentUpgradeableProxy 合约并尝试读取。你无法读取,尽管它被验证了。为什么呢?嗯,因为我们需要告诉区块浏览器该合约确实是一个代理,即使浏览器通常已经怀疑是这样。

在合约页面的代码选项卡下,单击 更多选项,然后单击 这是一个代理吗?

在这里你可以将合约验证为代理。然后,返回到原始页面。你现在应该在 TransparentUpgradeableProxy 的合约页面上看到几个额外的选项。点击 作为代理读取

在 Etherscan 上作为代理读取选项卡

瞧!你可以看到我们合约的状态变量值被存储为 10,这表明这是负责维护我们实现合约状态的智能合约。转到 作为代理写入 页面并调用 increase 功能。交易成功后,再次查看 number 的值。它增加了 1,意味着我们的函数成功调用了实现合约中的方法。

将合约 V1 升级为 V2

现在,我们已经对后端发生的事情有了扎实的理解,让我们回到代码并升级合约!在 scripts 文件夹下,创建一个名为 upgradeV1.js 的新文件。在里面粘贴以下代码:

const { ethers, upgrades } = require("hardhat");

const UPGRADEABLE_PROXY = "在这里插入你的代理合约地址";

async function main() {
   const gas = await ethers.provider.getGasPrice()
   const V2Contract = await ethers.getContractFactory("V2");
   console.log("Upgrading V1Contract...");
   let upgrade = await upgrades.upgradeProxy(UPGRADEABLE_PROXY, V2Contract, {
      gasPrice: gas
   });
   console.log("V1 Upgraded to V2");
   console.log("V2 Contract Deployed To:", upgrade.address)
}

main().catch((error) => {
   console.error(error);
   process.exitCode = 1;
 });

这个脚本与之前的脚本相比只改变了一处。在这里,我们不调用 deployProxy 函数。相反,我们调用 upgradeProxy 函数。这是因为我们的代理合约(例如,TransparentUpgradeableProxy)已经被部署,在这里我们只需部署一个新的实现合约(V2),并将其传递给代理合约。我们在这里不重新部署代理。

在升级合约之前,请记得将你的代理合约地址(例如,TransparentUpgradeableProxy 地址)粘贴到上面的 UPGRADEABLE_PROXY 变量中。

现在,让我们在终端中运行这个脚本:

npx hardhat run --network mumbai scripts/upgradeV1.js

简单来说,这里发生的事情是我们调用了代理管理员合约中的升级函数。请注意,只有部署代理合约的账户可以调用升级函数,这是显而易见的原因。这导致 TransparentUpgradeableProxy 代理合约现在指向新部署的 V2 合约的地址。查看下面的流程图:

可升级智能合约流程图

请注意,调用特定函数的用户的地址(msg.sender)在这里至关重要。该地址决定了整个逻辑流。

如果 msg.sender 是管理员以外的其他用户,则代理合约将简单地将调用转发给实现合约,相关函数将执行。因此,代理合约将代表 msg.sender 调用实现合约中的适当函数。正如之前所解释的,实现合约的状态是毫无意义的,因为它没有改变。改变的是代理合约的状态,这是基于当所需功能执行时从实现合约返回的内容。

这意味着,如果调用者不是管理员,代理合约甚至不会考虑执行任何类型的升级函数。如果调用者不是管理员,则该调用被转发或“委托”给实现合约,而没有进一步的延迟。这称为“委托调用”,这是一个重要的概念。如果调用者是管理员(在此情况下是我们的 ProxyAdmin 合约),则调用不是自动委托的,代理合约的任何函数都可以执行,包括升级函数。

现在到了最后的步骤。去你(Transparent)代理合约那里,尝试再次读取 ‘number’ 的值。你将无法做到这一点。这是因为代理现在指向一个新地址,而我们需要重新验证该合约为代理,以读取状态变量。

但为此,你需要提前验证合约 V2。在终端中运行此命令:

npx hardhat verify --contract "contracts/contractV2.sol:V2" <insert V2 address> --network mumbai

注意,你需要在上面的命令中插入 V2 合约地址。V2 地址在你运行 upgradeV1.js 脚本后已在终端中记录。

在你验证 V2 合约后,导航到 Mumbai 区块浏览器上的 TransparentUpgradeableProxy 合约,并在合约 - 作为代理写入选项卡下,你的屏幕应呈现如下:

Etherscan 读取选项卡为 deployV1.sol

正如你所看到的,代理合约现在指向我们刚刚部署的新实现合约(V2)。此外,我们现在也有了 decrease 函数。我们可以调用它并减少我们状态变量的值。

就这样。你刚刚部署了一个可升级的智能合约,然后将其升级以包含一个新功能。现在将代码推送到 Github,炫耀一下吧!最后一个警告,记得将 .env 文件名称列入你的 .gitignore。该文件的目的是防止我们的敏感数据被公开发布,从而危及我们在区块链上的资产。确认你在 .gitignore 文件中填列了 .env 文件之后,你就可以将代码推送到 GitHub,而不必担心,因为你在 hardhat.config 文件中没有私有数据。

结论

请为自己鼓掌。你值得!这是一个相当高级的教程,如果你仔细跟随,你现在应该了解如何使用 OpenZeppelin 库部署一个基本的可升级合约。

订阅我们的 新闻简报 以获取更多以太坊的文章和指南。如果你有任何反馈,请随时通过 Twitter 与我们联系。你可以随时在我们的 Discord 社区服务器上与我们聊天,那里面有一些你见过的开发者 😊

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。