第四课:ERC20代币合约实战
好,咱们今天来聊聊ERC20代币合约。说实话,这个标准我闭着眼睛都能写出来——因为用得实在太多了。但真正写好一个生产级的代币合约,远不止是抄个接口那么简单。
我个人习惯把ERC20合约分成三个层次:基础实现、功能扩展、安全加固。今天咱们就从这三个维度,把ERC20彻底讲透。
4.1 完整ERC20代币合约编写
先看一个最基础的ERC20实现。嗯,这里要注意,虽然OpenZeppelin已经帮我们封装好了,但我还是建议你手写一遍核心逻辑——这样才能真正理解它的运作机制。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyToken {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 amount) external returns (bool) {
require(to != address(0), "ERC20: transfer to zero address");
require(balanceOf[msg.sender] >= amount, "ERC20: insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(from != address(0), "ERC20: transfer from zero address");
require(to != address(0), "ERC20: transfer to zero address");
require(balanceOf[from] >= amount, "ERC20: insufficient balance");
require(allowance[from][msg.sender] >= amount, "ERC20: insufficient allowance");
balanceOf[from] -= amount;
balanceOf[to] += amount;
allowance[from][msg.sender] -= amount;
emit Transfer(from, to, amount);
return true;
}
}
这段代码虽然简单,但包含了ERC20的全部核心逻辑。你想想看,transfer和transferFrom的区别在哪?说白了就是一个是直接转账,一个是授权转账——这是DeFi组合性的基础。
transferFrom中,很多人会忘记减少授权额度。我曾经审计过一个项目,就是因为这个疏忽,导致用户可以无限次花别人的钱——嗯,后果很严重。
4.2 OpenZeppelin库集成
手写一遍是为了理解原理,但生产环境我强烈建议用OpenZeppelin。为什么?因为它的代码经过了无数次审计和实战检验,你踩过的坑它基本都帮你填平了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(string memory name, string memory symbol)
ERC20(name, symbol)
{
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}
看到没?用OpenZeppelin,几十行代码就搞定了。而且它自动处理了decimals默认值、事件发射、安全检查等细节。我个人习惯在项目初期就用这个模板,省时省力。
_beforeTokenTransfer钩子函数,这为后续的功能扩展(比如快照、暂停)提供了完美的切入点。
4.3 代币增发与销毁
增发和销毁是代币经济学的核心操作。增发用于激励,销毁用于通缩。但这里有个坑——权限控制。
我曾经见过一个项目,增发函数没有加onlyOwner,结果被黑客调用了一次,直接增发了10亿个代币...嗯,项目方当场崩溃。
// 增发
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// 销毁
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
// 从指定地址销毁(需要授权)
function burnFrom(address account, uint256 amount) external {
uint256 currentAllowance = allowance(account, msg.sender);
require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance");
_approve(account, msg.sender, currentAllowance - amount);
_burn(account, amount);
}
| 操作 | 权限 | 典型场景 |
|---|---|---|
| mint | onlyOwner | 挖矿奖励、空投 |
| burn | 任何人(销毁自己的) | 用户主动销毁、手续费销毁 |
| burnFrom | 授权地址 | DeFi协议自动销毁 |
totalSupply的上限。虽然Solidity的uint256理论上不会溢出,但无限增发会稀释代币价值——这不仅是技术问题,更是经济模型问题。
4.4 代币快照功能
快照功能有什么用?说白了就是记录某个时间点所有人的余额。这在空投、治理投票、分红场景中非常常见。
OpenZeppelin提供了ERC20Snapshot扩展,实现起来很简单:
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
contract MyToken is ERC20, ERC20Snapshot, Ownable {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function snapshot() external onlyOwner returns (uint256) {
return _snapshot();
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20, ERC20Snapshot) {
super._beforeTokenTransfer(from, to, amount);
}
}
快照的原理其实不复杂:每次快照时,记录当前区块号,然后通过一个映射存储每个地址在不同快照ID下的余额。查询时,找到最近的快照ID,返回当时的余额。
4.5 可暂停代币实现
可暂停功能,说白了就是一个紧急开关。当发现漏洞或异常时,管理员可以暂停所有转账操作,保护用户资产。
OpenZeppelin的Pausable结合ERC20,实现起来也很优雅:
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyToken is ERC20, Pausable, Ownable {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override whenNotPaused {
super._beforeTokenTransfer(from, to, amount);
}
}
这里的关键是whenNotPaused修饰符。它会在每次转账前检查合约是否暂停。如果暂停了,所有转账都会revert。
4.6 综合实战:一个完整的可暂停、可快照、可增发销毁的ERC20
好了,咱们把今天学的所有功能整合到一起。这才是生产级的代币合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract AdvancedToken is ERC20, ERC20Snapshot, Pausable, Ownable {
constructor() ERC20("AdvancedToken", "ATK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
// 增发
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// 销毁
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
// 快照
function snapshot() external onlyOwner returns (uint256) {
return _snapshot();
}
// 暂停
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// 重写钩子函数
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20, ERC20Snapshot) whenNotPaused {
super._beforeTokenTransfer(from, to, amount);
}
}
这个合约包含了我们今天讲的所有功能。你想想看,它是不是已经可以应对大部分DeFi场景了?
最后,我想说一句:写合约容易,写好合约难。每一个功能背后都有权衡——快照消耗gas,暂停影响用户体验,增发影响经济模型。作为开发者,你要做的不是堆砌功能,而是根据业务场景做出最合适的选择。
嗯,今天就到这里。下一章咱们会深入DeFi的核心——流动性池和AMM机制,那才是真正有意思的部分。