首页
学习
活动
专区
圈层
工具
发布
49 篇文章
1
《纸上谈兵·solidity》第 0 课:搭建 Solidity 开发环境(三种方式)
2
《纸上谈兵·solidity》第 1 课:部署你的第一个 Solidity 合约
3
《纸上谈兵·solidity》第 2 课:调用、修改、读取,Solidity 合约不是 REST API
4
《纸上谈兵·solidity》第 3 课:事件(Event)机制与链上日志——不是 print,是广播!
5
《纸上谈兵·solidity》第 4 课:Solidity 合约中的错误处理机制(`require`、`revert`、`assert`)和自定义错误
6
《纸上谈兵·solidity》第 5 课:依赖与外部调用 —— 合约交互的风险与防护
7
《纸上谈兵·solidity》第 6 课:Solidity 数据存储布局 —— memory、storage、calldata 傻傻分不清?
8
《纸上谈兵·solidity》第 7 课:Solidity 函数可见性和修饰器 —— public 和 private 不只是权限标签
9
《纸上谈兵·solidity》第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口
10
《纸上谈兵·solidity》第 9 课:Solidity 事件与日志机制 —— 合约世界的“printf”工具
11
《纸上谈兵·solidity》第 10 课:Solidity `fallback` / `receive` 函数 —— 合约如何收 ETH 和响应未知调用?
12
《纸上谈兵·solidity》第 11 课:Solidity 错误处理与异常机制 —— 让合约优雅地失败
13
《纸上谈兵·solidity》第 12 课:Solidity 函数选择器与 ABI 编码原理
14
《纸上谈兵·solidity》第 13 课:Solidity 低级调用 call/delegatecall/staticcall —— 直接和 EVM“对话”
15
《纸上谈兵·solidity》第 14 课:Solidity 中的可升级合约模式 —— 从代理合约到透明代理、UUPS 与安全陷阱
16
《纸上谈兵·solidity》第 15 课:Solidity 库与可重用代码
17
《纸上谈兵·solidity》第 16 课:Pull over Push 支付模式与 Check-Effects-Interactions 原则
18
《纸上谈兵·solidity》第 17 课:合约设计模式实战(二)—— Access Control 与权限管理
19
《纸上谈兵·solidity》第 18 课:合约设计模式实战(三)—— 代理 + 插件化架构(Diamond Standard / EIP-2535)
20
《纸上谈兵·solidity》第 19 课:安全专题(一)—— 常见攻击手法与防御
21
《纸上谈兵·solidity》第 20 课:Solidity 安全专题(二)—— 编译器特性与低级漏洞
22
《纸上谈兵·solidity》第 21 课:Gas 优化与成本分析 —— 写出便宜的智能合约
23
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
24
《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战
25
《纸上谈兵·solidity》第 24 课:去中心化众筹合约(Crowdfunding)实战
26
《纸上谈兵·solidity》第 25 课:简化版的去中心化交易所(DEX)简化版
27
《纸上谈兵·solidity》第 26 课:借贷合约简化实现
28
《纸上谈兵·solidity》第 27 课:DAO 治理合约(去中心化自治组织)
29
《纸上谈兵·solidity》第 28 课:智能合约安全审计案例复盘 -- The DAO Hack(2016)
30
《纸上谈兵·solidity》第 29 课:智能合约安全审计案例复盘 -- Parity Wallet Hack(2017)
31
《纸上谈兵·solidity》第 30 课:智能合约安全审计案例复盘 -- Nomad Bridge(2022)
32
《纸上谈兵·solidity》第 31 课:多签钱包在跨链桥中的应用 —— Nomad 事件复盘
33
《纸上谈兵·solidity》第 32 课:DeFi 基础合约
34
《纸上谈兵·solidity》第 33 课:多签钱包(Multisig Wallet)-- 合约设计与实现
35
《纸上谈兵·solidity》第 34 课:多签钱包(Multisig Wallet)-- 上线
36
《纸上谈兵·solidity》第 35 课:去中心化交易所(DEX)实战 — 合约设计
37
《纸上谈兵·solidity》第 36 课:去中心化交易所(DEX)实战 — 上线
38
《纸上谈兵·solidity》第 37 课:DeFi 实战 -- 资金池与利率模型
39
《纸上谈兵·solidity》第 38 课:DeFi 实战(2) -- 清算机制与价格预言机
40
《纸上谈兵·solidity》第 39 课:DeFi 实战(3) -- 利息累积与 aToken 设计
41
《纸上谈兵·solidity》第 40 课:DeFi 实战(4) -- 风险控制与防护
42
《纸上谈兵·solidity》第 41 课:DeFi 实战(5) -- 协议费与治理
43
《纸上谈兵·solidity》第 42 课:DeFi 实战(6) -- 跨资产借贷与多市场支持
44
《纸上谈兵·solidity》第 43 课:DeFi 实战(7) -- 清算机制进阶(多资产抵押清算路径、拍卖机制)
45
《纸上谈兵·solidity》第 44 课:DeFi 实战(8) -- 利率曲线与资金池优化(动态利用率模型)
46
《纸上谈兵·solidity》第 45 课:DeFi 实战(9) -- 利息累积与结算机制(可复利)
47
《纸上谈兵·solidity》第 46 课:DeFi 实战(10) -- 跨链借贷与流动性桥接
48
《纸上谈兵·solidity》第 47 课:DeFi 实战(11) -- 治理代币 & 激励机制(Tokenomics & Governance)
49
《纸上谈兵·solidity》第 48 课:DeFi 实战(12) -- 前端 DApp 集成与用户交互(React + ethers.js 实战)

