Smart Contract Develop Tutorial

米霖

2024-10-19

准备Node.js 项目

设置 Node 项目指南

新兴的软件行业通常会共享相同的技术栈,以太坊生态系统也不例外。开发以太坊项目时,首选的语言是 JavaScript。许多以太坊相关的库,包括 OpenZeppelin 的软件,都是用 JavaScript 或其变体编写的。

传统上,JavaScript 代码通常运行在浏览器中,作为网站的一部分,但通过 Node.js,它也可以作为独立进程运行。

本指南将帮助你设置 Node.js 开发环境,便于使用 OpenZeppelin 的工具和其他第三方产品。如果你已经熟悉 Node、npm 和 Git,可以跳过本指南。

安装 Node.js

你可以通过多种方式在你的机器上安装 Node.js:可以使用包管理器安装,或者直接下载安装程序。

如果你使用的是 Windows,建议考虑使用 Windows Subsystem for Linux(WSL),因为很多以太坊生态系统的工具都是为 Linux 编写的。

安装完成后,在终端中运行 node --version 来检查安装情况。任何有效或维护版本的 Node.js 都应与大多数以太坊软件兼容。

$ node --version
v20.17.0

创建项目

JavaScript 软件通常打包成 npm 包,并通过 npm 注册表分发。一个 npm 包其实就是一个目录,里面包含一个 package.json 文件,描述了包的名称、版本、内容等信息。当你创建自己的项目时,即使不打算分发它,你也会创建一个 npm 包。

所有的 Node.js 安装都包含一个用于 npm 注册表的命令行客户端,你在开发项目时会使用它。要启动一个新项目,首先创建一个目录:

$ mkdir learn && cd learn

然后初始化项目:

$ npm init -y

就这么简单!项目中的 package.json 文件会随着项目的增长而更新,例如在安装依赖时通过 npm install 命令自动更新。

JavaScript 和 npm 是世界上最常用的软件工具之一,如果你遇到疑问,可以在线找到丰富的信息。

使用 npx

npm 注册表中的包有两种类型:库和可执行文件。已安装的库可以像其他 JavaScript 代码一样使用,而可执行文件则比较特殊。

安装 Node.js 时,还包括了一个名为 npx 的第三个可执行文件。npx 用于运行本地安装的项目中的可执行文件。

虽然 Hardhat 可以全局安装,但我们建议为每个项目本地安装,这样你可以为每个项目控制不同的版本。

为确保避免由于二进制文件不在系统路径中导致的错误,我们会在命令中显示完整的 npx 命令:

$ hardhat init
hardhat: command not found
$ npx hardhat init
👷 Welcome to Hardhat v2.22.12 👷‍
? What do you want to do? …

确保在运行 npx 时处于项目的目录中!否则,npx 会重新下载整个可执行文件来运行该命令,而这通常不是你想要的结果。

使用版本控制

在开始编写代码之前,应该为项目添加版本控制软件来跟踪更改。

最常用的工具是 Git,通常与 GitHub 配合使用以托管项目。你可以在 GitHub 上找到 OpenZeppelin 软件的全部源代码和历史记录。

如果你从未使用过 Git,可以从 Git 手册入手。

注意!不要将助记词、私钥或 API 密钥等秘密信息提交到版本控制中!请确保将包含这些秘密的文件添加到 .gitignore 中,以免泄露。

开发Smart Contract

关于 Solidity

本指南不会详细讲解语言的语法或关键词,如果你需要相关知识,可以参考以下资源:

  • 了解以太坊和智能合约:以太坊官网上有一个专门为初学者设计的“了解以太坊”部分。
  • Solidity 官方文档:对于 Solidity 语言的学习,特别是其中的安全建议,它是一个非常好的参考资源。
  • Consensys 最佳实践:提供了广泛的开发模式、注意事项以及需要避免的陷阱。
  • Ethernaut 游戏:通过一个基于网络的游戏,帮助开发者发现智能合约中的微妙漏洞,逐步提高技能。

设置项目

创建项目的第一步是安装一个开发工具。目前最流行的以太坊开发框架是 Hardhat 和 Foundry。它们各有优点,建议都熟悉。

本指南主要讲解如何使用 Hardhat 来开发、测试和部署智能合约,并结合 ethers.js

  1. 在项目目录中安装 Hardhat:
