SC Day 14
复习 - Week 2 总结 + ERC 标准速查卡
### 1. Week 2 知识回顾图谱
2026-04-14
第一阶段:基础构建reviewerc-standardsrust-reviewweek-summary
日期: 2026-04-14 方向: Solidity / Rust 阶段: 第一阶段:基础构建 标签: #review #erc-standards #rust-review #week-summary
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 系统回顾 Week 2 核心知识点,查漏补缺 |
| 实操 | 完成自检练习题,验证掌握程度 |
| 产出 | ERC 标准速查卡 + Week 2 知识图谱 + 自检结果 |
核心概念
1. Week 2 知识回顾图谱
Week 2 知识体系
├── Solidity
│ ├── Day 8: mapping + struct + 事件
│ │ ├── mapping(address => uint256) 存储模式
│ │ ├── struct 自定义类型
│ │ ├── event + indexed 参数
│ │ └── 完整 ERC20 实现
│ ├── Day 10: ERC20 标准
│ │ ├── 核心函数: transfer/approve/transferFrom
│ │ ├── approve + transferFrom 两步授权模式
│ │ ├── decimals 精度处理
│ │ └── 常见漏洞: 无限授权/精度损失
│ ├── Day 12: ERC721 标准
│ │ ├── 非同质化: 每个 tokenId 唯一
│ │ ├── safeTransferFrom vs transferFrom
│ │ ├── IERC721Receiver 接口
│ │ └── _mint vs _safeMint 安全考量
│ └── ERC 生态总览
│ ├── ERC1155: 多代币标准
│ ├── ERC4626: 代币化金库
│ └── ERC2612: permit 签名授权
│
└── Rust
├── Day 9: 所有权 + 借用 + 生命周期
│ ├── 所有权三规则
│ ├── &T (不可变借用) vs &mut T (可变借用)
│ ├── 借用检查器规则
│ └── 基础生命周期概念
├── Day 11: enum + 模式匹配 + Option/Result
│ ├── 代数数据类型 (ADT)
│ ├── match 穷尽性检查
│ ├── Option<T> 替代 null
│ ├── Result<T, E> 替代异常
│ └── ? 运算符错误传播
└── Day 13: trait + 泛型 + trait bound
├── trait 定义共享行为
├── 泛型 <T> 编写通用代码
├── trait bound: T: Display + Clone
├── where 子句
└── 静态分发 vs 动态分发 (dyn)
2. ERC 标准速查卡
ERC20 vs ERC721 核心对比
| 维度 | ERC20 (同质化) | ERC721 (非同质化) |
|---|---|---|
| 代币性质 | 可互换,每个单位相同 | 不可互换,每个 token 唯一 |
| 余额表示 | mapping(address => uint256) | mapping(uint256 => address) |
| 转账参数 | transfer(to, amount) | transferFrom(from, to, tokenId) |
| 授权 | approve(spender, amount) | approve(to, tokenId) |
| 批量授权 | 无标准方法 | setApprovalForAll(operator, bool) |
| 安全转账 | 无(ERC20 没有) | safeTransferFrom + 接收者检查 |
| 元数据 | name/symbol/decimals | name/symbol/tokenURI |
| 典型用例 | 货币/治理代币/稳定币 | 头像 NFT/游戏道具/证书/地产 |
ERC 标准完整速查
ERC20 - 同质化代币标准
├── 核心: balanceOf, transfer, approve, transferFrom, allowance
├── 元数据: name(), symbol(), decimals()
├── 事件: Transfer, Approval
└── 陷阱: approve 前竞态条件, decimals 精度
ERC721 - 非同质化代币标准
├── 核心: ownerOf, balanceOf, transferFrom, safeTransferFrom
├── 授权: approve, setApprovalForAll, getApproved, isApprovedForAll
├── 元数据: name(), symbol(), tokenURI()
├── 接口: IERC721Receiver.onERC721Received
└── 陷阱: _mint vs _safeMint 重入风险
ERC1155 - 多代币标准(ERC20 + ERC721 的超集)
├── 核心: balanceOf(account, id), balanceOfBatch
├── 转账: safeTransferFrom(from, to, id, amount, data)
├── 批量: safeBatchTransferFrom(from, to, ids, amounts, data)
├── 授权: setApprovalForAll (没有单 token approve)
├── 接口: IERC1155Receiver.onERC1155Received + onERC1155BatchReceived
├── 元数据: uri(id) 返回 JSON 元数据 URL
└── 优势: Gas 效率高,一个合约管理多种代币
ERC4626 - 代币化金库标准
├── 继承: ERC20
├── 存入: deposit(assets, receiver) → 返回 shares
├── 取出: withdraw(assets, receiver, owner) → 返回 shares
├── 赎回: redeem(shares, receiver, owner) → 返回 assets
├── 预览: previewDeposit, previewMint, previewWithdraw, previewRedeem
├── 汇率: convertToShares(assets), convertToAssets(shares)
└── 用途: 收益聚合器, 借贷池, 质押合约
ERC2612 - Permit(签名授权,无需链上 approve 交易)
├── 函数: permit(owner, spender, value, deadline, v, r, s)
├── 原理: 用 EIP-712 签名代替 approve 交易
├── 优势: 节省一笔 Gas,改善 UX
└── 风险: 签名可被重放(需 nonce 保护)
ERC165 - 接口检测
├── 函数: supportsInterface(bytes4 interfaceId)
├── 用途: 查询合约是否支持某个标准
└── 计算: interfaceId = 所有函数 selector 的 XOR
ERC1155 代码速览
// ERC1155 关键接口
interface IERC1155 {
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
);
function balanceOf(address account, uint256 id) external view returns (uint256);
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external view returns (uint256[] memory);
function safeTransferFrom(
address from, address to, uint256 id, uint256 amount, bytes calldata data
) external;
function safeBatchTransferFrom(
address from, address to,
uint256[] calldata ids, uint256[] calldata amounts,
bytes calldata data
) external;
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address account, address operator) external view returns (bool);
}
3. Rust 核心概念回顾
所有权规则速查
Rust 所有权三大规则:
1. 每个值有且只有一个 owner
2. 当 owner 离开作用域,值被 drop
3. 赋值/传参 = 移动(move),原变量失效
借用规则:
- 同一时刻,要么 N 个 &T,要么 1 个 &mut T
- 引用的生命周期不能超过被引用值
- 编译器通过借用检查器 (borrow checker) 在编译期保证
enum + Option + Result 速查
// Option: 表示"有或没有"
let found: Option<u64> = map.get("key").copied();
// 使用:match / if let / unwrap_or / map / and_then / ?
// Result: 表示"成功或失败"
let result: Result<String, Error> = parse_address(input);
// 使用:match / ? / unwrap_or_else / map_err
// 从 Option 转 Result
let val = found.ok_or(Error::NotFound)?;
// 从 Result 取 Option
let opt = result.ok();
Trait + 泛型速查
// 定义 trait
trait Hashable {
fn hash(&self) -> Vec<u8>;
fn hash_hex(&self) -> String { // 默认实现
hex::encode(self.hash())
}
}
// 泛型 + trait bound
fn verify<T: Hashable + Eq>(a: &T, b: &T) -> bool {
a.hash() == b.hash()
}
// where 子句(复杂场景)
fn complex<T, U>(a: T, b: U) -> bool
where
T: Hashable + Clone + Send,
U: Hashable + Sync,
{
a.hash() == b.hash()
}
// impl Trait(简写)
fn simple(item: &impl Hashable) -> String {
item.hash_hex()
}
代码实战:综合练习
练习 1:Solidity - 简易 NFT 市场
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC721 {
function ownerOf(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
/// @title 简易 NFT 市场
/// @notice 综合运用 mapping/struct/event/ERC721 知识
contract SimpleNFTMarket {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
// listingId => Listing
mapping(uint256 => Listing) public listings;
uint256 public nextListingId;
// 平台手续费 (2.5%)
uint256 public constant FEE_BPS = 250; // basis points
address public immutable feeRecipient;
event Listed(
uint256 indexed listingId,
address indexed seller,
address indexed nftContract,
uint256 tokenId,
uint256 price
);
event Sold(
uint256 indexed listingId,
address indexed buyer,
uint256 price
);
event Cancelled(uint256 indexed listingId);
constructor() {
feeRecipient = msg.sender;
}
/// @notice 上架 NFT
function list(address nftContract, uint256 tokenId, uint256 price) external {
require(price > 0, "Price must be > 0");
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not the owner");
require(
nft.getApproved(tokenId) == address(this) ||
nft.isApprovedForAll(msg.sender, address(this)),
"Market not approved"
);
uint256 listingId = nextListingId++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
price: price,
active: true
});
emit Listed(listingId, msg.sender, nftContract, tokenId, price);
}
/// @notice 购买 NFT
function buy(uint256 listingId) external payable {
Listing storage listing = listings[listingId];
require(listing.active, "Listing not active");
require(msg.value >= listing.price, "Insufficient payment");
listing.active = false;
// 计算手续费
uint256 fee = (listing.price * FEE_BPS) / 10000;
uint256 sellerProceeds = listing.price - fee;
// 转移 NFT
IERC721(listing.nftContract).transferFrom(
listing.seller,
msg.sender,
listing.tokenId
);
// 支付
(bool sent1, ) = listing.seller.call{value: sellerProceeds}("");
require(sent1, "Payment to seller failed");
(bool sent2, ) = feeRecipient.call{value: fee}("");
require(sent2, "Fee payment failed");
// 退还多余 ETH
if (msg.value > listing.price) {
(bool refunded, ) = msg.sender.call{value: msg.value - listing.price}("");
require(refunded, "Refund failed");
}
emit Sold(listingId, msg.sender, listing.price);
}
/// @notice 取消上架
function cancel(uint256 listingId) external {
Listing storage listing = listings[listingId];
require(listing.active, "Listing not active");
require(listing.seller == msg.sender, "Not the seller");
listing.active = false;
emit Cancelled(listingId);
}
}
练习 2:Rust - 泛型代币注册表
use std::collections::HashMap;
use std::fmt;
// ========== Trait 定义 ==========
trait TokenInfo: fmt::Display {
fn address(&self) -> &str;
fn name(&self) -> &str;
fn standard(&self) -> &str;
}
// ========== 代币类型 ==========
struct ERC20Info {
address: String,
name: String,
symbol: String,
decimals: u8,
total_supply: u128,
}
impl fmt::Display for ERC20Info {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({}) - Supply: {}", self.name, self.symbol, self.total_supply)
}
}
impl TokenInfo for ERC20Info {
fn address(&self) -> &str { &self.address }
fn name(&self) -> &str { &self.name }
fn standard(&self) -> &str { "ERC20" }
}
struct ERC721Info {
address: String,
name: String,
symbol: String,
max_supply: Option<u64>,
}
impl fmt::Display for ERC721Info {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.max_supply {
Some(max) => write!(f, "{} ({}) - Max: {}", self.name, self.symbol, max),
None => write!(f, "{} ({}) - 无上限", self.name, self.symbol),
}
}
}
impl TokenInfo for ERC721Info {
fn address(&self) -> &str { &self.address }
fn name(&self) -> &str { &self.name }
fn standard(&self) -> &str { "ERC721" }
}
// ========== 泛型注册表 ==========
struct TokenRegistry<T: TokenInfo> {
tokens: HashMap<String, T>,
}
impl<T: TokenInfo> TokenRegistry<T> {
fn new() -> Self {
TokenRegistry { tokens: HashMap::new() }
}
fn register(&mut self, token: T) {
let addr = token.address().to_string();
println!("注册 {} 代币: {}", token.standard(), token);
self.tokens.insert(addr, token);
}
fn lookup(&self, address: &str) -> Option<&T> {
self.tokens.get(address)
}
fn count(&self) -> usize {
self.tokens.len()
}
fn list_all(&self) {
for (addr, token) in &self.tokens {
println!(" [{}] {}", &addr[..10], token);
}
}
}
// ========== 泛型搜索函数 ==========
fn find_by_name<'a, T: TokenInfo>(registry: &'a TokenRegistry<T>, name: &str) -> Vec<&'a T> {
registry.tokens.values()
.filter(|t| t.name().to_lowercase().contains(&name.to_lowercase()))
.collect()
}
fn main() {
// ERC20 注册表
let mut erc20_registry = TokenRegistry::new();
erc20_registry.register(ERC20Info {
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into(),
name: "USD Coin".into(),
symbol: "USDC".into(),
decimals: 6,
total_supply: 26_000_000_000_000_000,
});
erc20_registry.register(ERC20Info {
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".into(),
name: "Tether USD".into(),
symbol: "USDT".into(),
decimals: 6,
total_supply: 40_000_000_000_000_000,
});
println!("--- ERC20 代币列表 ({}) ---", erc20_registry.count());
erc20_registry.list_all();
// 搜索
let usd_tokens = find_by_name(&erc20_registry, "usd");
println!("搜索 'usd': 找到 {} 个", usd_tokens.len());
// ERC721 注册表
let mut nft_registry = TokenRegistry::new();
nft_registry.register(ERC721Info {
address: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D".into(),
name: "Bored Ape Yacht Club".into(),
symbol: "BAYC".into(),
max_supply: Some(10000),
});
println!("\n--- NFT 列表 ({}) ---", nft_registry.count());
nft_registry.list_all();
}
自检练习
Solidity 自检
| # | 问题 | 你的答案 | 正确答案 |
|---|---|---|---|
| 1 | ERC20 的 approve + transferFrom 为什么需要两步? | ___________ | 允许第三方(如 DEX)代你转账 |
| 2 | ERC721 的 safeTransferFrom 多做了什么? | ___________ | 检查接收方是否实现 IERC721Receiver |
| 3 | _mint 和 _safeMint 的区别? | ___________ | _safeMint 会调用 onERC721Received |
| 4 | mapping 的默认值是什么? | ___________ | 所有类型的零值(uint=0, bool=false, address=0x0) |
| 5 | event 中 indexed 的作用? | ___________ | 允许按该字段过滤日志 |
| 6 | ERC20 decimals=6 时,1 USDC 存多少? | ___________ | 1_000_000 (10^6) |
| 7 | ERC1155 和 ERC721 的主要区别? | ___________ | ERC1155 支持同一合约管理多种代币,包括同质和非同质 |
Rust 自检
| # | 问题 | 你的答案 | 正确答案 |
|---|---|---|---|
| 1 | let s2 = s1; 之后 s1 还能用吗?(String 类型) | ___________ | 不能,所有权已移动 |
| 2 | Option<T> 的两个变体是? | ___________ | Some(T) 和 None |
| 3 | Result<T, E> 的 ? 运算符做了什么? | ___________ | Ok → 取出值;Err → 立即 return 错误 |
| 4 | trait bound T: Display + Clone 是什么意思? | ___________ | T 必须同时实现 Display 和 Clone |
| 5 | &T 和 &mut T 能同时存在吗? | ___________ | 不能,Rust 借用规则禁止 |
| 6 | Vec<Box<dyn Trait>> 是什么? | ___________ | 存放实现了 Trait 的不同类型的动态分发集合 |
| 7 | match 不覆盖所有分支会怎样? | ___________ | 编译错误 |
编码练习
练习 1 (Solidity):
编写一个 ERC20 合约,增加 burn() 和 burnFrom() 函数。
burn() 让持有者销毁自己的代币。
burnFrom() 让已授权的第三方帮你销毁。
练习 2 (Rust):
实现一个 Block 结构体,包含 number, hash, parent_hash, transactions (Vec<String>)。
为它实现 Display trait,打印格式化的区块信息。
用 Option<Block> 实现一个 find_block(number: u64) 函数。
练习 3 (综合):
设计一个 Rust enum 来表示 ERC 标准的所有类型:
enum ERCStandard { ERC20 { ... }, ERC721 { ... }, ERC1155 { ... } }
为每个变体携带相关数据,并实现一个 describe() 方法。
关键要点总结
| 要点 | 说明 |
|---|---|
| ERC20 = 同质化 | 每个 token 相同,适合货币 |
| ERC721 = 非同质化 | 每个 token 唯一,适合收藏品 |
| ERC1155 = 多代币 | 一个合约管理多种代币类型 |
| Rust 所有权 | 编译期内存安全,零运行时开销 |
| Rust enum = ADT | 比 Solidity enum 强大得多 |
| Rust trait = 接口 + 默认实现 | 比 Solidity interface 更灵活 |
| 泛型 + trait bound | 类型安全的多态 |
常见误区
误区 1:ERC20 和 ERC721 的 balanceOf 语义不同
- ERC20:
balanceOf(address)→ 返回代币数量 (uint256) - ERC721:
balanceOf(address)→ 返回持有的 NFT 总数 (uint256) - 注意:ERC721 没有 "余额" 的概念,只有 "持有数量"
误区 2:认为 Rust 的所有权系统增加运行时开销
所有权检查完全在编译期完成。编译后的代码和 C/C++ 一样高效,没有任何运行时垃圾回收或引用计数(除非你显式使用 Rc/Arc)。
误区 3:混淆泛型和 trait object 的使用场景
- 泛型
fn foo<T: Trait>(x: T)→ 编译期单态化,为每种类型生成独立代码 - trait object
fn foo(x: &dyn Trait)→ 运行时虚表查找,支持异构集合
面试关联
Q: 请比较 ERC20, ERC721, ERC1155 三个标准
结构化回答:
- 同质性: ERC20 完全同质 → ERC721 完全非同质 → ERC1155 兼容两者
- Gas 效率: ERC1155 > ERC721 > ERC20(对于多代币场景)
- 授权模型: ERC20 只有
approve(amount)→ ERC721 有单个 + 全部 → ERC1155 只有全部 - 安全转账: ERC20 无 → ERC721/1155 有
safe*系列 - 典型场景: ERC20=货币 → ERC721=艺术品 → ERC1155=游戏道具
Q: Rust 的类型系统如何帮助区块链开发?
- 所有权防止 double-spend 类型的逻辑错误(编译期保证值不被复制)
- Option/Result 强制处理所有错误路径(区块链中错误处理极其关键)
- match 穷尽性确保所有状态都被处理(状态机不会遗漏分支)
- 泛型 + trait 让代码可复用且类型安全(跨链适配器的统一接口)
参考资源
| 资源 | 说明 |
|---|---|
| EIP-20 | ERC20 标准 |
| EIP-721 | ERC721 标准 |
| EIP-1155 | ERC1155 标准 |
| EIP-4626 | 代币化金库标准 |
| OpenZeppelin Contracts | 业界标准实现 |
| Rust Book 全文 | Rust 官方教程 |
| Rust Cheat Sheet | Rust 语法速查 |