《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战

1、学习目标

  • 理解 ERC721 与 ERC1155 的标准接口
  • 从零实现一个 最小化 ERC721(NFT)合约
  • 扩展功能:元数据管理(BaseURI)、批量铸造 / 批量转账
  • 对比 OpenZeppelin 实现
  • 使用 Foundry 测试

2、知识点梳理

  1. ERC721 核心接口
    • balanceOf(address)
    • ownerOf(uint256)
    • safeTransferFrom(address,address,uint256)
    • transferFrom(address,address,uint256)
    • approve(address,uint256) / setApprovalForAll(address,bool)
    • 事件:Transfer, Approval, ApprovalForAll
  2. ERC1155 核心接口
    • 支持 多代币标准(FT / NFT / SFT)
    • balanceOf(address,uint256)
    • safeTransferFrom(address,address,uint256,uint256,bytes)
    • safeBatchTransferFrom(...)
    • 事件:TransferSingle, TransferBatch, ApprovalForAll
  3. 应用场景差异
    • ERC721 → 独一无二的资产(头像、土地、艺术品)
    • ERC1155 → 大规模批量资产(游戏道具、门票、盲盒)

3、最小 ERC721 实现

MyERC721.sol

代码语言:solidity
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyERC721 {
    // NFT 名称和符号
    string public name = "MyNFT";
    string public symbol = "MNFT";

    // 记录每个 tokenId 的所有者
    mapping(uint256 => address) private _owners;
    // 记录每个地址拥有多少 NFT
    mapping(address => uint256) private _balances;
    // 每个 tokenId 的单独授权(一个地址可被允许转移这个 tokenId)
    mapping(uint256 => address) private _tokenApprovals;
    // 授权某个地址操作所有 token(批量授权)
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // 事件:转账、授权、批量授权
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    // 查询某个地址持有多少 NFT
    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "zero address");
        return _balances[owner];
    }

    // 查询某个 tokenId 的所有者
    function ownerOf(uint256 tokenId) public view returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "not minted");
        return owner;
    }

    // 授权某个地址可以转移指定的 tokenId
    function approve(address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner, "not owner");
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    // 查询某个 tokenId 被授权给了谁
    function getApproved(uint256 tokenId) public view returns (address) {
        return _tokenApprovals[tokenId];
    }

    // 批量授权:允许某个 operator 管理 msg.sender 所有的 token
    function setApprovalForAll(address operator, bool approved) public {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    // 转移 NFT
    function transferFrom(address from, address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);

        // 检查调用者是否有权限(是所有者 / 被单独授权 / 被批量授权)
        require(
            msg.sender == owner ||
            msg.sender == _tokenApprovals[tokenId] ||
            _operatorApprovals[owner][msg.sender],
            "not authorized"
        );
        require(from == owner, "wrong from");
        require(to != address(0), "zero address");

        // 更新余额
        _balances[from] -= 1;
        _balances[to] += 1;
        // 更新所有者
        _owners[tokenId] = to;

        // 清除旧授权
        delete _tokenApprovals[tokenId];
        emit Transfer(from, to, tokenId);
    }

    // 内部函数:铸造 NFT
    function _mint(address to, uint256 tokenId) internal {
        require(to != address(0), "zero address");
        require(_owners[tokenId] == address(0), "already minted");

        _balances[to] += 1;
        _owners[tokenId] = to;
        emit Transfer(address(0), to, tokenId);
    }
}