$ npm install --save-dev hardhat
  1. 安装完成后,运行以下命令创建 Hardhat 配置文件:
$ npx hardhat

编写第一个智能合约

将 Solidity 源文件(.sol)存储在 contracts 目录中,类似于其他语言中常用的 src 目录。

我们编写一个名为 Box 的简单智能合约,允许人们存储和检索一个值。代码如下:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Box {
    uint256 private _value;

    event ValueChanged(uint256 value);

    function store(uint256 value) public {
        _value = value;
        emit ValueChanged(value);
    }

    function retrieve() public view returns (uint256) {
        return _value;
    }
}

编译 Solidity 代码

以太坊虚拟机(EVM)不能直接执行 Solidity 代码,需要先将其编译为 EVM 字节码。

hardhat.config.js 文件中配置适当的 Solidity 编译器版本:

module.exports = {
  solidity: "0.8.24",
};

然后使用以下命令编译合约:

$ npx hardhat compile

编译完成后,会在项目目录中生成 artifacts 文件夹,存放编译好的字节码和元数据。

添加更多合约

随着项目的扩展,你会创建更多的合约并让它们相互交互。每个合约都应该存储在单独的 .sol 文件中。

例如,添加一个简单的访问控制系统,让只有被授权的用户可以使用 Box 合约。我们创建一个名为 Auth 的合约,用来存储管理员地址:

// contracts/access-control/Auth.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Auth {
    address private _administrator;

    constructor(address deployer) {
        _administrator = deployer;
    }

    function isAdministrator(address user) public view returns (bool) {
        return user == _administrator;
    }
}

然后在 Box 合约中导入并使用 Auth 合约:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./access-control/Auth.sol";

contract Box {
    uint256 private _value;
    Auth private _auth;

    event ValueChanged(uint256 value);

    constructor() {
        _auth = new Auth(msg.sender);
    }

    function store(uint256 value) public {
        require(_auth.isAdministrator(msg.sender), "Unauthorized");
        _value = value;
        emit ValueChanged(value);
    }

    function retrieve() public view returns (uint256) {
        return _value;
    }
}

使用 OpenZeppelin 合约

OpenZeppelin Contracts 提供了大量经过审计和验证的模块和库,可以用于构建智能合约。

例如,Ownable 合约标记部署者为合约的所有者,并提供一个 onlyOwner 修饰符,限制某些功能只能由所有者调用。通过继承该合约,可以避免重复实现这些功能。

首先,安装 OpenZeppelin 合约库:

$ npm install @openzeppelin/contracts

然后在 Box 合约中使用 OpenZeppelin 的 Ownable 合约来简化访问控制:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract Box is Ownable {
    uint256 private _value;

    event ValueChanged(uint256 value);

    function store(uint256 value) public onlyOwner {
        _value = value;
        emit ValueChanged(value);
    }

    function retrieve() public view returns (uint256) {
        return _value;
    }
}

部署和交互

设置本地区块链

在开始之前,我们需要一个可以部署合约的环境。以太坊主网(mainnet)要求使用以太币(Ether)支付费用,这使得它在测试新想法或工具时不是理想的选择。

为了解决这个问题,存在多个测试网络(testnets),例如 SepoliaHolesky 区块链。这些网络的工作方式与主网非常相似,唯一的区别是你可以免费获得这些网络上的以太币,因此使用它们不需要花费任何费用。然而,你仍然需要处理私钥管理、12秒以上的区块生成时间,以及获取免费以太币的问题。

在开发过程中,使用本地区块链会更为方便。它运行在你的本地机器上,不需要互联网连接,提供你所需的所有以太币,并能立即生成区块。由于这些原因,本地区块链非常适合进行自动化测试。

如果你想学习如何在公共区块链(例如以太坊测试网)上部署和使用合约,可以查看我们的连接到公共测试网络指南

Hardhat 自带一个本地区块链,即 Hardhat Network。启动时,Hardhat Network 会创建一组未锁定的账户并分配给它们以太币。

启动本地网络的命令如下:

$ npx hardhat node

Hardhat Network 会打印出它的地址 http://127.0.0.1:8545,并显示可用账户及其私钥。

