OpenZeppelin 指导手册

米霖

2024-10-19

openzepelin 体系

要梳理 OpenZeppelin 的知识体系,树状图的结构可以从核心功能模块入手,逐步细分到具体的合约类型、工具和用途。以下是 OpenZeppelin 的核心模块及其分支的树状结构图的文本描述:

OpenZeppelin 知识体系树状图

1. 核心模块

  • Contracts(合约库)
    • ERC 标准
      • ERC20
      • ERC721
      • ERC1155
      • ERC777
      • ERC1967Upgrade
    • Governance(治理模块)
      • Governor
      • Timelock
    • Access Control(访问控制)
      • Ownable
      • AccessControl
    • Pausability(暂停功能)
      • Pausable
    • Utility Contracts(实用合约)
      • SafeMath
      • Counters
      • Strings
      • Address
    • Token 相关模块
      • ERC20Votes
      • TokenTimelock
      • TokenVesting
    • Upgradeability(可升级合约)
      • TransparentUpgradeableProxy
      • UUPSUpgradeable
  • Security(安全模块)
    • ReentrancyGuard
    • PullPayment
    • Escrow
  • Cryptography(加密工具)
    • ECDSA
    • MerkleProof
    • SignatureChecker

2. Subgraphs(子图模块)

  • ERC 标准相关
    • ERC20 子图
    • ERC721 子图
    • ERC1155 子图
  • Governance(治理相关)
    • Governor 模块子图
    • Timelock 模块子图
  • Access Control(访问控制相关)
    • AccessControl 子图
    • Ownable 子图

3. 工具库

  • Math
    • SafeMath
    • SignedMath
  • Strings(字符串处理)
    • Base64 编码
    • 字符串转换工具
  • Context(上下文工具)
  • EnumerableSet(枚举集合)
  • EnumerableMap(枚举映射)
  • DoubleEndedQueue(双端队列)

4. 测试工具

  • Test Helpers
    • Address
    • Balance
    • Token
    • Time

5. 部署工具

  • Contracts Wizard(合约向导工具)
    • 模板化生成合约
  • OpenZeppelin Upgrades
    • 可升级合约工具
    • Proxy 管理

6. 文档与学习资源

  • 官方文档
  • OpenZeppelin 学习指南
  • GitHub 示例项目

如何使用这个树状图:

什么是openzepeling

能够减轻你智能合约开发工作量的一个工具集。

准备工作

如果使用的是hardhat,truffle 框架 :

npm install @openzeppelin/contracts

如果是使用的是foundry

forge install OpenZeppelin/openzeppelin-contracts

用法

安装完成之后,可以通过导入库来使用它们

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

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

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

这里有个问题: 可以导入哪些工具?我们在下文进行回答。

交互合约生成

Url : https://wizard.openzeppelin.com/

这个工具可以交互式的生成合约代码。

合约继承

关键字 is , 合约继承是通过 is 关键字来进行继承。

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

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

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

继承之后, 就可以使用父合约的函数和属性。

重写

关键字 overridde

如果需要修改父合约的函数,那么就需要进行重写。重写是针对函数而言的

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 父合约
contract Parent {
    // 虚函数,允许在子合约中重写
    function greet() public virtual returns (string memory) {
        return "Hello from Parent!";
    }
}

// 子合约
contract Child is Parent {
    // 重写父合约中的 greet 函数
    function greet() public override returns (string memory) {
        return "Hello from Child!";
    }
}

调用父方法

super

super 关键字用于在继承的子合约中调用父合约的函数。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Parent {
    function foo() public virtual returns (string memory) {
        return "Parent foo";
    }
}

contract Child is Parent {
    function foo() public virtual override returns (string memory) {
        // 使用 super 调用父合约的 foo 函数
        return string(abi.encodePacked("Child and ", super.foo()));
    }
}

可更新合约

在 OpenZeppelin 中,Upgrades 是指智能合约的可升级性解决方案。由于智能合约一旦部署到区块链上就变得不可更改,任何代码逻辑上的错误或需要的改进都可能导致项目无法正常运作。OpenZeppelin Upgrades 提供了一种机制,使智能合约能够在不改变其合约地址的前提下进行升级,确保合约的状态和数据在升级过程中保留不变。

安装

npm install @openzeppelin/contracts-upgradeable @openzeppelin/contracts

使用

  1. 可以看到, 包的都免都加上了后缀
-import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";

