remix编译运行solidity9

昨天学了enum枚举是一个常量,是一个数据类型,常用于表示等级、任务的状态机
昨天还了解了loyalty是web3增长最快的商业模式。并且在linkedin上查看了一些web3的岗位并发到了公众号上。
前天学了合约的继承,多重继承,多重继承。

好的!现在正式进入 今天任务 3:阅读 ERC20 标准合约
我们采用 OpenZeppelin 最经典、最清晰的 ERC20 实现(简化版),并且我会带你逐行分析,让你未来看任何 DeFi 协议都不会卡。


📘 标准 ERC20(教学用简化版)

下面这个版本去掉了复杂模块(如 ERC20Permit、ERC20Votes),保留完整核心逻辑,适合第一次阅读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ERC20 {
string public name;
string public symbol;
uint8 public decimals = 18;
uint public totalSupply;

mapping(address => uint) private balances;
mapping(address => mapping(address => uint)) private allowances;

event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);

constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}

function balanceOf(address account) public view returns (uint) {
return balances[account];
}

function transfer(address to, uint amount) public returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}

function approve(address spender, uint amount) public returns (bool) {
allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function allowance(address owner, address spender) public view returns (uint) {
return allowances[owner][spender];
}

function transferFrom(address from, address to, uint amount) public returns (bool) {
uint currentAllowance = allowances[from][msg.sender];
require(currentAllowance >= amount, "insufficient allowance");
allowances[from][msg.sender] = currentAllowance - amount;
_transfer(from, to, amount);
return true;
}

function _transfer(address from, address to, uint amount) internal {
require(from != address(0), "from zero");
require(to != address(0), "to zero");
require(balances[from] >= amount, "balance too low");

balances[from] -= amount;
balances[to] += amount;

emit Transfer(from, to, amount);
}

function _mint(address to, uint amount) internal {
require(to != address(0), "mint to zero");
totalSupply += amount;
balances[to] += amount;
emit Transfer(address(0), to, amount);
}

function _burn(address from, uint amount) internal {
require(from != address(0), "burn zero");
require(balances[from] >= amount, "burn too much");
balances[from] -= amount;
totalSupply -= amount;
emit Transfer(from, address(0), amount);
}
}

核心函数

我来帮你逐个讲解你这个 ERC20 合约里的核心函数,以及如何在 Remix 的 Deployed Contracts 页面里测试它们。我们按功能来拆解,尽量结合操作示例,直观易懂。


1️⃣ balanceOf

1
function balanceOf(address account) public view returns (uint)

作用:

  • 查询某个地址的 代币余额
  • 不修改区块链状态,只是读取。

使用方法:

  1. 部署合约后,在 Deployed Contracts 找到 balanceOf 函数。
  2. 输入一个地址(比如部署者地址)。
  3. 点击 call → 返回该地址的代币数量。

注意:

  • 如果还没有 mint 过代币,余额是 0。

2️⃣ transfer

1
function transfer(address to, uint amount) public returns (bool)

作用:

  • 将自己账户里的代币转给别人。
  • 调用者是 msg.sender(也就是发送者),只会扣自己的余额。

使用方法:

  1. 在 Deployed Contracts 找到 transfer

  2. 输入:

    • to → 对方地址
    • amount → 代币数量
  3. 点击 transact(这里会修改链上状态)。

  4. 使用 balanceOf 可以验证转账是否成功。

注意:

  • 余额不足会 revert → 报 "balance too low"
  • 不能给 0 地址转账 → 报 "to zero"

3️⃣ approve

1
function approve(address spender, uint amount) public returns (bool)

作用:

  • 授权别人(spender)可以 从你的账户花费一定数量的代币
  • 并不会真正转币,只是设置 allowance。

使用方法:

  1. 找到 approve 函数。

  2. 输入:

    • spender → 被授权者地址
    • amount → 允许花费的代币数量
  3. 点击 transact → 会触发 Approval 事件。

  4. 可以通过 allowance 查看授权额度。

注意:

  • 没有余额也可以 approve,只是别人 transferFrom 时会失败。

4️⃣ allowance