4、简化 ERC1155 实现

MyERC1155.sol

代码语言:solidity
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyERC1155 {
    // balances[tokenId][owner] = 持有数量
    mapping(uint256 => mapping(address => uint256)) private balances;
    // 操作授权
    mapping(address => mapping(address => bool)) private operatorApprovals;

    // ERC1155 事件(单次转账 & 批量转账)
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
    event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    // 查询余额
    function balanceOf(address account, uint256 id) public view returns (uint256) {
        return balances[id][account];
    }

    // 批量查询余额
    function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view returns (uint256[] memory) {
        require(accounts.length == ids.length, "length mismatch");
        uint256[] memory batchBalances = new uint256[](accounts.length);
        for (uint i = 0; i < accounts.length; i++) {
            batchBalances[i] = balances[ids[i]][accounts[i]];
        }
        return batchBalances;
    }

    // 设置批量操作授权
    function setApprovalForAll(address operator, bool approved) public {
        operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    // 铸造代币
    function mint(address to, uint256 id, uint256 amount) public {
        balances[id][to] += amount;
        emit TransferSingle(msg.sender, address(0), to, id, amount);
    }

    // 批量铸造代币
    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) public {
        require(ids.length == amounts.length, "length mismatch");
        for (uint i = 0; i < ids.length; i++) {
            balances[ids[i]][to] += amounts[i];
        }
        emit TransferBatch(msg.sender, address(0), to, ids, amounts);
    }
}

5、ERC721 与 ERC1155 对比表格

特性

ERC721

ERC1155

资产类型

单一、独特(每个 tokenId 仅对应一个资产)

多代币标准(可同质化 / 非同质化 / 半同质化)

典型场景

艺术品、头像、土地、收藏品

游戏道具、票券、盲盒、大规模 NFT

转账方式

transferFrom / safeTransferFrom(单个 NFT)

safeTransferFrom(单个),safeBatchTransferFrom(批量)

存储方式

mapping(uint256 => address) 记录所有权

mapping(uint256 => mapping(address => uint256)) 记录余额

批量支持

❌ 不支持批量转账

✅ 原生支持批量转账

Gas 成本

单个 NFT 操作时更低

批量操作时更节省 Gas

应用生态

PFP、艺术类 NFT 占主流

游戏、资产类项目采用更多

OpenZeppelin 实现

ERC721.sol, ERC721URIStorage

ERC1155.sol


6、Foundry 测试

MyNFTTest.sol

代码语言:solidity
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/MyERC721.sol";
import "../src/MyERC1155.sol";

// 继承 MyERC721,暴露一个外部 mint 供测试使用
contract MyERC721Mock is MyERC721 {
    function mint(address to, uint256 tokenId) external {
        _mint(to, tokenId);
    }
}