-contract MyCollectible is ERC721 {
+contract MyCollectible is ERC721Upgradeable {
  1. 构造函数都改成了 initializer 函数

在可升级合约中,构造函数(constructor)被替换为初始化函数(initializer),因为在使用代理模式进行合约升级时,代理合约不会执行逻辑合约中的构造函数。因此,合约需要通过初始化函数来完成状态的初始化,而不是像普通合约那样使用构造函数。

-    constructor() ERC721("MyCollectible", "MCO") public {
+    function initialize() initializer public {
+        __ERC721_init("MyCollectible", "MCO");
     }
  1. 使用upgrade 插件跟新
// scripts/deploy-my-collectible.js
const { ethers, upgrades } = require("hardhat");

async function main() {
  const MyCollectible = await ethers.getContractFactory("MyCollectible");

  const mc = await upgrades.deployProxy(MyCollectible);

  await mc.waitForDeployment();
  console.log("MyCollectible deployed to:", await mc.getAddress());
}

main();

Backwards Compatibility

OpenZeppelin 合约使用 语义化版本控制(semantic versioning 来传达其API和存储布局的向后兼容性。一般来说,补丁(patch)和次要(minor)更新将保持向后兼容,除了一些罕见的例外情况(如下所述)。主要(major)更新则通常与先前版本不兼容。本页面提供了这些兼容性保证的详细信息。

Access control 用户控制

这部分内容简单来说就是: 谁能做什么事情。

Ownable

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    constructor(address initialOwner) Ownable(initialOwner) {}

    function normalThing() public {
        // anyone can call this normalThing()
    }

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

步骤:

  1. 导入Ownable合约
  2. Ownable 继承合约
  3. 在构造函数中初始化所有者
  4. 使用 onlyOwner 修饰符来保护需要所有者权限的函数。

基于角色的访问控制(RBAC)的概念

基于所有权的访问控制(如 Ownable)适用于简单的系统,但许多情况下,需要更复杂的授权级别。 基于角色的访问控制(RBAC)允许你为不同角色分配不同的权限,例如“管理员”、“铸币者”、“审查员”等,而不是仅仅依赖于 onlyOwner。 每个角色可以通过 onlyRole 修饰符进行权限检查,确保只有特定角色的账户可以执行相应操作。

假设你正在创建一个ERC20代币合约,并希望不同的角色具有不同的权限:

  1. 管理员角色(DEFAULT_ADMIN_ROLE):能够授予和撤销其他角色的权限。
  2. 铸币者角色(MINTER_ROLE):可以铸造新的代币。
  3. 销毁者角色(BURNER_ROLE):可以销毁代币。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract RoleBasedERC20 is ERC20, AccessControl {
    // 定义两个角色:铸币者和销毁者
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    // 部署合约时,msg.sender 被授予管理员角色
    constructor() ERC20("MyToken", "MTK") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    // 铸币功能,只有被授予 MINTER_ROLE 角色的账户才能调用
    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    // 销毁功能,只有被授予 BURNER_ROLE 角色的账户才能调用
    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }

    // 为新账户授予铸币者角色,只有管理员可以调用
    function grantMinterRole(address account) public {
        grantRole(MINTER_ROLE, account); // 管理员授予铸币者角色
    }

    // 为新账户授予销毁者角色,只有管理员可以调用
    function grantBurnerRole(address account) public {
        grantRole(BURNER_ROLE, account); // 管理员授予销毁者角色
    }

    // 撤销账户的铸币者角色,只有管理员可以调用
    function revokeMinterRole(address account) public {
        revokeRole(MINTER_ROLE, account); // 管理员撤销铸币者角色
    }

    // 撤销账户的销毁者角色,只有管理员可以调用
    function revokeBurnerRole(address account) public {
        revokeRole(BURNER_ROLE, account); // 管理员撤销销毁者角色
    }
}

延迟操作的概念(Delayed Operation)

访问控制对于防止未经授权的访问至关重要,但管理员本身可能会对系统进行恶意操作,导致用户受损。TimelockController 是专门为解决这个问题而设计的。TimelockController 的作用:当 TimelockController 成为某个智能合约的管理员时,它确保任何重要的操作(如铸造代币、冻结转账或升级合约)都需要经过一个延迟。这个延迟为用户提供了时间去审查即将执行的操作,并决定是否要退出系统。

TimelockController 示例及讲解

我们来创建一个带有 TimelockController 的智能合约,展示如何使用它来确保关键操作需要经过延迟执行。我们会结合 ERC20 代币合约,并通过 TimelockController 来控制谁有权提议和执行代币的铸造和销毁操作。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

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

    // 使用 TimelockController 进行控制
    TimelockController public timelock;

    constructor(
        address timelockAddress, 
        address initialMinter, 
        address initialBurner
    ) ERC20("MyToken", "MTK") {
        // 设置 TimelockController 地址
        timelock = TimelockController(timelockAddress);

        // 将合约的 MINTER_ROLE 和 BURNER_ROLE 分配给 TimelockController 进行管理
        _grantRole(MINTER_ROLE, initialMinter);
        _grantRole(BURNER_ROLE, initialBurner);
    }

    // 只有具有 MINTER_ROLE 的账户才能铸造代币
    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    // 只有具有 BURNER_ROLE 的账户才能销毁代币
    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        _burn(from, amount);
    }
}

contract MyTimelockController is TimelockController {
    constructor(
        uint256 minDelay,        // 最小延迟时间(秒)
        address[] memory proposers, // 提议者账户
        address[] memory executors  // 执行者账户
    ) TimelockController(minDelay, proposers, executors) {}
}

代码讲解

1. TimelockController 的基础概念:

  • TimelockController 允许对关键操作(如代币铸造、合约升级等)设置一个延迟时间,在此期间用户可以审查操作内容。如果用户认为即将执行的操作不利于他们,他们可以选择退出系统。
  • 在这个例子中,我们为代币合约的铸造(mint)和销毁(burn)操作设置了权限,并通过 TimelockController 来确保这些操作需要经过延迟执行。

2. 合约结构:

  • TimelockControlledToken:这是一个 ERC20 代币合约,通过 AccessControl 来管理权限。
    • 定义了两个角色:MINTER_ROLE(铸币者)和 BURNER_ROLE(销毁者),分别控制代币的铸造和销毁操作。
    • 这些角色被授予给 TimelockController 进行管理,确保任何代币铸造或销毁操作都需要遵守延迟规则。
  • MyTimelockController:这是 TimelockController 的具体实现,用来设定延迟时间以及哪些账户可以提出提议或执行操作。

3. TimelockControlledToken 的工作流程:

  • 设置 TimelockController:合约的构造函数中接收了一个 timelockAddress,这是 TimelockController 的地址,用于管理合约的关键操作。这个 TimelockController 拥有 MINTER_ROLEBURNER_ROLE 的权限。
  • 角色分配:
    • 通过 _grantRole 函数,将铸币者和销毁者的角色分配给传入的初始地址。
    • 这些角色会通过 TimelockController 来提议和执行操作。

4. TimelockController 的工作流程:

  • 延迟设置:当 TimelockController 被配置为管理者时,任何需要执行的操作都会经过延迟,用户可以在延迟期间审核这些操作。
  • 提议者(proposer)和执行者(executor):在 TimelockController 中,有两个重要角色:
    • 提议者(proposer):可以提出需要延迟执行的操作。
    • 执行者(executor):在延迟结束后,执行提议的操作。

5. MyTimelockController 的构造函数:

  • MyTimelockController 继承自 TimelockController,它的构造函数接收三个参数:
    • minDelay:定义延迟的最小时间(以秒为单位)。
    • proposers:一组具有提议权限的账户地址。
    • executors:一组具有执行权限的账户地址。
  • 例如,你可以设定一个提议者(例如DAO或多签钱包)和多个执行者(负责执行提议的个人账户)。

6. 操作流程:

  • 提议:当某个提议者提出一项代币铸造或销毁操作时,操作不会立即执行,而是需要经过一段延迟。
  • 执行:当延迟结束后,执行者才能执行操作。
  • 通过这种方式,系统确保任何关键操作在执行前有足够的时间供用户进行审查,从而增加透明度和安全性。

运行流程示例:

  1. 部署 MyTimelockController
    • 你可以设置一个最小延迟时间(例如7天),并指定哪些账户是提议者,哪些账户是执行者。
    // 部署 TimelockController,延迟时间为7天
    address[] memory proposers = [address(0x123)];
    address[] memory executors = [address(0x456)];
    MyTimelockController timelock = new MyTimelockController(7 days, proposers, executors);
  2. 部署 TimelockControlledToken
    • timelock 作为代币的管理者,同时指定初始的铸币者和销毁者。
    TimelockControlledToken token = new TimelockControlledToken(address(timelock), minterAddress, burnerAddress);
  3. 提交操作:
    • 提议者可以通过 TimelockController 提出铸造或销毁代币的操作。
    • 执行者在延迟结束后,可以执行提议的操作。

总结:

  • TimelockController 的作用:它引入了一种延迟机制,确保系统的关键操作不会立即执行,给用户足够的时间去审查和退出系统,防止管理员恶意操作。
  • 提议者和执行者:TimelockController 通过提议者和执行者的角色分离来管理操作的执行(通过写合约来实现),并允许用户对操作进行提前审查。

Access Management

AccessManager 是一个集中管理合约权限的工具,设计用来简化多个合约的权限管理。通常,在每个合约中单独使用 AccessControl 来管理权限会增加系统的复杂性。而 AccessManager 允许你将所有权限集中在一个合约中进行管理,从而减少管理难度,并提高审计和维护的效率。

AccessManager 基于“角色”和“目标函数”的概念设计:

角色:多个账户可以拥有相同的角色,一个账户也可以拥有多个角色。每个角色通过数值(uint64)来标识,而不是像 AccessControl 那样使用 bytes32 类型。 目标函数:目标函数是合约中受限制的函数,只有具有特定角色的账户才能调用。 在 AccessManager 中,只有被授予角色的账户才能调用与角色相关的目标函数。调用者必须具备与目标函数相匹配的角色才能被授权执行该操作。

await manager.setTargetFunctionRole(
    tokenContractAddress,   // 目标合约地址(如 AccessManagedERC20Mint 的合约地址)
    ['0x40c10f19'],         // 函数选择器:mint(address,uint256)
    MINTER                  // 角色:MINTER_ROLE
);

函数选择器这个概念需要另起说明。

Tokens

Token 是区块链中的一个表示,可以代表各种东西,如金钱、时间、服务、公司股份、虚拟宠物等。通过将这些事物表示为token,智能合约能够与它们交互、交换、创建或销毁。

  1. Token合同与Token的区别
  1. 可替代性(Fungibility)
  1. Token的标准(Standards)

由于以太坊上的一切都是智能合约,智能合约没有强制规则,社区为token开发了一些标准,以便不同的智能合约之间能够互操作。 常见的token标准包括:

ERC 代表 Ethereum Request for Comment,即以太坊请求意见书。

这些数字本身没有特别的技术含义,只是方便我们识别每个标准的编号。

ERC20

ERC20代币合约主要用于记录和管理可替代的代币,这意味着每个代币彼此完全相同,没有特殊的权利或行为。这种特性使ERC20代币非常适合用于交换货币、投票权、质押等场景。OpenZeppelin 提供了许多与ERC20相关的合约,这些合约使我们可以轻松地创建和管理自己的ERC20代币合约。

通过使用OpenZeppelin的合约,我们可以很容易地创建自己的ERC20代币合约。下方展示了一个GLD(Gold)的代币合约代码示例,该代币将被用作假想游戏中的内部货币。

// contracts/GLDToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract GLDToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("Gold", "GLD") {
        _mint(msg.sender, initialSupply);
    }
}