1
function allowance(address owner, address spender) public view returns (uint)

作用:

  • 查询 ownerspender 授权了多少代币。

使用方法:

  1. 找到 allowance

  2. 输入:

    • owner → 授权人地址
    • spender → 被授权人地址
  3. 点击 call → 返回剩余可用额度。


5️⃣ transferFrom

1
function transferFrom(address from, address to, uint amount) public returns (bool)

作用:

  • 被授权者(msg.sender)从别人账户 from 转代币到 to

  • 需要:

    • from 已经 approve msg.sender 足够额度
    • from 有足够余额

使用方法:

  1. Alice approve Bob 50 个 token
  2. Bob 调用 transferFrom(Alice, Charlie, 20)
  3. _allowances[Alice][Bob] 会减少 20
  4. _balances[Alice] 扣 20,_balances[Charlie] 加 20

注意:

  • 余额或授权不足会 revert
  • msg.sender = 调用这个函数的人

6️⃣ _mint

1
function _mint(address to, uint amount) internal

作用:

  • 铸造新的代币给某个地址
  • 增加 totalSupply
  • 增加 to 的余额

使用方法(Deployed Contracts 不能直接调):

  • _mintinternal,只能在合约内部调用(比如部署时或写一个 public mint 函数)。
  • 如果想测试,需要在合约里加一个 public 包装函数:
1
2
3
function mint(address to, uint amount) public {
_mint(to, amount);
}
  • 然后在 Deployed Contracts 调用 mint,自己给自己增加代币。

7️⃣ _burn

1
function _burn(address from, uint amount) internal

作用:

  • 销毁代币
  • 扣除 from 的余额
  • 减少 totalSupply

使用方法(Deployed Contracts 不能直接调):

  • _mint,需要一个 public 包装函数:
1
2
3
function burn(uint amount) public {
_burn(msg.sender, amount);
}
  • 然后调用 burn,会减少你账户的代币和总量。

8️⃣ 在 Deployed Contracts 中测试流程示例

假设你想完整测试:

  1. mint 给自己代币

    • 如果部署时没有 initial supply,需要写 public mint
    • mint(自己地址, 100)
  2. 查询余额

    • balanceOf(自己地址) → 100
  3. transfer 给别人

    • transfer(朋友地址, 10)
    • balanceOf(自己) → 90
    • balanceOf(朋友) → 10
  4. 授权别人

    • approve(朋友地址, 20)
  5. 别人调用 transferFrom

    • balanceOf(朋友) = 0
    • 朋友调用 transferFrom(自己, 朋友, 10)
    • balanceOf(自己) → 80
    • balanceOf(朋友) → 10
    • allowance(自己, 朋友) → 10
  6. burn 自己代币

    • burn(5)
    • balanceOf(自己) → 75
    • totalSupply 减少 5

如果你愿意,我可以帮你画一张 ERC20 流程图,把 balanceOf、transfer、approve、transferFrom、mint、burn 一张图展示出来,流程直观、链上操作对应关系清晰。

你希望我画吗?

这个代码里好像没有代币,转账的时候老是失败

对,你观察得非常准确——你现在之所以 转账总是失败,是因为你的合约里 没有给任何地址分配代币

让我们分析一下你这个 ERC20 合约的情况:


1️⃣ 为什么转账失败

在你的 _transfer 函数里有这个检查:

1
require(balances[from] >= amount, "balance too low");
  • balances 是一个 mapping,默认所有地址的余额都是 0。
  • 你部署合约时没有调用 _mint,所以 任何地址的余额都是 0
  • 所以当你调用 transfertransferFrom 时,都会 revert 报 "balance too low"

2️⃣ 如何给自己代币

你的 _mintinternal,所以 Deployed Contracts 页面不能直接调用。你有两种办法:

方法 A:在合约里加一个 public mint 包装函数

1
2
3
function mint(address to, uint amount) public {
_mint(to, amount);
}

然后:

  1. 在 Deployed Contracts 找到 mint
  2. 输入 to = 你的地址, amount = 100
  3. 点击 transact → 你账户就有 100 个代币
  4. 再用 balanceOf 查看余额,确认成功

