如何使用OpenZeppelin开发智能合约
本文不是讲如何使用Solidity语言编写合约,而是如何使用框架来开发合约。关于Solidity语言的学习可以查看这里。
1.构建一个 Node 项目
新兴的软件行业通常以所有项目共享同一种技术栈开始。以太坊生态系统也不例外,其首选的语言是 JavaScript。包括 OpenZeppelin 软件在内的所有最常用的以太坊库都是用 JavaScript 或其变体编写的。
JavaScript 代码传统上是作为网站的一部分在网络浏览器上运行的,但也可以用 Node 作为一个独立进程来执行。
本文将帮助你建立你的 Node 开发环境,这将需要使用 OpenZeppelin 的各种工具和其他第三方产品。
如果你已经熟悉 Node、npm 和 Git,那么你可以跳过这个章节!
1.1安装 Node
有多种方式可以在您的计算机上获取 Node:您可以通过软件包管理器获取,也可以直接下载安装程序。
如果您使用的是 Windows 操作系统,请考虑使用 Windows Subsystem for Linux,因为许多生态系统组件都是为 Linux 编写的。
完成安装后,在终端上运行 node --version 命令来检查您的安装情况:14.x 或 16.x 系列的任何版本都与大多数以太坊软件兼容。
node --version
// v16.17.1
1.2 创建一个项目
JavaScript 代码通常被打成包,并通过 npm 注册表进行分发。一个包只是一个包含名为 package.json 的文件的目录,它描述了包的名称、版本、内容等信息。当您建立自己的项目时,即使您不打算将其分发,实际上您也是在创建一个包。
所有 Node 安装都包含用于 npm 注册表的命令行客户端,在开发自己的项目时您将会使用它。要开始一个新项目,请为其创建一个目录:
mkdir learn && cd learn
然后我们可以进行初始化:
npm init -y
就是这么简单!您新创建的 package.json 文件会随着项目的发展而不断演变,比如在使用 npm install
安装依赖时。
1.3 使用 npx
在 npm 注册表中有两种主要类型的软件包:库和可执行文件。已安装的库可以像其他任何 JavaScript 代码一样使用,但可执行文件则有特殊用途。
在安装 Node 时,会附带第三个二进制文件:npx
。它用于在项目中本地运行已安装的可执行文件。
虽然 Truffle
和 Hardhat
可以全局安装,但我们建议在每个项目中本地安装,这样您可以根据每个项目来控制版本。
为了清晰起见,在本指南中,我们将显示完整的命令,包括 npx,这样可以避免由于系统路径中没有该二进制文件而导致错误。
$ truffle init
truffle: command not found
$ npx truffle init
- Fetching solc version list from solc-bin. Attempt #1
Starting init...
================
> Copying project files to ...
Init successful, sweet!
2. 智能合约开发
在本指南中,不会涵盖语言概念,如语法或关键字。如果您需要了解这方面的内容,可以查看以下经过筛选的内容,其中包含了很多适合初学者和有经验的开发人员的学习资源:
- 如果您想了解以太坊和智能合约的一般概述,官方网站提供了一个“学习以太坊”部分,其中包含很多适合初学者的内容。
- 如果您是这门语言的新手,官方的Solidity文档是一个很好的资源。请注意他们的安全建议,这些建议很好地介绍了区块链和传统软件平台之间的区别。
- Consensys的最佳实践非常全面,包括了许多值得学习的经验和需要避免的已知陷阱。
- Ethernaut基于Web的游戏将让您寻找智能合约中微妙的漏洞,随着难度的增加逐步提高您的水平。
有了这些准备,我们开始吧!
2.1 设置Solidity
项目
创建项目后的第一步是安装开发工具。
比较知名的以太坊开发框架是Hardhat
,一般配合ethers.js
使用。另一个比较知名的开发框架是Truffle
,一般配合web3.js
使用。每个框架都有自己的优势。
在本指南中,我们将展示如何使用Hardhat
开发、测试和部署智能合约。
首先在我们的项目目录安装Hardhat
npm install --save-dev hardhat
安装完成后,我们可以运行npx hardhat
命令,它将在我们的项目目录中创建一个Hardhat
配置文件(hardhat.config.js
)。
2.2 第一份合约
我们将 Solidity
源文件(.sol
)存储在一个叫做 contracts
的目录中。这相当于你可能在其他语言中熟悉的 src
目录。
现在,我们可以编写我们的第一个简单的智能合约,称为 Box:它将允许人们存储一个值,稍后可以检索它。
我们将把这个文件保存为 contracts/Box.sol。每个 .sol 文件应该包含一个单独的合约代码,并以其命名。
// 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 value);
// Stores a new value in the contract
function store(uint256 value) public {
_value = value;
emit ValueChanged(value);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}
}
2.3 编译Solidity
以太坊虚拟机(EVM)无法直接执行Solidity代码:我们首先需要将其编译成EVM字节码。
我们的 Box.sol
合约使用 Solidity 0.8
,因此我们需要先配置 Hardhat
使用适当的 solc
版本。
我们在 hardhat.config.js
中指定 Solidity 0.8 solc
版本。
// hardhat.config.js
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.4",
};
可以通过运行单个编译命令来完成编译:
npx hardhat compile
内置的编译任务将自动查找位于 contracts
目录中的所有合约,并使用 hardhat.config.js
中的配置使用Solidity编译器对它们进行编译。
您会注意到一个 artifacts
目录被创建:它保存了编译后的合约构件(字节码和元数据),这些构件是 .json
文件。将该目录添加到您的 .gitignore
中。
2.4 添加更多的合约
随着您的项目的发展,您将开始创建更多相互作用的合约:每个合约应该存储在自己的.sol文件中。
为了看看这是什么样子,我们向 Box
合约添加一个简单的访问控制系统:我们将在一个名为 Auth
的合约中存储一个管理员地址,并只允许 Auth
允许的账户使用Box。
因为编译器将获取 contracts
目录和其子目录中的所有文件,所以您可以自由地组织您的代码。在这里,我们将 Auth
合约存储在一个名为 access-control
的子目录中:
// contracts/access-control/Auth.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Auth {
address private _administrator;
constructor(address deployer) {
// Make the deployer of the contract the administrator
_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 Auth from the access-control subdirectory
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 that the caller is registered as an administrator in Auth
require(_auth.isAdministrator(msg.sender), "Unauthorized");
_value = value;
emit ValueChanged(value);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
分离不同合约中的职责是保持每个合约简单的好方法,通常是一个好的实践。
然而,这不是将代码拆分为模块的唯一方法。在Solidity中,您还可以使用继承来实现封装和代码复用,接下来我们将看到如何使用继承。
3. 使用 OpenZeppelin
合约
可重用的模块和库是优秀软件的基石。OpenZeppelin 合约包含许多有用的构建块,供智能合约构建使用。并且在构建时,您可以放心使用它们:它们经过多次审计,其安全性和正确性经过了实战考验。
3.1 关于继承
OpenZeppelin
库中的许多合约都不是独立的,也就是说,您不应该将它们按原样部署。相反,您将使用它们作为构建您自己的合约的起点,通过向它们添加功能来实现您想要的功能。Solidity
提供了多重继承作为实现这一机制的一种方式:请查看Solidity文档以获取更多详细信息。
例如,Ownable
合约将部署账户标记为合约的所有者,并提供了一个名为 onlyOwner
的修饰符。当应用于函数时,onlyOwner
将导致所有不起源于所有者账户的函数调用失败。还提供了转让和放弃所有权的函数。
当以这种方式使用时,继承成为了一种强大的机制,允许进行模块化,而不需要强制您部署和管理多个合约。
3.2 导入OpenZeppelin
合约
可以通过运行以下命令下载OpenZeppelin Contracts库的最新发布版本:
npm install @openzeppelin/contracts
您应该始终使用这些发布版本中的库:将库源代码复制粘贴到您的项目中是一种危险的做法,会使您的合约非常容易引入安全漏洞。
要使用 OpenZeppelin Contracts
中的一个合约,需要在其路径前缀中加上@openzeppelin/contracts
。例如,为了替换我们自己的 Auth
合约,我们将导入@openzeppelin/contracts/access/Ownable.sol
以向Box添加访问控制:
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Import Ownable from the OpenZeppelin Contracts library
import "@openzeppelin/contracts/access/Ownable.sol";
// Make Box inherit from the Ownable contract
contract Box is Ownable {
uint256 private _value;
event ValueChanged(uint256 value);
// The onlyOwner modifier restricts who can call the store function
function store(uint256 value) public onlyOwner {
_value = value;
emit ValueChanged(value);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
OpenZeppelin Contracts文档是学习开发安全智能合约系统的好地方。它既有指南,也有详细的API参考:例如,可以查看访问控制指南以了解上面代码示例中使用的Ownable合约的更多信息。