这段代码中,通过构造函数constructor设置代币的名称为“Gold”(黄金),符号为“GLD”,并指定初始供应量initialSupply。_mint函数用于铸造初始供应量的代币,并将它们分配给部署合约的地址。

一旦部署合约后,你可以使用以下命令查询代币的余额和转移代币:

ERC 20的供应机制

ERC20 是一种广泛使用的以太坊代币标准,OpenZeppelin 提供了其实现。标准的ERC20合约本身并未定义如何生成供应量,因此在部署默认ERC20合约时,代币是没有供应的。每个代币可以定义自己的供应机制,从最中心化到最去中心化,具体实现方式非常灵活。

固定供应

假设我们想要创建一个具有固定供应量的代币,比如 1000 个代币,初始供应量分配给部署合约的账户。

早期的合约(如Contracts v1)可能会直接修改 totalSupply 和 balances 变量来创建固定供应量:

contract ERC20FixedSupply is ERC20 {
    constructor() {
        totalSupply += 1000;
        balances[msg.sender] += 1000;
    }
}

但从OpenZeppelin Contracts v2开始,totalSupply 和 balances 被设为私有,不能直接修改。代替的方法是使用 _mint 函数,它可以安全地创建供应并同步更新。

contract ERC20FixedSupply is ERC20 {
    constructor() ERC20("Fixed", "FIX") {
        _mint(msg.sender, 1000);
    }
}

这种方式更安全,因为 _mint 会自动处理 totalSupply 和 balances 的同步更新,并触发标准要求的 Transfer 事件,防止忘记这些操作。

奖励矿工

我们可以扩展 ERC20 的供应机制,为生产区块的矿工提供代币奖励。通过 Solidity 的 block.coinbase 变量,可以访问当前区块的矿工地址。每次调用 mintMinerReward() 函数时,会铸造1000个代币作为奖励发送给矿工。

contract ERC20WithMinerReward is ERC20 {
    constructor() ERC20("Reward", "RWD") {}

    function mintMinerReward() public {
        _mint(block.coinbase, 1000);
    }
}

自动矿工奖励

我们可以在每次代币转账时自动触发矿工奖励。通过扩展 _update 函数,可以在每个代币转账操作时自动为矿工铸造奖励代币。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20WithAutoMinerReward is ERC20 {
    constructor() ERC20("Reward", "RWD") {
        _mintMinerReward();
    }

    function _mintMinerReward() internal {
        _mint(block.coinbase, 1000);
    }

    function _update(address from, address to, uint256 value) internal virtual override {
        if (!(from == address(0) && to == block.coinbase)) {
            _mintMinerReward();
        }
        super._update(from, to, value);
    }
}

ERC 721

ERC721 是用于创建非同质化代币(NFT)的标准,每个代币都是唯一的,不能互换,适用于具有独特属性的资产,如游戏物品、收藏品等。下方代码展示了如何使用ERC721标准创建一个游戏物品(GameItem)合约,每个物品都有自己的独特属性和元数据。

// contracts/GameItem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract GameItem is ERC721URIStorage {
    uint256 private _nextTokenId;

    constructor() ERC721("GameItem", "ITM") {}

    function awardItem(address player, string memory tokenURI)
        public
        returns (uint256)
    {
        uint256 tokenId = _nextTokenId++;
        _mint(player, tokenId);
        _setTokenURI(tokenId, tokenURI);

        return tokenId;
    }
}

元数据