方法 B:部署时直接初始化余额

1
2
3
4
5
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
_mint(msg.sender, 1000); // 部署者初始拥有 1000 个代币
}
  • 部署完成后,部署者账户自动有代币
  • 然后直接可以 transfer 给别人

3️⃣ 测试顺序示例

  1. 给自己 mint 100 个代币
  2. balanceOf(自己) → 100
  3. transfer(朋友, 10) → 成功
  4. balanceOf(朋友) → 10

💡 总结

  • ERC20 不像 ETH,部署合约后 不会自动给你代币
  • 必须通过 _mint 给自己分配,或者在 constructor 里初始化
  • 没有代币,任何转账都会 "balance too low"

如果你愿意,我可以帮你写一个 完整可在 Remix 测试的 ERC20 版本,自带 mint/transfer/approve/transferFrom 流程,部署就能直接测试转账。

你希望我直接帮你写吗?

deploy


输入名字alice,输入字符1234,点击transact。成功部署了ERC20合约

name\ symbol


成功获取到alice和1234

mint


在mint输入eth和100,点击transact
给这个eth地址转100个代币

输入balanceof,和eth地址,输入call
显示100
说明eth地址有100个代币

注意: 如果你的合约没有mint说明,你在sol代码中没有添加public
要在合约里加一个 public mint 包装函数

1
2
3
function mint(address to, uint amount) public {
_mint(to, amount);
}

📌 逐模块深度解析(DeFi 开发 & Auditing 必须掌握)


🔹 1. 状态变量(name/symbol/decimals/totalSupply)

ERC20 规定:

  • name — 代币名称(Human readable)
  • symbol — 代币符号
  • decimals=18 — 大多数代币默认 18 位(仿照 ETH)
  • totalSupply — 总发行量

注意:ERC20 标准本身没有规定 decimals 必须是 18,USDC 是 6。


🔹 2. 核心数据结构:balances & allowances

📍 balance(余额)

1
mapping(address => uint) balances;

记录每个地址的 Token 数量。

📍 allowance(授权)

1
mapping(address => mapping(address => uint)) allowances;

表示“owner 授权给 spender 可以花费 owner 的多少 Token”。

你未来看 AMM、DEX、路由器、桥,会发现 90% 操作都是:

1
2
approve(router, amount)
router.transferFrom(user, ...)

🔹 3. event(日志)

ERC20 标准规定了两个必须的 event:

Transfer

1
emit Transfer(from, to, amount);

用途:

  • 前端(React, Dapp)监听代币转账
  • 区块浏览器(Etherscan)展示转账记录
  • The Graph 做索引

Approval

1
emit Approval(owner, spender, amount);

应用场景:

  • 前端钱包(Metamask)弹出“授权金额”窗口
  • DEX 调用 transferFrom 前必须看到授权成功

🔹 4. transfer / transferFrom 是 ERC20 的核心

transfer

1
transfer(to, amount)

直接从 msg.sender 转给 to。

这是最常见的路径。


transferFrom

1
transferFrom(from, to, amount)

逻辑:

  1. spender(msg.sender)必须已经获得 owner 的授权
  2. allowance 要足够
  3. 花掉授权额度
  4. 最终调用 _transfer

所有 DEX(Uniswap)都用 transferFrom 实现 swap。


🔹 5. _transfer / _mint / _burn 是内部功能

真实项目永远不会让用户直接调用 _mint/_burn,你会在子合约里这样写:

1
_mint(msg.sender, 1000 ether);

_transfer 是逻辑核心——你未来写的任何代币模型(税费、销毁、黑名单、额度控制),都需要重写 _transfer 来实现。


📘 你的第一个阅读任务:请你回答这 7 个问题

这是为了确保你真的理解了 ERC20 逻辑,而不是只读一遍。

请你回答:


① 为什么 transfer 不直接写逻辑,而是调用内部函数 _transfer?

因为任何代币模型都需要重写_transfer来实现。
我猜测调用内部函数_transfer来能节省gas费。
直接写逻辑,在遇到多次转账的时候就需要写多次的逻辑,不如调用内部函数_transfer方便

