开发智能合约

欢迎来到激动人心的智能合约开发世界!本指南将引导你开始编写 Solidity 合约,内容包括:

关于 Solidity

本指南不会涵盖语言概念,如语法或关键字。为此,你需要查看以下精选内容,其中包含面向新手和经验丰富的开发人员的优秀学习资源:

  • 对于以太坊和智能合约如何工作的一般概述,官方网站有一个 了解以太坊 部分,其中包含大量适合初学者的内容。

  • 如果你是这门语言的新手,https://solidity.readthedocs.io/en/latest/introduction-to-smart-contracts.html[官方 Solidity 文档] 是一个很好的资源。请查看他们的 安全建议,其中很好地介绍了区块链和传统软件平台之间的区别。

  • Consensys 的 最佳实践 非常广泛,包括 经过验证的模式 以供学习和 已知的陷阱 以避免。

  • 基于 Web 的游戏 Ethernaut 将让你在智能合约中寻找细微的漏洞,随着你通过难度越来越大的关卡。

说完这些,让我们开始吧!

设置项目

创建项目后的第一步是安装开发工具。

以太坊最流行的开发框架是 HardhatFoundry。 每一个都有其优势,并且熟悉使用它们是有用的。

在这些指南中,我们将展示如何使用 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 的合约中存储一个管理员地址,并且只允许 BoxAuth 允许的那些帐户使用。

因为编译器会选取 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。 用于 transferrenounce 所有权的函数也可用。

以这种方式使用时,继承成为一种强大的机制,允许模块化,而无需你部署和管理多个合约。

导入 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 合约的信息。

下一步

编写和编译 Solidity 合约只是让你的去中心化应用在以太坊网络上运行的第一步。 熟悉此设置后,你将希望转移到更高级的任务: