OpenZeppelin Contracts 4.x 文档

OpenZeppelin Contracts 4.x 文档

本文是对 OpenZeppelin Contracts 4.x 文档(截止2022.9.14)的部分翻译。

Overview

OpenZeppelin Contracts 是建立在经过社区审查的代码的坚实基础之上的用于安全智能合约开发的库。当前有 2.x、3.x 和 4.x 三个版本。

其包含有:

  • ERC20 和 ERC721 等标准的实施
  • 灵活的基于角色的许可方案
  • 可重用的 Solidity 组件,用于构建自定义合约和复杂的去中心化系统

OpenZeppelin Contracts 具有稳定的 API,这意味着合约在升级到新的次版本(minor version)时不会意外中断。

使用 npm 即可安装:

$ npm install @openzeppelin/contracts

安装后,在项目中导入库中的合约即可使用:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MyNFT is ERC721 {
    constructor() ERC721("MyNFT", "MNFT") {
    }
}

如果不熟悉智能合约开发,可以前往 Developing Smart Contracts 来了解有关创建新项目和变异合约的信息。

为了保证系统安全,应该始终按照原样使用已安装的代码,既不要从在线资源复制粘贴,也不要执行修改。该库被设计为仅您使用的合约和函数会被部署,因此您无需担心它会不必要地增加 gas 成本。

官网的 guides 上给出了几个常见的用例和良好实践。以下文章提供了很好的阅读背景,但应注意,随着生态系统中工具继续快速发展,一些引用的工具已经发生了变化。

合约向导 页面提供的交互式生成器可以帮助引导您的合约来了解 OpenZeppelin Contracts 中提供的组件。使用交互式生成器,可以根据多种需求来生成定制化的合约,比如可以选择 FEATURES、ACCESS CONTROL、UPGRADEABILITY 等特性。生成的合约可以使用 Hardhat 或 Truffle 等工具编译部署。

Extending Contracts

大多数的 OpenZeppelin Contracts 被期望通过继承(inheritance)来使用:通过继承库中的合约来编写自己的合约。

常见的是 is 语法,比如 contract MyToken is ERC20

NOTE:与 contract 不同,Solidity library 不是继承而是依赖于 using for 语法。Openzeppelin Contracts 有一些 library,大部分都在 Utils 目录中。

Overriding

继承通常用于将父合约的功能添加到自己的合约中,但这并不是它所能做的全部。还可以使用 overrides 来改变父合约某些部分的行为。

例如,假设想要更改 AccessControl 以便不再调用 revokeRole,可以使用 overrides 来实现:

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

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

contract ModifiedAccessControl is AccessControl {
    // Override the revokeRole function
    function revokeRole(bytes32, address) public override {
        revert("ModifiedAccessControl: cannot revoke roles");
    }
}

然后旧的 revokeRole 就被替换了,任何对它的调用都会立即 revert。我们无法从合约从删除(remove)该函数,但 revert 所有的调用就足够了。

Calling super

有时想要 extend 父合约的行为,而不是将其更改为其他行为,就需要用到 super

super 关键字允许您调用父合约中定义的函数,即使它被覆盖(overridden)。该机制可用于向函数添加额外的检查、发出事件或以其他方式添加您认为合适的功能。

TIP:有关 overrides 如何工作的信息,请参阅 Solidity 官方文档

下面是 AccessControl 的修改版本,其中 revokeRole 不能用于 revoke DEFAULT_ADMIN_ROLE:

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

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

contract ModifiedAccessControl is AccessControl {
    function revokeRole(bytes32 role, address account) public override {
        require(
            role != DEFAULT_ADMIN_ROLE,
            "ModifiedAccessControl: cannot revoke default admin role"
        );

        super.revokeRole(role, account);
    }
}

最后的 super.revokeRole 语句将调用 AccessControl 的原始版本的 revokeRole,如果没有 override 那么相同的代码会运行。

NOTE:从 v3.0.0 开始,view 函数在 OpenZeppelin 中不是 virtual,因此不能被覆盖。我们正在考虑在即将发布的版本中取消此限制

Using Hooks

有时,为了扩展父合约,需要覆盖多个相关函数,这会导致代码重复并增加 bug 的可能性。

例如,考虑以 IERC721Receiver 的方式实现安全的 ERC20 transfer。您可能认为覆盖 transfertransferFrom 就足够了,但是 _transfer_mint 呢?为了避免你不得不处理这些细节,我们引入了钩子(hooks)。

Hooks 只是在某些操作发生之前或之后调用的函数。它们提供了一个集中的点来钩入(hook into)和扩展原始行为。

以下是在 ERC20 中使用 _beforeTokenTransfer 实现 IERC721Receiver 模式的方法:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20WithSafeTransfer is ERC20 {
    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal virtual override
    {
        super._beforeTokenTransfer(from, to, amount);

        require(_validRecipient(to), "ERC20WithSafeTransfer: invalid recipient");
    }

    function _validRecipient(address to) private view returns (bool) {
        ...
    }

    ...
}

以这种方式使用钩子会使代码更干净、更安全,而不必依赖对父合约内部的深入了解。

NOTE:Hooks 是 OpenZeppelin Contracts v3.0.0 的新功能,我们渴望了解您打算如何使用它们!到目前为止,唯一可用的钩子是 _beforeTransferHook,在所有的ERC20, ERC721, ERC777ERC1155 中。

Rules of Hooks

为了防止在编写使用钩子的代码出现问题,您应该遵循一些准则。它们非常简单,但请确保您遵循它们:

  1. 每当您覆盖(override)父合约的钩子时,将 virtual 属性重新应用于钩子。这将允许子合约向钩子添加更多的功能。
  2. 总是在你的覆盖中使用 super 调用父合约的钩子。这将确保调用继承树中的所有钩子:像 ERC20Pausable 这样的合约依赖于这种行为。
contract MyToken is ERC20 {
    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal virtual override // Add virtual here!
    {
        super._beforeTokenTransfer(from, to, amount); // Call parent hook
        ...
    }
}

Using with Upgrades

如果您的合约要部署可升级,例如使用 OpenZeppelin Upgrades Plugins,您将需要使用 OpenZeppelin Contracts 的可升级变体。

此变体可作为名为 @openzeppelin/contracts-upgradeable 的单独包提供,该软件包托管在仓库 OpenZeppelin/openzeppelin-contracts-upgradeable 中。

它遵循 编写可升级合约 的所有规则:构造函数替换为初始化函数,状态变量在初始化函数中初始化,我们还检查次要版本之间的存储不兼容性。

Overview

使用 npm 命令安装:

$ npm install @openzeppelin/contracts-upgradeable

这个包复制了主 OpenZeppelin Contracts 包结构,但每个文件和合约都有后缀 Upgradeable

-import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";

-contract MyCollectible is ERC721 {
+contract MyCollectible is ERC721Upgradeable {

构造函数被内部初始化函数替换,遵循命名约定 __{ContractName}_init。由于这些是 internal,因此您必须始终定义自己的公共初始化函数(public initializer function) 并调用您扩展的合约的父合约初始化程序。

-    constructor() ERC721("MyCollectible", "MCO") public {
+    function initialize() initializer public {
+        __ERC721_init("MyCollectible", "MCO");
     }

CAUTION:与多重继承一起使用需要特别注意。请参阅下面标题为多重继承的部分。

Further Notes

Multiple Inheritance

初始化函数不像构造函数那样被编译器线性化。因此,每个 __{ContractName}_init 函数都将线性化调用嵌入到了所有父合约初始化函数中。因此,调用其中两个 init 函数可能会初始化同一个合约两次。

每个合约中的 __{ContractName}_init_unchained 函数是初始化函数减去对父合约初始化函数的调用,可用于避免双重初始化问题,但不建议手动执行此操作。我们希望能够在 Upgrades Plugins 的未来版本中对此进行安全检查。

Storage Gaps

您可能会注意到每个合约都包含一个名为 __gap 的状态变量。这是在可升级合约中放置的存储(sotrage)中的空的保留空间。它允许我们在未来自由添加新的状态变量,而不会影响与现有部署的存储兼容性。

简单地添加一个状态变量是不安全的,因为它会“向下移动(shifts down)”继承链中下面的所有状态变量。这使得存储布局不兼容,如 Writing Upgradeable Contracts 中所述。计算 __gap 数组的大小,以便合约使用的存储量加起来总是相同的数字(在本例中为 50 storage slots)。

Access Control

访问控制——即“谁被允许做这件事”—— 在智能合约的世界中非常重要。你的合约的访问控制可以控制谁可以铸造代币、对提案进行投票、冻结转账和许多其他事情。因此,了解您如何实现它至关重要,以免其他人窃取您的整个系统

Ownership and Ownable

最常见和最基本的访问控制形式是所有权(ownership)的概念:有一个账户是合约的 owner,可以对其执行管理任务。这种方法对于只有一个管理用户的合约来说是完全合理的。

OpenZeppelin Contracts 提供 Ownable 用于在您的合约中实现所有权。

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

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

contract MyContract is Ownable {
    function normalThing() public {
        // anyone can call this normalThing()
    }

    function specialThing() public onlyOwner {
        // only the owner can call specialThing()!
    }
}

默认情况下,Ownable 合约的 owner 是部署它的帐户,这通常正是您想要的。

Ownable 还可以让您:

  • transferOwnership 从所有者账户到新账户,以及
  • renounceOwnership 让所有者放弃此管理特权,这是集中管理初始阶段结束后的常见模式。

WARNING:完全删除所有者将意味着受 onlyOwner 保护的管理任务将不再可调用!

请注意,一个合约也可以是另一个合约的所有者!例如,这为使用 Gnosis SafeAragon DAO 或您创建的完全自定义合约打开了大门。

通过这种方式,您可以使用可组合性(composability)为您的合约添加额外的访问控制复杂性层。例如,您可以使用由项目负责人运行的 2-of-3 多重签名,而不是将单个常规以太坊账户(外部拥有的账户,或 EOA)作为所有者。该领域的知名项目,例如 MakerDAO,使用与此类似的系统。

Role-Based Access Control

虽然 ownership 的简单性对于简单的系统或快速原型设计很有用,但通常需要不同级别的授权。您可能希望帐户有权禁止用户进入系统,但不能创建新代币。Role-Based Access Control (RBAC) 在这方面提供了灵活性。

本质上,我们将定义多个角色,每个角色都允许执行不同的操作集。例如,一个帐户可能具有“moderator”、“minter”或“admin”角色,然后您将检查这些角色,而不是简单地使用 onlyOwner。可以通过 onlyRole 修饰符强制执行此检查。另外,您将能够定义如何向帐户授予角色、撤销角色等规则。

大多数软件使用基于角色的访问控制系统:一些用户是普通用户,一些可能是主管(supervisor)或经理(manager),还有一些通常具有管理权限。

Using AccessControl

OpenZeppelin Contracts 为实现基于角色的访问控制提供了 AccessControl。它的用法很简单:对于您要定义的每个角色,您将创建一个新的角色标识符(role identifier),用于授予(grant)、撤销(revoke)和检查(check)帐户是否具有该角色。

这是一个在 ERC20 token 中使用 AccessControl 来定义“minter”角色的简单示例,该角色允许拥有它的帐户创建新的 token:

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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20, AccessControl {
    // Create a new role identifier for the minter role
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor(address minter) ERC20("MyToken", "TKN") {
        // Grant the minter role to a specified account
        _setupRole(MINTER_ROLE, minter);
    }

    function mint(address to, uint256 amount) public {
        // Check that the calling account has the minter role
        require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
        _mint(to, amount);
    }
}

NOTE:在您的系统上使用 AccessControl 或复制粘贴本指南中的示例之前,请确保您完全了解其工作原理。

虽然清晰明确,但这并不是我们使用 Ownable 无法实现的。事实上,AccessControl 的亮点在于需要细粒度权限的场景,这可以通过定义多个角色来实现。

让我们通过定义一个“burner”角色来扩充我们的 ERC20 代币示例,该角色允许帐户销毁代币,并使用 onlyRole 修饰符:

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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor(address minter, address burner) ERC20("MyToken", "TKN") {
        _setupRole(MINTER_ROLE, minter);
        _setupRole(BURNER_ROLE, burner);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }
}

多干净!通过这种方式拆分关注点,可以实现比使用更简单的 ownership 方法进行访问控制更细化的权限级别。限制系统的每个组件能够执行的操作被称为 最小权限原则,并且是一种良好的安全实践。请注意,如果需要,每个帐户可能仍具有多个角色。

Granting and Revoking Roles

上面的 ERC20 token 示例使用 _setupRole,这是一个 internal 函数,在以编程方式分配角色时(例如在构造期间)很有用。但是,如果我们稍后想将“minter”角色授予其他帐户怎么办?

默认情况下,具有角色的帐户无法从其他帐户授予或撤销它:拥有角色所做的只是使 hasRole 检查通过。要动态授予和撤销角色,您需要角色管理员(role’s admin)的帮助。

每个角色都有一个关联的管理员角色,该角色授予调用 grantRolerevokeRole 函数的权限。如果调用帐户具有相应的管理员角色,则可以使用这些来授予或撤销角色。多个角色可能具有相同的管理员角色,以便于管理。一个角色的管理员甚至可以是同一个角色本身,这将导致具有该角色的帐户也能够授予和撤销它。

这种机制可用于创建类似于组织结构图的复杂许可结构,但它也提供了一种管理简单应用程序的简单方法。 AccessControl 包含一个特殊角色,称为 DEFAULT_ADMIN_ROLE,它充当所有角色的默认管理员角色。具有此角色的帐户将能够管理任何其他角色,除非 _setRoleAdmin 用于选择新的管理员角色。

让我们看一下 ERC20 代币示例,这一次利用了默认管理员角色:

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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC20("MyToken", "TKN") {
        // Grant the contract deployer the default admin role: it will be able
        // to grant and revoke any roles
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }
}

请注意,与前面的示例不同,没有帐户被授予“minter”或“burner”角色。但是,由于这些角色的管理员角色是默认管理员角色,并且该角色已授予 msg.sender,因此同一帐户可以调用 grantRole 授予铸币或销毁权限,也可以调用 revokeRole 将其删除。

动态角色分配通常是理想的属性,例如在对参与者的信任可能随时间变化的系统中。它还可用于支持诸如 KYC 之类的用例,其中角色承担者的列表可能事先不知道,或者包含在单个交易中可能过于昂贵。

Querying Privileged Accounts

由于帐户可能会动态授予和撤销角色,因此并不总是可以确定哪些帐户拥有特定角色。这很重要,因为它可以证明系统的某些属性,例如管理帐户是多重签名或 DAO,或者某个角色已从所有用户中删除,从而有效地禁用任何相关功能。

在底层,AccessControl 使用 EnumerableSet,这是 Solidity mapping 类型的一个更强大的变体,它允许键枚举。 getRoleMemberCount 可用于检索具有特定角色的帐户的数量,然后可以调用 getRoleMember 来获取每个帐户的地址。

const minterCount = await myToken.getRoleMemberCount(MINTER_ROLE);

const members = [];
for (let i = 0; i < minterCount; ++i) {
    members.push(await myToken.getRoleMember(MINTER_ROLE, i));
}

Delayed operation

访问控制对于防止未经授权访问关键函数来说至关重要。这些函数可用于铸造代币、冻结转账或执行完全改变智能合约逻辑的升级。虽然 OwnableAccessControl 可以防止未经授权的访问,但它们并没有解决行为不端的管理员攻击他们自己的系统以损害其用户的问题。

这是 TimelockController 正在解决的问题。

TimelockController 是由提议者(proposer)和执行者(executor)管理的代理。当设置为智能合约的所有者(owner)/管理员(admin)/控制者(controller)时,它确保提议者下令的任何维护操作都会受到延迟。这种延迟保护了智能合约的用户,让他们有时间审查维护操作并在他们认为这样做最符合他们利益的情况下退出系统。

Using TimelockController

默认情况下,部署了 TimelockController 的地址获得对时间锁的管理权限。此角色授予指定提议者、执行者和其他管理员的权利。

配置 TimelockController 的第一步是至少分配一个提议者和一个执行者。这些可以在构建期间或以后由具有管理员角色的任何人分配。这些角色不是独占的,这意味着一个帐户可以同时拥有这两个角色。

角色由 AccessControl 接口管理,每个角色的 bytes32 值可通过 ADMIN_ROLEPROPOSER_ROLEEXECUTOR_ROLE 常量访问。

AccessControl 之上构建了一个附加特性:将执行者角色赋予 address(0) 可以在时间锁到期后向任何人开放执行提案的权限。此功能虽然有用,但应谨慎使用。

此时,同时分配了提议者和执行者,时间锁就可以执行操作了。

可选的下一步是部署者放弃其管理权限并让时间锁自行管理。如果部署者决定这样做,所有进一步的维护,包括分配新的提议者/调度者或更改时间锁持续时间都必须遵循时间锁工作流程。这将时间锁的治理与附加到时间锁的合约的治理联系起来,并强制延迟时间锁维护操作。

WARNING:如果部署者放弃管理权限以支持 timelock 本身,则分配新的提议者或执行者将需要 timelocked 操作。这意味着,如果负责这两个角色中的任何一个的账户不可用,那么整个合约(以及它控制的任何合约)都会被无限期锁定。

通过分配提议者和执行者角色以及负责其自身管理的时间锁,您现在可以将任何合约的所有权/控制权转移到时间锁。

TIP:推荐的配置是将这两个角色授予安全治理合约(例如 DAO 或多重签名),并将执行者角色授予负责帮助维护操作的人员持有的一些 EOA。这些钱包无法接管时间锁,但它们可以帮助简化工作流程。

Minimum delay

TimelockController 执行的操作不受固定延迟的影响,而是最小延迟。一些主要更新可能需要更长的延迟。例如,如果仅仅几天的延迟可能足以让用户审核铸币操作,那么在安排智能合约升级时使用几周甚至几个月的延迟是有意义的。

可以通过调用 updateDelay 函数来更新最小延迟(可通过 getMinDelay 方法访问)。请记住,只有时间锁本身才能访问此功能,这意味着此维护操作必须通过时间锁本身。

Tokens

啊,“代币”:区块链最强大也是最容易被误解的工具。

代币是区块链中某物的表示。这可以是金钱、时间、服务、公司股份、虚拟宠物,任何东西。通过将事物表示为代币,我们可以允许智能合约与它们交互、交换、创建或销毁它们。

But First, Coffee a Primer on Token Contracts

围绕代币的大部分混淆来自于两个概念的混淆:代币合约(token contracts)和实际代币(token)。

代币合约只是一个以太坊智能合约。 “发送代币”实际上意味着“调用某人编写和部署的智能合约上的方法”。归根结底,代币合约只不过是地址到余额的映射,加上一些从这些余额中加减的方法。

正是这些余额代表了代币本身。当代币合约中的余额不为零时,某人“拥有代币”。That’s it!这些余额可以被视为金钱、游戏中的经验值、所有权契约或投票权,并且这些代币中的每一个都将存储在不同的代币合约中。

Different Kinds of Tokens

请注意,拥有两个投票权和两个所有权契约之间有很大的区别:每个投票都等于所有其他投票,但房子通常不是!这称为可替代性(fungibility)。可替代商品(Fungible goods)是等价且可互换的,例如以太币、法定货币和投票权。不可替代的(Non-fungible)商品是独一无二的,就像所有权契约或收藏品一样。

简而言之,在处理不可替代的资产(例如您的房子)时,您关心的是您拥有哪些资产,而在可替代资产(例如您的银行账户对账单)中,重要的是您拥有多少

Standards

尽管代币的概念很简单,但它们在实现中具有各种复杂性。因为以太坊中的一切都只是一个智能合约,并且没有关于智能合约必须做什么的规则,社区已经开发了各种标准(称为 EIP 或 ERC)来记录合约如何与其他合约互操作。

您可能听说过 ERC20 或 ERC721 代币标准,这就是您来这里的原因。前往我们的专业指南了解更多信息:

  • ERC20:可替代资产最广泛的代币标准,尽管在某种程度上受到其简单性的限制。
  • ERC721:不可替代代币的实际解决方案,通常用于收藏品和游戏。
  • ERC777:更丰富的可替代代币标准,支持新的用例并建立在过去的学习基础上。向后兼容 ERC20。
  • ERC1155:一种新的多代币标准,允许单个合约代表多个可替代和不可替代的代币,以及批量操作以提高gas效率。

ERC20

ERC721

ERC777

ERC1155

Governance

CrossChain

Utilities

OpenZeppelin Contracts 提供了大量有用的实用程序,您可以在项目中使用它们。这里有一些比较流行的。

Cryptography

Checking Signatures On-Chain

ECDSA 提供了恢复和管理以太坊账户 ECDSA 签名的功能。这些通常是通过 web3.eth.sign 生成的,是一个 65 字节的数组(Solidity 中的 bytes 类型),排列方式如下:[[v (1)], [r (32)], [s (32)]]

可以使用 ECDSA.recover 恢复数据签名者,并将其地址进行比较以验证签名。大多数钱包会对数据进行哈希签名并添加前缀“\x19Ethereum Signed Message:\n”,因此在尝试恢复以太坊签名消息哈希的签名者时,您需要使用 toEthSignedMessageHash

using ECDSA for bytes32;

function _verify(bytes32 data, bytes memory signature, address account) internal pure returns (bool) {
    return data
        .toEthSignedMessageHash()
        .recover(signature) == account;
}

WARNING:正确进行签名验证并非易事:确保您完全阅读并理解 ECDSA 的文档。

Verifying Merkle Proofs

MerkleProof 提供:

Introspection

在 Solidity 中,了解合约是否支持您想要使用的接口通常很有帮助。 ERC165 是一个有助于进行运行时接口检测的标准。合约为在你的合约中实现 ERC165 和查询其他合约提供了帮助:

contract MyContract {
    using ERC165Checker for address;

    bytes4 private InterfaceId_ERC721 = 0x80ac58cd;

    /**
     * @dev transfer an ERC721 token from this contract to someone else
     */
    function transferERC721(
        address token,
        address to,
        uint256 tokenId
    )
        public
    {
        require(token.supportsInterface(InterfaceId_ERC721), "IS_NOT_721_TOKEN");
        IERC721(token).transferFrom(address(this), to, tokenId);
    }
}

Math

OpenZeppelin Contracts 提供的最流行的数学相关库是 SafeMath,它提供了保护您的合约免受上溢和下溢的数学函数。

通过 using SafeMath for uint256; 包含合约;然后调用函数:

  • myNumber.add(otherNumber)
  • myNumber.sub(otherNumber)
  • myNumber.div(otherNumber)
  • myNumber.mul(otherNumber)
  • myNumber.mod(otherNumber)

Payment

想要在多人之间分摊一些付款?也许你有一个应用程序,将 30% 的艺术品购买发送给原始创作者,将 70% 的利润发送给当前所有者;您可以使用 PaymentSplitter 构建它!

在 Solidity 中,盲目地向账户汇款存在一些安全问题,因为它允许账户执行任意代码。您可以在 Ethereum Smart Contract Best Practices 网站上阅读这些安全问题。解决重入和停滞问题的方法之一是,您可以使用 PullPayment,而不是立即将 Ether 发送到需要它的帐户,它提供了一个 _asyncTransfer 函数,用于向某物汇款并要求他们稍后再 withdrawPayments()

如果你想托管一些资金,请查看 EscrowConditionalEscrow 以管理一些托管 Ether 的释放。

Collections

如果您需要比 Solidity 的原生数组和映射更强大的集合支持,请查看 EnumerableSetEnumerableMap。它们类似于映射,因为它们在恒定时间内存储和删除元素并且不允许重复条目,但它们还支持枚举(enumeration),这意味着您可以轻松地查询链上和链下的所有存储条目。

Misc

想检查地址是否为合约?使用 AddressAddress.isContract()

想要跟踪一些每次需要另一个数字时递增 1 的数字?查看 Counters。这对很多事情都很有用,例如创建增量标识符,如 ERC721 指南 中所示。

Base64

Base64 实用程序允许您将 bytes32 数据转换为其 Base64 string 表示。

这对于为 ERC721ERC1155 构建 URL 安全的 tokenURI 特别有用。该库提供了一种巧妙的方式来提供符合 URL 安全的 Data URI 兼容字符串以提供链上数据结构。

考虑这是一个使用 ERC721 通过 Base64 Data URI 发送 JSON 元数据的示例:

// contracts/My721Token.sol
// SPDX-License-Identifier: MIT

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

contract My721Token is ERC721 {
    using Strings for uint256;

    constructor() ERC721("My721Token", "MTK") {}

    ...

    function tokenURI(uint256 tokenId)
        public
        pure
        override
        returns (string memory)
    {
        bytes memory dataURI = abi.encodePacked(
            '{',
                '"name": "My721Token #', tokenId.toString(), '"',
                // Replace with extra ERC721 Metadata properties
            '}'
        );

        return string(
            abi.encodePacked(
                "data:application/json;base64,",
                Base64.encode(dataURI)
            )
        );
    }
}

Multicall

Multicall 抽象合约带有一个 multicall 函数,该函数将多个调用捆绑在一个外部调用中。有了它,外部帐户可以执行包含多个函数调用的原子操作。这不仅对 EOA 在单个交易中进行多次调用很有用,它也是一种在后续调用失败时恢复先前调用的方法。

考虑这个虚拟合约:

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

import "@openzeppelin/contracts/utils/Multicall.sol";

contract Box is Multicall {
    function foo() public {
        ...
    }

    function bar() public {
        ...
    }
}

这是使用 Truffle 调用 multicall 函数的方法,允许在单个交易中调用 foobar

// scripts/foobar.js

const Box = artifacts.require('Box');
const instance = await Box.new();

await instance.multicall([
    instance.contract.methods.foo().encodeABI(),
    instance.contract.methods.bar().encodeABI()
]);

API

OpenZeppelin Docs API 文档提供了库中所有 合约(Contract)、库(Library)、接口(Interface) 的 修饰符(Modifier)、函数(Function) 和 事件(Events) 等的用法和 API 说明,以供检索查阅。

可以在顶部的搜索栏中检索关键字,搜索栏会在整个文档中查找匹配的对应关键字。


OpenZeppelin Contracts 4.x 文档
https://alphafitz.com/2022/09/14/openzeppelin-contracts-4-docs/
作者
alphafitz
发布于
2022年9月14日
许可协议