开发智能合约
欢迎来到激动人心的智能合约开发世界!本指南将引导你开始编写 Solidity 合约,内容包括:
关于 Solidity
本指南不会涵盖语言概念,如语法或关键字。为此,你需要查看以下精选内容,其中包含面向新手和经验丰富的开发人员的优秀学习资源:
说完这些,让我们开始吧!
设置项目
创建项目后的第一步是安装开发工具。
在这些指南中,我们将展示如何使用 Hardhat 开发、测试和部署智能合约,并且我们将介绍它与 ethers.js 的最常见用法。
要开始使用 Hardhat,我们将在 项目目录中安装它。
$ npm install --save-dev hardhat
安装完成后,我们可以运行 npx hardhat
。 这将在我们的项目目录中创建一个 Hardhat 配置文件 (hardhat.config.js
)。
$ npx 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.22.12 👷
✔ What do you want to do? · Create an empty hardhat.config.js
已创建配置文件
第一个合约
我们将我们的 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;
// 当存储的值更改时发出
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 字节码。
我们的 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.24",
};
然后可以通过运行单个编译命令来实现编译:
如果你不熟悉 npx 命令,请查看我们的 Node 项目设置指南。
|
$ npx hardhat compile
成功编译 1 个 Solidity 文件 (evm target: paris)。
`compile
内置任务将自动查找 contracts
目录中的所有合约,并使用 Solidity 编译器使用 hardhat.config.js
中的配置编译它们。
你会注意到创建了一个 artifacts
目录:它保存了编译后的 artifacts(字节码和元数据),它们是 .json 文件。 最好将此目录添加到你的 .gitignore
中。
添加更多合约
随着你的项目增长,你将开始创建更多相互交互的合约:每个合约都应存储在自己的 .sol
文件中。
要了解它的外观,让我们向我们的 Box
合约添加一个简单的访问控制系统:我们将在一个名为 Auth
的合约中存储一个管理员地址,并且只允许 Box
被 Auth
允许的那些帐户使用。
因为编译器会选取 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) {
// 使合约的部署者成为管理员
_administrator = deployer;
}
function isAdministrator(address user) public view returns (bool) {
return user == _administrator;
}
}
要从 Box
中使用此合约,我们使用 import
语句,通过其相对路径引用 Auth
:
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 从 access-control 子目录导入 Auth
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 {
// 要求调用者在 Auth 中注册为管理员
require(_auth.isAdministrator(msg.sender), "Unauthorized");
_value = value;
emit ValueChanged(value);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
跨多个合约分离关注点是保持每个合约简单的绝佳方式,并且通常是一个好的做法。
但是,这不是将你的代码拆分为模块的唯一方法。 你还可以使用 继承 在 Solidity 中进行封装和代码重用,我们将在接下来看到。
使用 OpenZeppelin Contracts
可重用的模块和库是优秀软件的基石。 OpenZeppelin Contracts 包含许多有用的智能合约构建块。 并且在它们的基础上构建时,你可以放心:它们已经过多次审计,它们的安全性得到了实战检验。
关于继承
库中的许多合约不是独立的,也就是说,不希望你按原样部署它们。 相反,你将使用它们作为起点,通过向它们添加功能来构建你自己的合约。 Solidity 提供 多重继承 作为实现此目的的一种机制:有关更多详细信息,请查看 Solidity 文档。
例如,Ownable
合约将部署者帐户标记为合约的所有者,并提供一个名为 onlyOwner
的修饰符。 当应用于一个函数时,onlyOwner
将导致所有不是来自所有者帐户的函数调用都 revert。 用于 transfer 和 renounce 所有权的函数也可用。
以这种方式使用时,继承成为一种强大的机制,允许模块化,而无需你部署和管理多个合约。
导入 OpenZeppelin Contracts
可以通过运行以下命令下载 OpenZeppelin Contracts 库的最新发布版本:
$ npm install @openzeppelin/contracts
你应该始终使用来自这些已发布版本的库:将库源代码复制粘贴到你的项目中是一种危险的做法,很容易在你的合约中引入安全漏洞。 |
要使用其中一个 OpenZeppelin Contracts,请 import
它,方法是在其路径前加上 @openzeppelin/contracts
。 例如,为了替换我们自己的 Auth
合约,我们将导入 @openzeppelin/contracts/access/Ownable.sol
以将访问控制添加到 Box
:
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 从 OpenZeppelin Contracts 库导入 Ownable
import "@openzeppelin/contracts/access/Ownable.sol";
// 使 Box 从 Ownable 合约继承
contract Box is Ownable {
uint256 private _value;
event ValueChanged(uint256 value);
constructor() Ownable(msg.sender) {}
// onlyOwner 修饰符限制了谁可以调用 store 函数
function store(uint256 value) public onlyOwner {
_value = value;
emit ValueChanged(value);
}
function retrieve() public view returns (uint256) {
return _value;
}
}
OpenZeppelin Contracts 文档 是学习开发安全智能合约系统的好地方。 它提供了指南和详细的 API 参考:例如,请参阅 Access Control 指南,以了解更多关于上面代码示例中使用的 Ownable
合约的信息。