tokenURI 指向一个 JSON 文件,描述了每个代币的属性。这个 URI 通常是一个存储在外部服务器上的链接,指向代币的详细信息,例如图像、描述和自定义属性。

{
    "name": "Thor's hammer",
    "description": "Mjölnir, the legendary hammer of the Norse god of thunder.",
    "image": "https://game.example/item-id-8u5h2m.png",
    "strength": 20
}

核心功能

如何使用

ERC1155

ERC1155 是一种新型的多代币标准,它结合了 ERC20、ERC721 和 ERC777 的优点,旨在创建一种对代币可替代性不敏感并且Gas 效率高的合约。该标准允许在一个智能合约中表示多种代币,既可以支持可替代代币(如游戏内的货币),也可以支持非同质化代币(如独特的物品或收藏品)。

ERC1155 的特点

  • 多代币标准:ERC1155 的一个显著特征是,它通过一个合约管理多个代币类型。每个代币有唯一的 ID,你可以通过 balanceOf 查询某个地址持有的某个代币 ID 的余额。
  • 代币可替代性:不同于 ERC721,ERC1155 可以同时表示可替代和不可替代的代币。可替代代币通过数量实现(如游戏金币),而不可替代代币只铸造一个(如唯一的物品)。
  • 批量操作(Batch Operations):ERC1155 允许在一个交易中批量操作多个代币,这大大减少了交易中的 Gas 费用。批量查询余额和批量转移代币通过 balanceOfBatch 和 safeBatchTransferFrom 实现。
// contracts/GameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract GameItems is ERC1155 {
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;
    uint256 public constant THORS_HAMMER = 2;
    uint256 public constant SWORD = 3;
    uint256 public constant SHIELD = 4;

    constructor() ERC1155("https://game.example/api/item/{id}.json") {
        _mint(msg.sender, GOLD, 1018, "");
        _mint(msg.sender, SILVER, 1027, "");
        _mint(msg.sender, THORS_HAMMER, 1, "");
        _mint(msg.sender, SWORD, 109, "");
        _mint(msg.sender, SHIELD, 109, "");
    }
}
  • 代币类型:该合约定义了多个代币,如 GOLD(可替代)、THORS_HAMMER(非同质化)等。通过 uint256 常量为每个代币分配一个唯一的 ID。
  • 元数据:ERC1155 的构造函数中定义了代币的 tokenURI 模板,代币的具体元数据(如图像、描述等)可以通过替换 {id} 来获取。
  • 铸造代币:在合约的构造函数中,初始代币被铸造并分配给合约的部署者。每种代币都有各自的数量。

关键操作

  1. 查询余额:你可以通过 balanceOf 查询某个地址持有的某个代币 ID 的余额:
> gameItems.balanceOf(deployerAddress,3) // 查询持有 SWORD 代币的数量
1000000000
  1. 转移代币:通过 safeTransferFrom 转移代币,指定代币 ID 和数量:
> gameItems.safeTransferFrom(deployerAddress, playerAddress, 2, 1, "0x0") // 转移 1 个 THORS_HAMMER
  1. 批量转移:通过 safeBatchTransferFrom 可以批量转移多个代币
> gameItems.safeBatchTransferFrom(deployerAddress, playerAddress, [0,1,3,4], [50,100,1,1], "0x0") // 批量转移多个代币
  1. 查询批量余额:通过 balanceOfBatch 批量查询多个地址、多个代币的余额:
> gameItems.balanceOfBatch([playerAddress,playerAddress,playerAddress,playerAddress,playerAddress], [0,1,2,3,4])
[50,100,1,1,1]
  1. 元数据查询:通过 uri 函数获取代币的元数据 URI。{id} 会被替换为代币的 ID(以 64 位的十六进制形式表示),指向代币的具体元数据文件。
> gameItems.uri(2) // 获取 Thor's Hammer 的元数据 URI
"https://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json"

由于 ERC1155 允许通过一个合约管理多个代币类型,并且提供了批量操作功能,因此在处理多个代币时,ERC1155 可以极大地减少 Gas 消耗。例如,批量转移多个代币只需发起一笔交易,相比于单独调用每个代币的转移操作,大幅降低了交易成本。

代币的可替代性由其供应量和用途决定,供应量为1且独特的代币是 NFT。供应量大于1且每个代币相同的则是 虚拟货币。

ERC4626

ERC4626 是 ERC20 标准的扩展,专门用于代币化金库(tokenized vaults)。在 ERC4626 中,用户可以存入基础资产(如 ETH、USDC),并收到对应的“shares”(股份)。这些股份代表了用户在金库中持有的份额,用户可以通过销毁这些股份来赎回他们的资产。ERC4626 的标准接口为各种不同的合约(如借贷市场、聚合器、和自带利息的代币)提供了统一的交互方式。

通胀攻击问题