请注意,每次运行 Hardhat Network 时都会创建一个全新的本地区块链——之前的运行状态不会被保留。对于短期实验,这没有问题,但你需要确保在这些指南期间保持 Hardhat Network 窗口的打开。

部署智能合约

Developing Smart Contracts 指南中,我们已经设置了开发环境。如果你还没有设置,请先创建并配置项目,然后创建并编译 Box 智能合约。

项目设置完成后,我们就可以部署合约了。这里我们将部署 Box 合约,确保你在 contracts/Box.sol 中有该合约的副本。

Hardhat 支持两种方式来部署合约:声明式部署和脚本部署。我们将编写一个脚本来部署 Box 合约,并将其保存为 scripts/deploy.js 文件。

// scripts/deploy.js
async function main () {
  // 获取要部署的合约
  const Box = await ethers.getContractFactory('Box');
  console.log('Deploying Box...');
  const box = await Box.deploy();
  await box.waitForDeployment();
  console.log('Box deployed to:', await box.getAddress());
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

我们在脚本中使用了 ethers.js,因此需要安装 ethers@nomicfoundation/hardhat-ethers 插件:

$ npm install --save-dev @nomicfoundation/hardhat-ethers ethers

接下来,在 hardhat.config.js 文件中添加对 hardhat-ethers 插件的引用:

require("@nomicfoundation/hardhat-ethers");

module.exports = {
  solidity: "0.8.24",
};

现在可以运行以下命令,将 Box 合约部署到本地网络(Hardhat Network):

$ npx hardhat run --network localhost scripts/deploy.js

你将会看到以下输出(示例地址):

Deploying Box...
Box deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

请注意,Hardhat 不会自动跟踪已部署的合约,因此我们在脚本中输出了部署的合约地址(如示例中的 0x5FbDB2315678afecb367f032d93F642f64180aa3)。你可以使用此地址在后续的编程交互中与合约进行交互。

本地区块链的部署速度非常快,但请记住,本地区块链不会保留状态。如果你关闭了本地区块链进程,你将需要重新部署合约。

从控制台交互智能合约

在我们部署了 Box 合约后,可以立即与其交互。我们将使用 Hardhat 控制台与本地网络上的已部署 Box 合约进行交互。

步骤 1:启动 Hardhat 控制台

首先,需要通过 Hardhat 控制台连接到我们本地网络上的 Box 合约。在控制台启动时,确保指定网络为 localhost。如果不指定网络,Hardhat 将默认使用一个新的临时网络,而 Box 合约不会部署到该网络。

$ npx hardhat console --network localhost

这将启动 Node.js 控制台。

步骤 2:获取合约工厂

我们可以使用 ethers.getContractFactory('Box') 获取合约工厂类。该工厂类代表了合约的抽象定义。

> const Box = await ethers.getContractFactory('Box');

步骤 3:附加到已部署的合约

要与已部署的合约进行交互,需要将其附加到相应的合约地址。将部署脚本中输出的 Box 合约地址传递给 Box.attach

> const box = Box.attach('0x5FbDB2315678afecb367f032d93F642f64180aa3');

这一步后,box 对象就表示我们部署的 Box 合约,可以开始与它交互了。

发送交易

Box 合约的 store 函数接收一个整数值,并将其存储在合约的存储中。因为这个函数会修改区块链的状态,因此需要发送一笔交易来执行它。

例如,将 42 作为值发送到合约:

> await box.store(42);

当交易发送成功后,控制台将返回交易的哈希值,类似于:

{
  hash: '0x3d86c5c2c8a9f31bedb5859efa22d2d39a5ea049255628727207bc2856cce0d3',
  ...
}

查询状态

Box 合约的 retrieve 函数返回存储的整数值。因为这是对区块链状态的查询,不需要发送交易,只需要调用该函数:

> await box.retrieve();

查询结果为 42n(JavaScript 中的 BigInt 类型)。可以将该数值转换为字符串进行显示:

> (await box.retrieve()).toString();
'42'

总结

通过 Hardhat 控制台,您可以轻松与部署的合约进行交互。发送交易会修改区块链的状态,而查询状态则只读取数据,且不需要花费以太币。

通过编程方式与智能合约交互

尽管使用控制台可以方便地进行原型设计和运行查询或交易,但在实际应用中,你最终需要通过自己的代码与合约交互。

本节将展示如何从 JavaScript 与已部署的合约交互,并使用 Hardhat 来运行脚本和配置。

设置

首先,我们在 scripts/index.js 文件中编写 JavaScript 代码,并包含一些基本的异步代码模板:

// scripts/index.js
async function main () {
  // 我们的代码将写在这里
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

测试与本地节点的连接,例如获取启用的账户列表:

// 从本地节点获取账户
const accounts = (await ethers.getSigners()).map(signer => signer.address);
console.log(accounts);

运行以下命令来执行脚本,确保获取到可用的账户:

$ npx hardhat run --network localhost ./scripts/index.js

输出类似于:

[
  '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
  ...
]

这些账户应该与启动本地区块链时显示的账户匹配。

获取合约实例

为了与部署的 Box 合约交互,我们将使用 ethers.js 的合约实例。合约实例是代表区块链上合约的 JavaScript 对象,可以用来与合约交互。

首先,提供已部署的合约地址并获取合约实例:

// 设置 ethers 合约实例,代表已部署的 Box 合约
const address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
const Box = await ethers.getContractFactory('Box');
const box = Box.attach(address);

注意替换示例地址为你实际部署合约时得到的地址。

调用合约

接下来,我们从合约中调用 retrieve 方法并打印出当前的存储值:

// 调用已部署 Box 合约的 retrieve() 方法
const value = await box.retrieve();
console.log('Box value is', value.toString());

运行脚本:

$ npx hardhat run --network localhost ./scripts/index.js

输出结果应为:

Box value is 42

如果你在运行期间重启了本地区块链,可能会出现地址不匹配的情况,此时需要重新部署合约。

发送交易

现在我们将发送一笔交易来存储新值 23,并使用之前的代码检查更新后的值:

// 发送交易存储新值到 Box
await box.store(23);

// 调用 retrieve() 方法
const value = await box.retrieve();
console.log('Box value is', value.toString());

运行脚本来验证 Box 的值已更新:

$ npx hardhat run --network localhost ./scripts/index.js

输出结果应为:

Box value is 23

总结

通过编程方式,你可以方便地与已部署的智能合约交互。使用 ethers.js 提供的合约实例对象,你可以读取和修改合约中的数据。

编写自动化智能合约测试

在区块链环境中,一个小错误可能导致所有资金损失,甚至更糟,影响用户的资金。通过编写自动化测试,你可以确保应用程序按照预期运行。本指南将帮助你编写测试来验证你的合约行为。

我们将涵盖以下内容:

关于测试

测试有多种方式,从简单的手动验证到复杂的端到端设置,每种方式都有其用途。对于智能合约开发,实践证明,单元测试非常有效。单元测试简单易写,执行迅速,能够自信地添加功能和修复 bug。

智能合约的单元测试通常由多个小的、集中的测试组成,每个测试验证合约的某个部分是否正确。例如,“管理员可以暂停合约”、“转移代币时会触发事件”或”非管理员不能铸造新代币”等。

设置测试环境

由于智能合约在区块链中执行,使用实际的以太坊网络进行测试非常昂贵,而测试网络虽然免费,但速度较慢。因此,我们将使用本地区块链,它不需要以太币,且能立即生成区块,非常适合自动化测试。

我们将使用 Hardhat 的 Chai 断言库 进行单元测试,并通过安装 Hardhat Toolbox 来实现:

$ npm install --save-dev @nomicfoundation/hardhat-toolbox

我们将测试文件放在 test 目录中,并与 contracts 目录保持对应结构。每个合约对应一个 .sol 文件和一个测试文件。

编写单元测试

现在,我们编写第一个测试,测试简单的 Box 合约,该合约允许检索之前存储的值。

在项目根目录下创建 test 文件夹,将以下测试代码保存为 test/Box.test.js

// test/Box.test.js
const { expect } = require('chai');

describe('Box', function () {
  before(async function () {
    this.Box = await ethers.getContractFactory('Box');
  });

  beforeEach(async function () {
    this.box = await this.Box.deploy();
    await this.box.waitForDeployment();
  });

  it('retrieve returns a value previously stored', async function () {
    await this.box.store(42);
    expect((await this.box.retrieve()).toString()).to.equal('42');
  });
});

运行测试

通过运行以下命令来执行 test 目录中的所有测试,确保你的合约按预期工作:

$ npx hardhat test

输出结果应为:

Box
   retrieve returns a value previously stored

进行复杂断言

有时你可能需要捕捉智能合约中的更复杂的属性,例如:

  • 验证合约在错误时是否回滚
  • 测量账户的以太币余额变化
  • 检查是否触发了正确的事件

OpenZeppelin Test Helpers 是一个专门用于测试这些属性的库。它还可以简化模拟区块链时间的流逝以及处理大数值的任务。

安装 Test Helpers:

$ npm install --save-dev @openzeppelin/test-helpers

更新测试以使用 Test Helpers

我们可以更新之前的测试,使用 Test Helpers 来处理大数值、检查事件是否被触发,以及验证交易是否回滚:

// test/Box.test.js
const { expect } = require('chai');
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');

const Box = artifacts.require('Box');

contract('Box', function ([ owner, other ]) {
  const value = new BN('42');

  beforeEach(async function () {
    this.box = await Box.new({ from: owner });
  });

  it('retrieve returns a value previously stored', async function () {
    await this.box.store(value, { from: owner });
    expect(await this.box.retrieve()).to.be.bignumber.equal(value);
  });

  it('store emits an event', async function () {
    const receipt = await this.box.store(value, { from: owner });
    expectEvent(receipt, 'ValueChanged', { value: value });
  });

  it('non owner cannot store a value', async function () {
    await expectRevert(
      this.box.store(value, { from: other }),
      'Ownable: caller is not the owner',
    );
  });
});

这些测试会验证 Ownable 合约的相关属性:管理员可以存储值、触发事件、非管理员无法存储值。

再次运行测试:

$ npx hardhat test

输出结果应为:

Contract: Box
   retrieve returns a value previously stored
   store emits an event
   non owner cannot store a value

结论

自动化测试是保障智能合约安全性的重要手段。通过使用 Chai 断言库和 OpenZeppelin Test Helpers,可以编写高效且强大的测试,确保合约在多种场景下都能按预期运行。

连接到公共测试网络

在你编写并本地测试了智能合约之后,接下来的一步是将它们部署到公共测试环境,供你和你的测试用户使用。公共测试网络(testnets)非常类似于以太坊主网,不同的是测试网络上的以太币(Ether)是免费的,没有实际价值,非常适合零成本地测试智能合约。

本指南将带你了解如何在测试网络上部署和与合约交互,包括:

可用的测试网络

测试网络是开发者在不消耗真实以太币的情况下测试应用的地方。推荐的测试网络是 Sepolia(网络ID = 11155111)。你可以选择其他测试网络,但 Sepolia 是目前的首选。

连接项目到公共网络

要将项目连接到公共测试网络,步骤如下:

  1. 获取测试网络节点:你可以运行自己的以太坊节点,或使用公共节点服务如 AlchemyInfura 提供的公共节点。
  2. 创建一个新账户:用于发送交易。
  3. 更新配置文件:配置测试网络的连接信息。
  4. 为测试账户提供资金:获取免费的测试以太币。

获取测试网络节点

我们可以使用公共节点服务,如 AlchemyInfura。这些服务允许开发者访问测试网络和主网节点。本指南使用 Alchemy,你需要在 Alchemy 注册并获取一个 API 密钥,用于连接网络。

创建新账户

使用 mnemonics 包可以生成一个新的以太坊账户助记词:

$ npx mnemonics
drama film snack motion ...

记住助记词,不要将其提交到版本控制系统中。

配置网络

我们将使用 Sepolia 测试网络,首先更新 hardhat.config.js 文件:

// hardhat.config.js
const { alchemyApiKey, mnemonic } = require('./secrets.json');

module.exports = {
  networks: {
    sepolia: {
      url: `https://eth-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
      accounts: { mnemonic: mnemonic },
    },
  },
};

确保将 API 密钥和助记词保存在 secrets.json 文件中,并将该文件添加到 .gitignore 中以保护机密信息。

{
  "mnemonic": "drama film snack motion ...",
  "alchemyApiKey": "JPV2..."
}

测试配置

通过以下命令检查配置是否正确,并查看 Sepolia 网络上的账户:

$ npx hardhat console --network sepolia
> accounts = (await ethers.getSigners()).map(signer => signer.address)

你还可以查询账户余额:

> (await ethers.provider.getBalance(accounts[0])).toString();

为测试账户提供资金

要发送交易,你需要一些测试以太币。你可以访问 Alchemy 的免费 Sepolia 测试网络水龙头(faucet)来获取测试以太币。

部署合约到测试网络

现在我们可以将 Box 合约部署到 Sepolia 测试网络,命令与本地部署相同,但会耗时几秒钟,因为需要等待新的区块生成。

$ npx hardhat run --network sepolia scripts/deploy.js

部署成功后,你可以在 Sepolia 的区块浏览器上查看你的合约,例如 sepolia.etherscan.io

与测试网络上的合约交互

你可以使用 Hardhat 控制台 或通过编程方式与部署的合约交互。例如,在控制台中调用 Box 合约的 storeretrieve 方法:

$ npx hardhat console --network sepolia
> const Box = await ethers.getContractFactory('Box');
> const box = await Box.attach('0x1b99CCaCea0e4046db618770dEF72180F8138641');
> await box.store(42);
> (await box.retrieve()).toString();
'42'

请记住,每次交易都会消耗少量的 gas,因此你可能需要定期为你的账户充值测试以太币。

总结

通过将你的项目连接到公共测试网络,你可以在不产生成本的情况下,测试智能合约在接近真实网络的环境中的表现。使用像 Alchemy 或 Infura 这样的公共节点服务,并确保账户有足够的测试以太币来执行交易。

升级智能合约

通过 OpenZeppelin 的升级插件部署的智能合约可以在保持其地址、状态和余额的情况下升级代码。这使得开发者可以逐步添加新功能,或修复生产环境中的 bug。

本指南内容:

  • 为什么升级重要
  • 使用升级插件升级 Box 合约
  • 学习升级的内部工作原理
  • 编写可升级合约

为什么需要升级

在以太坊中,智能合约一旦部署就不可更改。然而,在某些情况下,能够修改合约是有利的。例如,修复 bug 或添加新功能。对于不可升级的合约,要修复 bug 需要:

  1. 部署新版本的合约。
  2. 手动迁移状态(这非常耗费 gas)。
  3. 更新所有与旧合约交互的合约。
  4. 通知用户切换到新合约。

为避免这些繁琐步骤,OpenZeppelin 插件支持合约的代码升级,同时保持状态和地址不变。

使用升级插件升级合约

使用 deployProxy 部署的合约实例可以升级。只有最初部署合约的地址有权限进行升级。

deployProxy 执行以下操作: 1. 部署实现合约(如 Box 合约)。 2. 部署代理合约并运行初始化函数。 3. 自动部署 ProxyAdmin 合约来管理代理。

示例:部署可升级的 Box 合约

  1. 安装插件:
npm install --save-dev @openzeppelin/hardhat-upgrades
  1. 配置 Hardhat 使用升级插件:
// hardhat.config.js
require('@nomicfoundation/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');
  1. 编写部署脚本 scripts/deploy_upgradeable_box.js
const { ethers, upgrades } = require('hardhat');

async function main() {
  const Box = await ethers.getContractFactory('Box');
  console.log('Deploying Box...');
  const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
  await box.waitForDeployment();
  console.log('Box deployed to:', await box.getAddress());
}

main();
  1. 部署合约:
$ npx hardhat run --network localhost scripts/deploy_upgradeable_box.js
  1. 使用控制台与合约交互:
$ npx hardhat console --network localhost
> const Box = await ethers.getContractFactory('Box');
> const box = await Box.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');
> (await box.retrieve()).toString();
'42'

升级合约:添加新功能

创建新版本的合约 BoxV2,增加 increment 函数:

// contracts/BoxV2.sol
pragma solidity ^0.8.0;

contract BoxV2 {
    uint256 private _value;

    // Increments the stored value by 1
    function increment() public {
        _value = _value + 1;
    }
}

编写升级脚本 scripts/upgrade_box.js

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

async function main () {
  const BoxV2 = await ethers.getContractFactory('BoxV2');
  console.log('Upgrading Box...');
  await upgrades.upgradeProxy('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', BoxV2);
  console.log('Box upgraded');
}

main();

升级合约:

$ npx hardhat run --network localhost scripts/upgrade_box.js

再次使用控制台交互:

$ npx hardhat console --network localhost
> const BoxV2 = await ethers.getContractFactory('BoxV2');
> const box = await BoxV2.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');
> await box.increment();
> (await box.retrieve()).toString();
'43'

升级机制的工作原理

OpenZeppelin 的升级插件部署三个合约: 1. 实现合约:你编写的逻辑合约。 2. 代理合约:与用户交互并将调用委托给实现合约。 3. ProxyAdmin:管理代理的管理员合约。

代理合约通过 delegatecall 将所有调用委托给实现合约,而代理保存状态,这允许我们升级实现合约而不影响状态。

升级步骤: 1. 部署新的实现合约。 2. 发送交易更新代理合约的实现地址。

升级合约的限制

  1. 初始化:升级合约不能有构造函数,需使用 initializer 函数。
  2. 存储布局:升级不能更改存储布局,不能删除或重新排列已声明的状态变量。

测试可升级合约

测试可升级合约时,应对每个实现合约进行单元测试,并对代理合约进行高层次的交互测试。

准备部署到主网

在测试网运行项目一段时间且没有问题后,下一步就是将其部署到以太坊主网。然而,部署到主网的规划应该在预定发布日期之前开始。

本指南内容:

  • 审计与安全
  • 验证源代码
  • 安全管理密钥
  • 项目治理

请记住,尽管在测试网和主网上管理合约的技术操作是相同的,但主网与测试网的一个重大区别在于:主网管理的是用户的真实价值,因此需要额外的安全措施。

审计与安全

在智能合约中,安全至关重要,因为任何人都可以直接向你的合约发送交易,而且所有代码和状态都是公开的。如果遭遇黑客攻击,资金将不可挽回。安全应贯穿项目开发的各个阶段,而不仅仅是上线前的最后几周。

建议参考智能合约安全最佳实践并加入相关安全讨论。此外,项目完成后,可以向审计公司申请审计,例如 OpenZeppelin 的研究团队。尽管审计不能完全消除漏洞,但让有经验的安全研究人员审查代码能大大降低风险。

验证源代码

部署到主网后,验证合约源代码非常重要。验证是将 Solidity 代码提交到第三方平台(如 Etherscan 或 Sourcify),这些平台会将代码编译并与已部署的合约字节码进行匹配,以确保用户能查看到合约代码。

你可以手动在 Etherscan 上验证代码,也可以使用 hardhat-verify 插件:

npm install --save-dev @nomicfoundation/hardhat-verify

hardhat.config.js 文件中配置:

const { etherscanApiKey } = require('./secrets.json');
require("@nomicfoundation/hardhat-verify");

module.exports = {
  networks: {
    mainnet: { ... }
  },
  etherscan: {
    apiKey: etherscanApiKey
  }
};

最后运行验证任务,指定合约地址、网络以及构造函数参数:

npx hardhat verify --network mainnet 合约地址 "构造函数参数"

密钥管理

在主网上运行项目时,保护私钥尤为重要。合约部署和交互的账户将持有真正的以太币,因此成为黑客的目标。建议使用硬件钱包来确保密钥安全。

此外,部署到主网时,需要通过交易所购买以太币来支付部署费用。

管理员账户

管理员账户在系统中具有特殊权限,如暂停合约的能力。如果管理员账户被恶意用户控制,系统可能遭受巨大损害。建议使用多重签名(multisig)合约来管理这些关键账户。Safe 是一个常用的多签工具。

升级管理员

在 OpenZeppelin 升级插件中,默认情况下,合约的升级权限属于最初部署合约的外部账户。在主网部署时,建议将 ProxyAdmin 的控制权转移给更安全的多签账户。可以通过 admin.transferProxyAdminOwnership 转移 ProxyAdmin 的所有权。

项目治理

管理员账户的存在意味着项目并不完全去中心化,因为它们具有改变系统合约的能力。因此,治理机制至关重要。项目中某些操作(如系统参数调整或合约升级)需要特别的权限,如何分配这些权限取决于你的项目需求。

常见的治理模式包括由少数开发者管理或通过社区投票进行决策。

接下来的步骤

恭喜你完成从编写合约到生产部署的整个开发流程。但这只是个开始,接下来你需要持续收集用户反馈、添加新功能(通过合约升级实现)、监控应用并扩展项目。