你的答案 非常接近标准答案,而且已经有明显的“工程思维 + 可扩展性意识”了。
下面我将用 专业 Solidity 工程师 / DeFi 开发者 / Auditor 的角度,帮你把这个问题补全到完全体。


① 为什么 transfer 不直接写逻辑,而是调用内部函数 _transfer?

你的三个理由都成立,但还没有触及“核心原因”。
我帮你拆成 5 个必须掌握的高级理由(你已经答对了其中 3 个)。


最终标准答案(专业级)

💠 1. 避免逻辑重复(你答对)

ERC20 核心转账逻辑有两处需要调用:

  • transfer()
  • transferFrom()

如果不抽出 _transfer,你就要写两份完全一样的代码,看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
function transfer(...) {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(...);
}

function transferFrom(...) {
require(balances[from] >= amount);
balances[from] -= amount;
balances[to] += amount;
emit Transfer(...);
}

重复代码 = bug 概率翻倍

抽一个 _transfer
一个地方改动,两处逻辑同步。

✔ 你已正确说到这一点。


💠 2. 子合约方便扩展(你答对)

比如你想做:

  • 黑名单代币(BlackList)
  • 征税代币(3% Tax)
  • 铸造奖励代币(Mining Token)
  • 反射代币(Reflection Token)

全部都要基于 _transfer 扩展。

例如:

1
2
3
4
5
6
7
8
function _transfer(address from, address to, uint amount) internal override {
require(!blacklist[from] && !blacklist[to], "blacklisted");
uint tax = amount * 3 / 100;
uint real = amount - tax;

super._transfer(from, to, real);
super._transfer(from, treasury, tax);
}

如果 transfer 和 transferFrom 都有独立逻辑,你就 无法统一重写

✔ 你也答到了这一点。


💠 3. 节省 Gas(你猜得对)

理由非常具体:

  • _transfer 在 bytecode 中只有一份
  • transfertransferFromjump 到同一段逻辑
  • 如果重复写,会让 bytecode 增加几十字节,部署费增加
  • 执行时也会多消耗 gas(因为指令更多)

所以:

共享内部函数 = 节省部署成本 + 节省调用成本

你直觉很准。


💠 4. 提高可审计性(真正的关键)

在 ERC20 这种规模化使用的合约里,“可审计性”是非常关键的考量。

抽象出 _transfer 后:

  • 核心逻辑全部在一个函数里 → 审计只需重点看 _transfer
  • 任何外部操作最终都要走 _transfer
  • Auditor 只要找到一个函数就能检查全部风险点

如果逻辑散落在 2-3 个函数里,审计难度会上升很多。

优秀智能合约 = 可读性高 = 审计清晰 = 风险更低


💠 5. 允许多重继承组合扩展(高级)

例如:

  • ERC20
  • ERC20Burnable(可烧毁)
  • ERC20Pausable(可暂停)
  • ERC20Permit(EIP-2612)
  • ERC20Votes(治理)
  • ERC20FlashMint(闪电铸币)

它们全部依赖 _transfer

OpenZeppelin 的组合方式是:

1
2
3
4
5
ERC20
├── ERC20Burnable
├── ERC20Pausable
├── ERC20Votes
└── ERC20FlashMint

全部 override _transfer 实现功能叠加。

如果每个地方都写自己的一套 transfer 逻辑,会直接爆炸。


🎯 总结(你可以记住这句话)

ERC20 抽出 _transfer 的根本原因不是语法,而是工程设计:复用、可扩展、可审计、可继承、可组合、节省 Gas。

你给出的三点:

  • 重写方便 ✔
  • 不重复逻辑 ✔
  • gas 可能更省 ✔

都是正确的基础理由,已经不错。
但以上五点才是完整的专业答案。


② transferFrom 为什么需要 allowance,而 transfer 不需要?