通胀攻击(Inflation Attack)是 ERC4626 金库面临的潜在安全问题之一。它利用了舍入误差和金库中股份与资产的兑换率变化,攻击者可以通过操纵资产与股份的比率,使其他用户的存款无法得到应有的股份,从而导致用户损失资产。

  1. 攻击者存入少量资产:攻击者首先存入一小部分资产,这使得他们成为金库中的唯一持有人。
  2. 攻击者直接捐赠大量资产:随后,攻击者直接将大量资产捐赠给金库,而不铸造新的股份。这导致金库的资产与股份的比率严重失衡(即资产数量激增但股份数量不变),使得新的存款无法获得足够的股份。
  3. 用户存款受损:当其他用户存入少量资产时,由于股份的比率被攻击者的操作扭曲,用户可能得到的股份非常少甚至为零,从而造成他们的资产损失。

防御机制:虚拟偏移

为防止通胀攻击,ERC4626 可以使用虚拟偏移(Virtual Offset)的策略。这种方法通过在计算股份与资产比率时引入虚拟的资产和股份来增加安全性。

防御机制的两部分: - 精度偏移:在表示股份和资产时,使用更高的精度来减少计算时的舍入误差。 - 虚拟资产和股份:在金库刚开始为空时,使用虚拟的资产和股份来维护转换率,避免因为攻击者操控比率造成的影响。 通过这种方式,攻击者即使进行了大量捐赠,仍然无法通过操纵比率来导致用户存款的损失。

各种合约的使用场景

这四种标准(ERC20、ERC721、ERC1155 和 ERC4626)各自适用于不同的应用场景,具体如下:

1. ERC20: 可替代代币标准

应用场景: - 虚拟货币:ERC20 是目前最常用的可替代代币标准,适用于表示像 ETH、USDT、DAI 这样的虚拟货币。这些代币可以互相替代,且每个单位都具有相同的价值。 - 去中心化金融(DeFi):ERC20 代币被广泛用于 DeFi 应用中,包括借贷平台、流动性挖矿、收益聚合器等。用户可以用 ERC20 代币进行交易、抵押、借贷和流动性提供。 - 支付系统:ERC20 代币可以作为支付手段,用于商品或服务的购买。这些代币是可替代的,因此非常适合支付场景。

典型应用: - 虚拟货币:USDT、DAI、UNI、LINK 等。 - DeFi 平台:Aave、Compound、Uniswap 等平台都使用 ERC20 代币进行流动性提供和交易。

2. ERC721: 非同质化代币(NFT)标准

应用场景: - 数字收藏品和艺术品:ERC721 标准用于表示每个代币都是独一无二的,如 CryptoKitties、数字艺术品(如 Beeple 的作品)等。每个代币都有其独特的属性和价值。 - 虚拟地产:在 Decentraland、The Sandbox 等元宇宙平台中,虚拟土地使用 ERC721 标准,因为每块土地都是独特的、不可替代的。 - 游戏道具:在区块链游戏中,ERC721 代币可以表示独特的游戏道具,如角色、装备等,这些物品不可互换。

典型应用: - NFT 项目:CryptoPunks、Bored Ape Yacht Club、Art Blocks 等。 - 元宇宙项目:Decentraland、The Sandbox 等。

3. ERC1155: 多代币标准

应用场景: - 游戏道具和资产:ERC1155 允许同时管理可替代和不可替代的代币,非常适合区块链游戏。在游戏中,像金币(可替代的)和独特武器(不可替代的)可以通过一个合约来管理,极大节省了 Gas 费用。 - 批量交易和管理:ERC1155 支持批量转移多个代币,因此特别适合那些需要一次性处理多个代币交易的场景,如游戏内奖励发放或批量交易收藏品。 - 数字收藏品:ERC1155 可以用于管理同一系列中的多个收藏品,每个代币可以是独一无二的(NFT),也可以是相同的(如游戏代币)。

典型应用: - 区块链游戏:Gods Unchained、Enjin、Axie Infinity 等。 - 批量交易收藏品:支持批量发行和交易的数字收藏品平台。

4. ERC4626: 金库代币标准

应用场景: - 收益聚合器和金库:ERC4626 是专门为代币化金库设计的标准,它允许用户将资产存入金库并获得相应的股份,股份代表了金库中的资产份额。它适用于收益聚合器(如 Yearn Finance)等场景,用户可以存入资产赚取利息或其他收益。 - 借贷平台:ERC4626 可以用于借贷市场,允许用户存入抵押资产并获得股份,股份的价值会随金库中资产的变化而变化。 - 自动化收益策略:用户可以通过存入资产参与 DeFi 策略,金库会自动执行收益最大化的策略,并按比例分发给所有股份持有人。

典型应用: - Yearn Finance:用户存入 ERC20 代币,金库会自动将资金分配给各种收益策略并返还股份。 - 借贷市场:Compound、Aave 等平台可以基于 ERC4626 来管理用户的存款和利息分配。

总结表格:

标准 描述 应用场景 典型应用
ERC20 可替代代币标准 虚拟货币、支付系统、DeFi 应用、流动性提供 USDT、DAI、Uniswap、Aave
ERC721 非同质化代币(NFT)标准 数字艺术品、收藏品、游戏道具、虚拟地产 CryptoKitties、Decentraland
ERC1155 多代币标准,可同时支持可替代和不可替代代币 游戏资产管理、批量转移、数字收藏品 Gods Unchained、Enjin
ERC4626 金库代币标准 收益聚合器、借贷市场、自动化收益策略 Yearn Finance、借贷平台

通过这些标准,你可以根据不同的应用场景选择合适的代币标准进行开发。

governance

在 OpenZeppelin 中,“governance”(治理)是指通过智能合约实现去中心化组织或项目的决策机制和管理流程。治理模型允许持有代币的用户(通常是项目的利益相关者)参与项目的管理和决策,比如升级合约、修改参数、分配资金等。OpenZeppelin 提供的治理工具是为了帮助开发者方便地实施去中心化治理机制,使项目能够在无需依赖单一中心化实体的情况下进行管理和演化。

OpenZeppelin 中,“governance”(治理)是指通过智能合约实现去中心化组织或项目的决策机制和管理流程。治理模型允许持有代币的用户(通常是项目的利益相关者)参与项目的管理和决策,比如升级合约、修改参数、分配资金等。OpenZeppelin 提供的治理工具是为了帮助开发者方便地实施去中心化治理机制,使项目能够在无需依赖单一中心化实体的情况下进行管理和演化。

OpenZeppelin Governance 模块的主要功能和组件

  1. 提案与投票机制(Proposals and Voting)
    • 提案(Proposal):持有治理代币的用户可以发起提案,建议对项目进行某些更改,比如修改某个合约的参数,或者分配资金。每个提案包含执行这些更改的具体步骤。
    • 投票(Voting):项目中的代币持有者可以对提案进行投票。通常,投票权与用户所持有的治理代币数量成正比,也就是说持有更多代币的用户有更大的投票权。
    • 投票方式:OpenZeppelin 提供了多种投票方式,包括简单多数制投票、加权投票、时间锁投票等,确保治理过程的灵活性和透明度。
  2. 执行决策(Execution of Decisions)
    • 一旦提案通过,系统会执行提案中规定的动作。OpenZeppelin Governance 模块允许用户将投票结果自动转化为链上操作,如调用合约函数、转移代币等。这种自动化执行的功能是区块链治理的重要特性,避免了中心化机构的干预。
  3. 治理代币(Governance Tokens)
    • 治理代币是治理机制的核心,持有这些代币的用户可以参与项目的管理决策。通过持有代币,用户可以获得提案权和投票权。这些代币通常是ERC20标准的代币。
    • 有时,代币的投票权还会根据锁仓时间等因素进行调整,持有时间越长,投票权越大。
  4. 时间锁(Timelocks)
    • 时间锁 是治理中的一个关键机制,用于确保在某个决策被批准后,执行操作前有一定的延迟时间。这段时间允许社区成员审查和讨论即将执行的提案,确保安全性,防止突然的恶意更改。
    • 时间锁还可以为治理提供透明度,允许所有利益相关者在提案生效前看到结果。
  5. 代理投票(Delegation)
    • OpenZeppelin 的治理模块允许代币持有者将他们的投票权委托给其他人。这个功能对于那些不想频繁参与日常治理活动的用户非常重要,因为他们可以将自己的投票权转移给他们信任的人。
  6. 治理模块的扩展性
    • OpenZeppelin 的治理模块是可扩展的,你可以根据项目的需求调整治理逻辑、投票规则、时间锁设置等。开发者可以使用OpenZeppelin的合约基础,创建符合项目需求的治理机制。

使用场景

OpenZeppelin 的治理模块通常用于去中心化自治组织(DAO),DeFi 项目,和其他依赖社区决策的区块链项目。以下是一些典型的使用场景: - 去中心化金融(DeFi)项目:比如在一个DeFi协议中,治理代币持有者可以通过提案决定协议的参数修改,流动性奖励分配,或者资金池的管理。 - 协议升级:当区块链协议需要升级时,治理模块允许社区投票决定是否接受新的合约部署。 - 资金管理:DAO项目中,社区成员可以通过提案和投票决定如何使用项目的资金,或者如何激励开发者。

总结:

OpenZeppelin 的 Governance 模块提供了一种可靠和灵活的机制,使项目能够通过去中心化的方式管理自身。它通过提案、投票、时间锁等方式,确保了治理过程的透明性和社区参与度。治理代币持有者可以对项目的未来进行表决,并通过智能合约直接执行这些决策,使治理过程公开、公平且高效。

这个治理模块非常适合任何希望实施去中心化管理的项目,尤其是DAO或去中心化金融(DeFi)协议。

实用工具

这段代码主要介绍了 OpenZeppelin Contracts 提供的实用工具,这些工具可以帮助你在项目中简化区块链开发过程。以下是梳理后的中文解释:

Cryptography(密码学)

1. 签名验证(Checking Signatures On-Chain): - ECDSA 用于恢复和管理以太坊账户的ECDSA签名。签名通常通过 web3.eth.sign 生成,是一个包含65字节数组的签名,结构为 [v, r, s]。 - 通过 ECDSA.recover 可以恢复签名者的地址并与目标地址进行对比,验证签名的合法性。签名验证时,需要使用 toEthSignedMessageHash 方法处理带有以太坊前缀的签名数据。

using ECDSA for bytes32;
using MessageHashUtils for bytes32;

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

Verifying Merkle Proofs(验证Merkle证明)

  • MerkleProof 提供以下方法:
    • verify:证明某个值属于Merkle树的一部分。
    • multiProofVerify:证明多个值属于Merkle树的一部分。

Introspection(接口检测)

  • ERC165 是一个标准,用于在运行时检测合约是否支持某个接口。
    • IERC165 定义了 supportsInterface 方法。
    • ERC165 可以继承该合约实现接口检测功能。
    • ERC165Checker 简化了检查合约是否支持特定接口的过程。
using ERC165Checker for address;

myAddress._supportsInterface(bytes4)
myAddress._supportsAllInterfaces(bytes4[])

contract MyContract {
    using ERC165Checker for address;
    bytes4 private InterfaceId_ERC721 = 0x80ac58cd;

    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 提供了 MathSignedMath 库,用于处理额外的数学运算,如求平均值。它还支持有符号整数的运算。
contract MyContract {
    using Math for uint256;
    using SignedMath for int256;

    function tryOperations(uint256 a, uint256 b) internal pure {
        (bool overflowsAdd, uint256 resultAdd) = x.tryAdd(y);
        (bool overflowsSub, uint256 resultSub) = x.trySub(y);
        (bool overflowsMul, uint256 resultMul) = x.tryMul(y);
        (bool overflowsDiv, uint256 resultDiv) = x.tryDiv(y);
    }

    function unsignedAverage(int256 a, int256 b) {
        int256 avg = a.average(b);
    }
}

Structures(数据结构)

OpenZeppelin 提供增强的数据结构: - BitMaps:用于存储布尔值。 - Checkpoints:记录并查找检查点的值。 - DoubleEndedQueue:支持队列操作的双端队列。 - EnumerableSet/EnumerableMap:支持枚举操作的集合和映射,可以轻松查询链上和链下的存储内容。

Misc(杂项)

Base64: - 将 bytes32 数据转为 Base64 字符串,适用于ERC721或ERC1155中生成URL安全的 tokenURI

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Base64} from "@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(), '"',
            '}'
        );
        return string(
            abi.encodePacked(
                "data:application/json;base64,",
                Base64.encode(dataURI)
            )
        );
    }
}

Multicall(多重调用)

  • Multicall 合约允许将多个调用打包成一个外部调用,方便外部账户在一次交易中进行多个操作。
import "@openzeppelin/contracts/utils/Multicall.sol";

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

使用 multicall 函数进行批量调用的示例:

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

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

总结:

OpenZeppelin 提供了大量的实用工具和库,包括加密、数学操作、接口检测、数据结构以及多重调用等。这些工具帮助开发者快速、安全地开发区块链应用,减少代码冗余并提升效率。

Subgraphs

OpenZeppelin Subgraphs 是一种帮助你索引和查询区块链数据的工具,主要用于自动记录和分析区块链上的合约活动。它的作用是在区块链上发生的事件(如交易、智能合约调用等)被触发时,通过子图(subgraph)记录这些活动,并将数据存储到数据库中,以便后续可以通过 GraphQL 查询这些数据。

简化理解一下: 1. 区块链事件监听:当区块链上某个合约(比如 ERC20 代币合约)发生了事件,比如代币转移或合约调用,Subgraph 会监听到这些事件。

  1. 数据存储和查询:Subgraph 把这些事件的数据索引到数据库中,并且你可以用类似于查询数据库的方式(使用 GraphQL)来获取这些数据。

  2. 自动化索引:OpenZeppelin Subgraphs 提供现成的模板和工具,简化了你去编写和手动处理这些索引逻辑的工作。你只需要配置好合约地址、事件等,子图会自动帮你处理数据的采集和存储。

实际用途:

  • 区块链项目监控:如果你开发了一个基于区块链的项目(比如发行代币、部署智能合约等),你可能想实时了解项目上的合约运行情况,比如查询所有的代币转移记录、智能合约执行情况等,Subgraph 可以自动帮你把这些活动整理成数据,并提供接口让你轻松查询。

  • 去中心化应用(DApp)数据查询:你可以使用 Subgraph 记录和查询你 DApp 的用户行为、代币余额变化等操作,方便在前端展示信息。

简而言之,Subgraph 就是一个自动化工具,帮助你记录和查询区块链合约活动的数据,让你能够轻松访问和分析这些数据,而不必自己编写复杂的监听和处理代码。

附录

  1. OpenZeppelin :https://rpubs.com/liam/1202700
  2. https://docs.openzeppelin.com/contracts/5.x/