1、学习目标
- 理解 ERC721 与 ERC1155 的标准接口
- 从零实现一个 最小化 ERC721(NFT)合约
- 扩展功能:元数据管理(BaseURI)、批量铸造 / 批量转账
- 对比 OpenZeppelin 实现
- 使用 Foundry 测试
2、知识点梳理
- ERC721 核心接口
balanceOf(address)
ownerOf(uint256)
safeTransferFrom(address,address,uint256)
transferFrom(address,address,uint256)
approve(address,uint256)
/setApprovalForAll(address,bool)
- 事件:
Transfer
,Approval
,ApprovalForAll
- ERC1155 核心接口
- 支持 多代币标准(FT / NFT / SFT)
balanceOf(address,uint256)
safeTransferFrom(address,address,uint256,uint256,bytes)
safeBatchTransferFrom(...)
- 事件:
TransferSingle
,TransferBatch
,ApprovalForAll
- 应用场景差异
- ERC721 → 独一无二的资产(头像、土地、艺术品)
- ERC1155 → 大规模批量资产(游戏道具、门票、盲盒)
3、最小 ERC721 实现
MyERC721.sol:
// 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:
// 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:
// 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 的数量
}
}
执行测试:
➜ 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、作业
- 在 ERC721 中实现
safeTransferFrom
,并写一个合约模拟onERC721Received
。 - 在 ERC1155 中补充
safeBatchTransferFrom
的实现,并写测试。 - 思考:如果要为 ERC721 添加 版税(Royalty) 功能,应该放在哪些函数中处理?