在本指南中,我们将用一个完整的实战案例,带你手写一个极简 去中心化交易所(DEX) 智能合约,演示如何通过 Solidity 与 ERC-20 代币 完成转账(transfer)、授权(approve/allowance)及兑换操作。阅读完本文后,你不仅能读懂合约,还能自己部署测试。
核心关键词
- Solidity 智能合约
- ERC-20 代币
- transfer
- approve
- allowance
- DEX
- Ethereum
01 快速回顾:ERC-20 Interface
任何与 ERC-20 代币交互的合约,都必须先认识这四个最关键的行为:
transfer(address to, uint256 amount):把代币转给某个地址approve(address spender, uint256 amount):授权某地址在未来可以自由花费你限额内的代币allowance(address owner, address spender):查询当前授权额度transferFrom(address from, address to, uint256 amount):被授权方代为转账
我们先写一个最简接口文件,方便后续调用:
pragma solidity ^0.8.0;
interface 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 recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, 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);
}02 部署内置的基础 ERC-20 代币合约
为了让 DEX 有代币储备,我们在构造函数里手动发行 10 个以太单位(10 ether)的 ERC20Basic。
contract ERC20Basic is IERC20 {
string public constant name = "ERC20Basic";
string public constant symbol = "ERC";
uint8 public constant decimals = 18;
uint256 totalSupply_ = 10 ether;
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowed;
constructor() {
balances[msg.sender] = totalSupply_;
}
function totalSupply() public override view returns (uint256) {
return totalSupply_;
}
function balanceOf(address tokenOwner) public override view returns (uint256) {
return balances[tokenOwner];
}
function transfer(address receiver, uint256 amount) public override returns (bool) {
require(amount <= balances[msg.sender], "Insufficient balance");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Transfer(msg.sender, receiver, amount);
return true;
}
function approve(address delegate, uint256 numTokens) public override returns (bool) {
allowed[msg.sender][delegate] = numTokens;
emit Approval(msg.sender, delegate, numTokens);
return true;
}
function allowance(address owner, address delegate) public override view returns (uint256) {
return allowed[owner][delegate];
}
function transferFrom(address owner, address buyer, uint256 numTokens)
public
override
returns (bool)
{
require(numTokens <= balances[owner], "Insufficient owner balance");
require(numTokens <= allowed[owner][msg.sender], "Allowance exceeded");
balances[owner] -= numTokens;
allowed[owner][msg.sender] -= numTokens;
balances[buyer] += numTokens;
emit Transfer(owner, buyer, numTokens);
return true;
}
}03 手写最小可用 DEX
DEX 地址内部保存了 ERC20Basic 实例,并通过两个函数完成双向兑换:
3.1 构造函数初始化代币储备
contract DEX {
IERC20 public token;
event Bought(uint256 amount);
event Sold(uint256 amount);
constructor() {
token = new ERC20Basic(); // 自动把全部供应量转到 DEX 自身
}
}3.2 buy:用 ETH 买 ERC-20 代币
规则:1 Wei = 1 Token(简化模型)。
function buy() payable public {
uint256 amountToBuy = msg.value;
uint256 dexBalance = token.balanceOf(address(this));
require(amountToBuy > 0, "Need to send ETH");
require(amountToBuy <= dexBalance, "Not enough tokens in reserve");
token.transfer(msg.sender, amountToBuy); // 直接转账
emit Bought(amountToBuy);
}执行成功后,节点日志会先后出现:
Transfer:DEX → 用户Bought:DEX 自定义事件
3.3 sell:卖代币换回 ETH
过程拆解:
- 用户需要提前
approve(DEX, amount); - DEX 检测额度是否满足;
- 用
transferFrom把代币转回 DEX; - 回退对应 ETH 给用户。
function sell(uint256 amount) public {
require(amount > 0, "Need to sell at least some tokens");
uint256 allowance = token.allowance(msg.sender, address(this));
require(allowance >= amount, "Check allowance");
token.transferFrom(msg.sender, address(this), amount); // 代币转入
payable(msg.sender).transfer(amount); // 转账 ETH
emit Sold(amount);
}成功后同样会出现:
Transfer:用户 → DEXSold:DEX 自定义事件
04 带注释的完整合约
将以上所有代码片段合并,即可得到可直接复制到 Remix 的单一文件:
pragma solidity ^0.8.0;
interface 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 recipient,uint256 amount) external returns (bool);
function approve(address spender,uint256 amount) external returns (bool);
function transferFrom(address sender,address recipient,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);
}
contract ERC20Basic is IERC20 {
// ...同上...
}
contract DEX {
IERC20 public token;
event Bought(uint256 amount);
event Sold(uint256 amount);
constructor() {
token = new ERC20Basic();
}
function buy() payable external {
uint256 amountToBuy = msg.value;
uint256 dexBalance = token.balanceOf(address(this));
require(amountToBuy > 0, "Need to send ETH");
require(amountToBuy <= dexBalance, "Not enough tokens in reserve");
token.transfer(msg.sender, amountToBuy);
emit Bought(amountToBuy);
}
function sell(uint256 amount) external {
require(amount > 0, "Need to sell at least some tokens");
uint256 allowance = token.allowance(msg.sender, address(this));
require(allowance >= amount, "Allowance too low");
token.transferFrom(msg.sender, address(this), amount);
payable(msg.sender).transfer(amount);
emit Sold(amount);
}
}05 常见问题解答(FAQ)
Q1:为什么 approve 之后还要手动调用 sell?
→ approve 只是把“花钱额度”开给了 DEX,真正的代币转移发生在 DEX 合约内部。两步分离可以提高安全性,避免一次性授权过大。
Q2:如何防止价格操纵?
→ 本文仅用 1:1 定价作教学示例;实际 DEX 应采用自动做市商(AMM)公式或引入 Oracle。
Q3:为什么我用 Metamask 调用 sell 一直失败?
→ 大概率是忘了先 approve,或者提取的额度不足。建议先用 Remix 事件调试 Approval 日志。
Q4:能否在测试网直接体验?
→ 把 new ERC20Basic() 替换为已部署代币地址,即可在 Goerli、Sepolia 等网络重复整套流程。
Q5:手续费(Gas)如何优化?
→ 可采用 OpenZeppelin 内置的 ERC-20;其符号常量和事件已优化,能减少不少字节码体积。
通过动手实现上述代码,你已经掌握了 Solidity 智能合约 与 ERC-20 代币 的常用交互套路:transfer、approve 和 transferFrom。把这些积木拼接起来,可以继续拓展为流动性池、闪电借贷甚至全功能 DEX。祝你链上编码愉快!