在deployed contracts里面,transferFrom有三个参数,from,to,amount
transfer只需要两个参数to,amount。
根据这一行
allowances[from][msg.sender] = currentAllowance - amount;
transferFrom需要allowance,因为需要发送者支付转账手续费,是从现有的allowance减去数量
transfer不需要,因为没有from参数,默认扣除了msg.sender的转账手续费。

你的答案方向基本正确,但还没有抓到 ERC20 设计的最核心动机。
我来带你把这题补全到 DeFi 开发者 / 审计师级别的完整理解


② transferFrom 为什么需要 allowance,而 transfer 不需要?

你的理解里有三点是对的:

  • transfer 的“from”永远是 msg.sender,所以默认自己给别人转 → 不需要授权 ✔
  • transferFrom 的调用者(msg.sender)不是 from → 需要授权 ✔
  • allowance 每次使用都会减少(扣减授权额度)✔

这些全部对,但下面是行业级的“核心理由”。


🔥 ERC20 标准设计 transferFrom 的真正原因:授权第三方代你花钱

🧠 transfer:自己花自己的钱
1
transfer(to, amount)

相当于:

“我自己给别人转钱,不需要别人授权我。”

msg.sender 本来就是要花钱的人,所以不需要 allow。


🧠 transferFrom:让别人代你花钱(核心设计)
1
transferFrom(from, to, amount)

这里出现了三个角色:

参数 意义
from 钱来自谁(代扣对象)
to 钱转给谁
msg.sender 实际执行交易的人(第三方)

举例:

用户 U = from
Uniswap 路由器 = msg.sender

当用户在 Uniswap Swap ETH → Token 时:

  1. 用户先授权:

    1
    approve(Uniswap, 100 USDC)
  2. Uniswap 自动调用:

    1
    transferFrom(U, Pool, 100 USDC)

这就是为什么它必须要 allowance!

transferFrom 是“受托花钱”机制。
需要用户提前授权,否则任何人都可以随便扣你的钱。


🔥 你提到的 “手续费” 部分 —— 有一点误解

你写:

transferFrom 需要 allowance,因为需要发送者支付手续费,是从现有的 allowance 减去数量

这里你把“手续费(gas)”和“代扣金额”混在了一起。

######### ❌ allowance 扣的不是手续费

######### ✔ 扣的是 “from 地址要被扣的 token 数量”

手续费(gas)永远由 msg.sender 支付,与 allowance 无关。

流程应该是:

  • user(from)支付 Token
  • caller(msg.sender)支付 gas

这是 ERC20 的核心安全模型。


🔥 最专业的解释(你可以记住这一句)

transferFrom 存在的原因,是让第三方(如 DEX、合约、机器人)代你执行转账。
为了防止第三方乱扣钱,必须 require 授权,即 allowance。


🎓 你已经理解了 90%,我把你的答案升级后的总结写给你:

你理解的是:

  • transfer 默认用 msg.sender 当 from → 不需要授权
  • transferFrom 有独立的 from → 必须授权

补充的专业版理解:

  • transferFrom 是“第三方代扣模式”
  • 为了安全必须用 allowance 明确用户授权额度
  • DEX / lending / routers / NFT 市场全部依赖 transferFrom
  • allowance 减少的是“可扣额度”,不是手续费
  • 手续费永远由交易发起者(msg.sender)付

你现在已经完全理解了。

为什么不能用户自己 transfer,而是要让“第三方代扣 from 的钱”?

答案很简单:

因为用户不可能在每一次复杂 DeFi 操作中手动一步步转账。
transferFrom 让智能合约自动化处理整个流程。


🚀 1. 你在 DeFi 里使用一个功能 ≠ 单次转账,而是至少 3–10 次操作

如果没有 transferFrom,你做任何 DeFi 操作都会变成:

  • 用户手动转账一次
  • 用户再手动调用一个函数
  • 再转账
  • 再调用
  • 再转账……

人类根本不会这样用。

举例你就懂了👇


📌 黄金案例:用户在 Uniswap 上买一枚代币

不使用 transferFrom 会发生什么?
👇

❌ 如果没有 transferFrom(很麻烦)

