Solidity 中 ERC20 和 ERC721 代币标准的核心实现原理是什么?
ERC20 实现同质化代币,核心是 balanceOf/transfer/approve/transferFrom 四个函数加上双映射存储(_balances 和 _allowances);ERC721 实现非同质化代币,核心是 ownerOf(tokenId) 加上 tokenId→owner 的单映射,配合 tokenApprovals 和 operatorApprovals 两层授权机制。两者的根本区别在于:ERC20 按金额操作,ERC721 按 tokenId 操作。
ERC20:同质化代币的存储与流转
ERC20 的状态只有三个:mapping(address => uint256) _balances 记录每个地址的余额,mapping(address => mapping(address => uint256)) _allowances 记录授权额度,uint256 _totalSupply 记录总供应量。
转账逻辑 _transfer 做三件事:检查 from 不为零地址、检查 to 不为零地址、检查余额充足后扣减和增加。关键细节:Solidity 0.8+ 内置溢出检查,所以用 unchecked 包裹加减法来节省 gas——这要求开发者自己保证不会溢出。
授权机制是 ERC20 最容易出问题的地方。approve 直接覆盖授权额度,而不是累加。这就导致了经典的授权抢跑攻击:用户想把授权从 100 改成 50,攻击者在用户交易上链前用 gas 竞价抢先消费掉原来的 100,等用户的新授权生效后再消费 50,总共拿走 150。解决方案是用 increaseAllowance / decreaseAllowance 代替直接 approve,或者使用 ERC20 Permit 扩展(EIP-2612)通过签名授权避免两次交易。
solidityinterface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); }
transferFrom 的执行顺序很重要:先扣减 allowance,再执行 _transfer。这遵循 Checks-Effects-Interactions 模式,先检查授权、修改状态,最后触发外部交互。如果顺序反过来,就可能被重入攻击利用。另外,当 allowance 等于 type(uint256).max 时不扣减,这是为了兼容某些合约一次性授权无限额度的场景。
铸造 _mint 和销毁 _burn 是内部函数,不暴露在接口中。_mint 从零地址转出,_burn 转入零地址,这样 Transfer 事件保持一致,区块链浏览器可以统一解析。
ERC721:非同质化代币的唯一性保证
ERC721 的核心存储是 mapping(uint256 => address) _owners,用 tokenId 直接映射到所有者。这决定了每个 tokenId 只能有一个 owner,不可互换、不可分割。
授权机制比 ERC20 多一层:approve 授权某个地址操作指定的 tokenId,setApprovalForAll 授权某个地址操作自己的所有 NFT。前者是单 token 授权,后者是批量授权。转账前要先清空 tokenApprovals,防止前任 owner 的授权在新 owner 不知情的情况下仍然有效。
solidityinterface IERC721 { function balanceOf(address owner) external view returns (uint256); function ownerOf(uint256 tokenId) external view returns (address); function safeTransferFrom(address from, address to, uint256 tokenId) external; function transferFrom(address from, address to, uint256 tokenId) external; function approve(address to, uint256 tokenId) external; function setApprovalForAll(address operator, bool approved) external; function getApproved(uint256 tokenId) external view returns (address); function isApprovedForAll(address owner, address operator) external view returns (bool); 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); }
safeTransferFrom 和 transferFrom 的区别是面试高频点。safe 版本在转账后会调用接收方的 onERC721Received 回调,确认接收方实现了 IERC721Receiver 接口。如果接收方是合约但没有实现这个接口,交易会回滚。这防止了 NFT 被转到无法处理的合约里永久锁死。普通 transferFrom 不做这个检查,适合 gas 敏感且确定接收方安全的场景。
铸造也有 _mint 和 _safeMint 两个版本,区别同样是是否检查接收方。实际开发中推荐使用 _safeMint,除非明确知道接收地址是 EOA(外部账户)。
关键差异对比
| 维度 | ERC20 | ERC721 |
|---|---|---|
| 存储结构 | address → uint256 | uint256 → address |
| 代币单位 | amount(数量) | tokenId(唯一标识) |
| 授权粒度 | 按金额 | 按 tokenId + 全局操作员 |
| 可分割性 | 可分割 | 不可分割 |
| 典型场景 | 货币、积分、治理代币 | 艺术品、道具、凭证 |
存储结构的差异是理解一切的关键:ERC20 用地址查余额,ERC721 用 tokenId 查所有者。这意味着 ERC20 的余额是一个 uint256,可以加减;ERC721 的"余额"只是某个地址拥有的 NFT 数量,不能直接用来转账,必须指定 tokenId。
安全要点
整数溢出在 Solidity 0.8+ 中已由编译器自动检查,但 unchecked 块内的操作不受保护,使用时必须确保逻辑安全。重入攻击的防御遵循 Checks-Effects-Interactions:先检查条件、修改状态,最后才调用外部合约。ERC721 的 _checkOnERC721Received 就是一个外部调用,必须在状态更新之后执行。零地址检查防止代币铸造到或转入 address(0),否则会导致代币永久丢失且无法通过 _burn 回收。
扩展标准
ERC777 在 ERC20 基础上增加了钩子函数(tokensReceived/tokensToSend),转账时自动通知双方,但这也引入了重入风险。ERC1155 合并了同质化和非同质化,用一个合约管理多种代币类型,批量操作节省 gas。ERC2981 定义了 NFT 版税接口,让市场合约能自动向创作者分成。ERC4907 实现了 NFT 租赁,分离使用权和所有权。
追问
为什么 ERC20 的 approve 会有抢跑风险,怎么解决? 因为 approve 是覆盖式授权,攻击者可以在旧授权被新授权覆盖前抢先使用。用 increaseAllowance/decreaseAllowance 累加式修改,或用 ERC20Permit 签名授权一次完成。
ERC721 的 safeTransferFrom 和 transferFrom 怎么选? 转给合约必须用 safe 版,转给 EOA 用哪个都行。safe 版多一次外部调用,gas 约贵 3000-5000。不确定接收方类型时,始终用 safe 版。
ERC1155 和 ERC721 的取舍? 需要批量操作(如游戏背包一次转移多件道具)选 ERC1155,gas 效率高。强调唯一性和独立元数据(如艺术品)选 ERC721,生态兼容性更好。