contract MyNFTTest is Test {
    MyERC721Mock nft721;
    MyERC1155 nft1155;

    address alice = address(0x1);
    address bob   = address(0x2);

    function setUp() public {
        // 部署合约
        nft721 = new MyERC721Mock();
        nft1155 = new MyERC1155();

        // 预铸造 ERC721 tokenId = 1 给 Alice
        nft721.mint(alice, 1);
        // 预铸造 ERC1155 tokenId = 100 给 Alice(数量 10)
        nft1155.mint(alice, 100, 10);
    }

    // ---------------- ERC721 测试 ----------------
    function testERC721OwnerOf() public view {
        assertEq(nft721.ownerOf(1), alice);
    }

    function testERC721BalanceOf() public view {
        assertEq(nft721.balanceOf(alice), 1);
    }

    function testERC721Transfer() public {
        vm.prank(alice); // 伪装成 Alice
        nft721.transferFrom(alice, bob, 1);

        assertEq(nft721.ownerOf(1), bob);
        assertEq(nft721.balanceOf(alice), 0);
        assertEq(nft721.balanceOf(bob), 1);
    }

    function testERC721ApproveAndTransfer() public {
        // Alice 授权 Bob 操作 tokenId=1
        vm.prank(alice);
        nft721.approve(bob, 1);

        // Bob 代替 Alice 转移
        vm.prank(bob);
        nft721.transferFrom(alice, bob, 1);

        assertEq(nft721.ownerOf(1), bob);
    }

    function test_RevertWhen_ERC721TransferNotAuthorized() public {
        // 未授权的地址尝试转移,应失败
        vm.prank(bob);
        vm.expectRevert(bytes("not authorized"));
        nft721.transferFrom(alice, bob, 1);
    }

    // ---------------- ERC1155 测试 ----------------

    function testERC1155BalanceOf() public view {
        assertEq(nft1155.balanceOf(alice, 100), 10);
    }

    function testERC1155MintBatch() public {
        uint256[] memory ids = new uint256[](2);
        ids[0] = 200;
        ids[1] = 201;

        uint256[] memory amounts = new uint256[](2);
        amounts[0] = 5;
        amounts[1] = 10;

        nft1155.mintBatch(alice, ids, amounts);

        assertEq(nft1155.balanceOf(alice, 200), 5);
        assertEq(nft1155.balanceOf(alice, 201), 10);
    }

    function testERC1155BalanceOfBatch() public {
        address[] memory accounts = new address[](2);
        accounts[0] = alice;
        accounts[1] = alice;

        uint256[] memory ids = new uint256[](2);
        ids[0] = 100;
        ids[1] = 200;

        // 先给 Alice 铸造 id=200 数量 7
        nft1155.mint(alice, 200, 7);

        uint256[] memory balances = nft1155.balanceOfBatch(accounts, ids);

        assertEq(balances[0], 10); // id=100 的数量
        assertEq(balances[1], 7);  // id=200 的数量
    }
}

执行测试:

代码语言:bash
复制
➜  tutorial git:(main) ✗ forge test --match-path test/MyNFTTest.t.sol -vvv

[⠊] Compiling...
[⠒] Compiling 2 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 577.98ms
Compiler run successful!

Ran 8 tests for test/MyNFTTest.t.sol:MyNFTTest
[PASS] testERC1155BalanceOf() (gas: 11033)
[PASS] testERC1155BalanceOfBatch() (gas: 45135)
[PASS] testERC1155MintBatch() (gas: 68116)
[PASS] testERC721ApproveAndTransfer() (gas: 58954)
[PASS] testERC721BalanceOf() (gas: 10689)
[PASS] testERC721OwnerOf() (gas: 10794)
[PASS] testERC721Transfer() (gas: 52908)
[PASS] test_RevertWhen_ERC721TransferNotAuthorized() (gas: 22145)
Suite result: ok. 8 passed; 0 failed; 0 skipped; finished in 4.75ms (7.22ms CPU time)

Ran 1 test suite in 160.73ms (4.75ms CPU time): 8 tests passed, 0 failed, 0 skipped (8 total tests)

7、本课总结

  • ERC721:最经典的 NFT 标准 → 单一、独特、适合收藏品
  • ERC1155:面向游戏和批量资产 → 更节省 Gas、灵活度高
  • 元数据管理(BaseURI + tokenId)是 NFT 的灵魂
  • 学习从零实现,能深入理解标准;实战推荐使用 OpenZeppelin

8、作业

  1. ERC721 中实现 safeTransferFrom,并写一个合约模拟 onERC721Received
  2. ERC1155 中补充 safeBatchTransferFrom 的实现,并写测试。
  3. 思考:如果要为 ERC721 添加 版税(Royalty) 功能,应该放在哪些函数中处理?
下一篇
举报
领券