要买 100 USDC 价值的 ETH:

  1. 用户先 transfer(USDC → UniswapPool, 100)
  2. 然后用户再手动调用 swap()
  3. 再手动确认返回代币
  4. 最后 Uniswap 再手动把 ETH 转回给你

而且每一步都要 用户亲手执行 → 完全无法自动化。

这是 Web2 操作,不是 DeFi。


✔ 如果使用 transferFrom(智能合约自动处理)

用户只做一件事:

1
approve(UniswapRouter, 1000 USDC)

授权一次之后,Uniswap 可以自动为你做:

  1. transferFrom(from=用户, to=Pool, amount) → 自动代扣
  2. 进行交换
  3. 把结果 token 自动发送给你

也就是:

一次点击,合约完成多步操作

这就是 DeFi 的自动化本质。


📌 2. DeFi 操作几乎都需要“合约代扣”

下面每个真实场景都必须使用 transferFrom:


🏦 (场景 1)在借贷协议 Aave / Compound 中抵押资产

用户要抵押 1000 USDC。

如果没有 transferFrom:

  • 用户手动 transfer USDC 给合约
  • 用户再调用 deposit
  • 合约还要检查余额是否正确
  • 流程超级麻烦,还容易出错

✔ 有 transferFrom:

用户只做一步:

1
approve(LendingPool, 1000 USDC)

然后 LendingPool 执行:

1
transferFrom(user → LendingPool, 1000)

完全自动抵押。


🧠 (场景 2)在 NFT 市场 OpenSea 购买 NFT

NFT 市场需要你付款,但市场本身不能动你的钱,除非你授权。

所以流程是:

1
2
approve(OpenSea, amount)
sellContract.transferFrom(buyer, seller, amount)

否则 NFT 市场无法收款。


🔁 (场景 3)做 DCA(定投)、自动换币、自动付费机器人

比如:

  • 每天自动买 10 USDC 的 ETH
  • 自动支付手续费
  • 自动做市 AMM rebalance

这些都无法让用户每天手动执行:

1
2
3
4
transfer()
transfer()
swap()
transfer()

全部必须由机器人或合约完成 → transferFrom 必须存在。


💰 (场景 4)Token 的授权给 spender(比如质押池)

质押池必须从用户钱包扣 Token:

1
2
approve(StakingPool, 100 Token)
StakingPool.transferFrom(user → Pool, 100)

否则用户无法质押(质押本质上就是把代币转入池中)。


📦 (场景 5)跨链桥(Bridge)锁定资产

桥需要锁你的 Token,然后在另一个链铸造。

流程:

1
2
approve(Bridge, amount)
Bridge.transferFrom(user → BridgeVault, amount)

没有 transferFrom 就没法锁你的 Token。


🎮 (场景 6)链游 GameFi 自动扣用户的 Token 购买道具

你不会每买一次武器就手动转账。

所以:

1
2
approve(Game, unlimited)
Game.transferFrom(playergameTreasury, price)

游戏自动扣费。


🕹 (场景 7)订阅付费 Web3 Subscription

比如:

  • 每月自动扣 10 USDC
  • 每天扣 gas
  • 自动续费会员 NFT
  • 自动支付预言机手续费(Chainlink)

全部必须 transferFrom,否则需要你手动转账 → 完全无法自动化。


📌 如果没有 transferFrom,就没有 DeFi。

DeFi 的核心是:

自动化金融

自动化意味着:

  • 合约要有权限代替用户执行动作
  • 用户只需要做授权
  • 后续所有多步流程自动完成

没有 transferFrom,所有 DeFi 操作都要你自己执行 10+ 步。

这不叫 DeFi,这叫 Web2 手动流程。


🎯 核心总结(写得非常清晰)

transfer:自己给别人转钱,不需要授权

transferFrom:让合约或第三方自动代你扣钱,所以必须授权

transferFrom 是 ERC20 里 最重要 的函数,没有它:

  • 没有 DEX
  • 没有借贷
  • 没有质押
  • 没有 NFT 市场
  • 没有链游
  • 没有订阅
  • 没有跨链桥
  • 没有机器人
  • 没有任意复杂交易组合

Web3 会直接倒退回单机时代。