如何用 Solidity 实现 ERC-20 代币的转账和批准:从零到去中心化交易所

·

在本指南中,我们将用一个完整的实战案例,带你手写一个极简 去中心化交易所(DEX) 智能合约,演示如何通过 Solidity 与 ERC-20 代币 完成转账(transfer)、授权(approve/allowance)及兑换操作。阅读完本文后,你不仅能读懂合约,还能自己部署测试。

核心关键词


01 快速回顾:ERC-20 Interface

任何与 ERC-20 代币交互的合约,都必须先认识这四个最关键的行为:

我们先写一个最简接口文件,方便后续调用:

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);
}

执行成功后,节点日志会先后出现:

👉 想在本地一步起跑完整示例?点击查看无需环境的测试脚本。

3.3 sell:卖代币换回 ETH

过程拆解:

  1. 用户需要提前 approve(DEX, amount)
  2. DEX 检测额度是否满足;
  3. transferFrom 把代币转回 DEX;
  4. 回退对应 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);
}

成功后同样会出现:


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 代币 的常用交互套路:transferapprovetransferFrom。把这些积木拼接起来,可以继续拓展为流动性池、闪电借贷甚至全功能 DEX。祝你链上编码愉快!