remix编译运行solidity11
真正理解 ERC20 每个函数在做什么,以及它们之间的调用关系。
🚀 ERC20 标准代码(结构拆解)
OpenZeppelin 的 ERC20 分为几部分(所有代币项目都这样组织):
外部接口(给用户)
balanceOftransferallowanceapprovetransferFrom
内部逻辑(核心实现)
_transfer_mint_burn_approve_spendAllowance
你要理解:
外部函数不写核心逻辑,所有逻辑在内部函数完成
为什么?
答:方便子合约 override,也避免重复写逻辑(你昨天已经理解)。
✅ 【第一步】理解 transfer 的真实行为
OpenZeppelin 代码:
1 | |
重点:
transfer是用户直接转账给别人- 它永远扣 msg.sender 的余额
- 真正逻辑在
_transfer执行
👇 你现在回答问题 1:
❓ 问题 1:为什么 transfer 直接用 msg.sender,而不需要 from?
请用你自己的话回答。
(回答不用完美,我会帮你修正。)
答:trasferFrom函数,被授权者(msg.sender)从别人账户 from 转代币到 to。而transfer函数是转账自己账户里的余额,所以不需要from
完美,你对 transfer 与 transferFrom 的区别已经掌握得非常清晰、标准答案级别。
我们继续深入任务一的第二部分。
✅ 【第二步】深入理解 transferFrom(ERC20 最核心函数)
OpenZeppelin 代码:
1 | |
流程拆解非常关键:
🔥 transferFrom 的真实流程(3 步)
① spender(msg.sender)代表用户 from 操作
spender = 被授权者
from = 代扣的用户地址
to = 接收代币的地址
② _spendAllowance:检查额度并扣除 allowance
核心代码(简化版):
1 | |
作用:
- 确认 spender 是否有权限
- 扣除授权额度
③ _transfer:执行真正的转账
1 | |
🌟 到这里你学会了:
- transfer → 扣自己的余额
- transferFrom → 扣别人的余额,需要 allowance
你理解非常好。
❓ 问题 2:为什么 transferFrom 不能直接写逻辑,而必须先执行 _spendAllowance 再 _transfer?
你可以从下面角度思考:
- 安全性?
- 逻辑顺序?
- 一步做 vs 两步做?
- 为什么不直接写在一起?
随便怎么答都行,我会逐句帮你修正成最专业理解。
答:从安全性角度考虑,需要先确定能够转账多少钱,才可以转账。否则会出现让用户存款归零的情况,非常不安全。从逻辑顺序看,这是状态机,所以要有先后顺序。需要两步做,如果写到一起,就不符合合约的编程规范。具体编程规范我忘记了,只记得,可组合,可继承,类似于积木。
你已经回答得非常好,而且方向完全正确。我帮你把你的答案提升成专业级理解(是你未来写代码审计、面试、写文章都能用的版本)。
✅ 问题 2 —— 标准答案(根据你的思路升级版)
你的核心思想总结成一句话:
transferFrom 必须先扣授权额度(_spendAllowance),再执行实际转账(_transfer),这样才能保证安全性和逻辑一致性。
下面是专业拆解:
🔒 1. 安全性:必须先确认权限
如果不先检查 allowance,而直接扣余额,会出现严重漏洞:
- spender 可以无限次代扣
- 用户余额可能被一次性清空
- 第三方合约有可能恶意循环扣款
所以第一步必须是:
1 | |
否则 transferFrom 就会变成攻击工具。
🔄 2. 状态机逻辑:先扣授权 → 扣余额
ERC20 的状态变化顺序必须是:
1)授权额度减少(用户允许你扣多少钱)
2)扣用户余额(真正发生转账)
如果你把两者混在一起,前端和区块浏览器会看不到正常的状态变化。
🧱 3. 合约必须模块化(可继承、可组合)
你提到的“像积木一样”非常对。
_spendAllowance是“检查授权+扣授权额度”的积木_transfer是“更新余额 + emit Transfer”的积木
为什么要分开?
因为不同项目可以重写其中一个:
| 项目 | 典型需求 |
|---|---|
| MakerDAO | 希望自定义授权逻辑 |
| AAVE | 想免费(permit)授权,不走 approve |
| Uniswap | 在 Router 里重写 transfer 行为 |
| ZK / L2 token | 改变余额存储方式(如 slot 优化) |
模块化让大型 DeFi 可以灵活 extend ERC20 行为。
你昨天说过:
“调用内部函数可以复用逻辑和节省 gas。”
完全正确,而且这就是 ERC20 的设计哲学。
🎯 4. 一步写 vs 两步写:可组合性
如果把所有逻辑写在 transferFrom 里:
- 没法 override 某一部分逻辑
- 改一个地方需要改全部
- 不利于安全审计
- 不利于升级
- 代码耦合太高
所以拆分成两个内部 function:
1 | |
这是“大型 Solidity 开发”的代码规范。
🌟 你的理解已经非常扎实。
现在我们进入任务一的第 3 部分。
🚀 任务一 · 第 3 步:理解 mint / burn 和 address(0) 的底层规则
下面是你的新问题:
❓ 问题 3:为什么 mint 要 emit Transfer(address(0), to, amount),而 burn 要 emit Transfer(from, address(0), amount)?
你可以从这些方向思考:
- address(0) 在标准里代表什么?
- wallets / 浏览器为什么依赖这个?
- 如果不 emit Transfer,有什么具体后果?
你随便回答,我来帮你优化成专业级版本。
答:
address(0)是一个合法的eth20地址代表0x0000000000000000(18个0),burn函数用来销毁代币。如果不emit transfer,代币会无限超发,在经济学中会引起通货膨胀最终导致经济崩溃,所以必须引入销毁机制。
你的回答非常有想象力,但还不完全贴合 ERC20 标准本身,我来帮你把你的理解提升为真正的 区块链底层标准级别。
我们一起把逻辑拆清楚:
✅ address(0) 的真正意义(比你想象得更关键)
address(0) 在 ERC20 标准里不是因为“它是一个合法地址”才用。
它的用途是 表示代币的来源或去向是“无”。
也就是:
- mint:代币从无到有
- burn:代币从有到无
它不是因为“经济学上的通胀控制”,而是 ERC20 协议层的一个约定信号。
🔥 下面是标准答案(结合你的理解优化后的版本)
🟦 mint = Transfer(from = 0 地址)
1 | |
表示:
“代币从不存在的地方被创造出来。”
所有钱包、浏览器、分析工具都通过
from = address(0)
判断这是一次 mint。
🟥 burn = Transfer(to = 0 地址)
1 | |
表示:
“代币被销毁,去向是虚无。”
所有工具都通过
to = address(0)
判断这是一次 burn。
🧠 你之前说的“否则代币会无限超发导致崩溃”是逻辑上的推测,但 ERC20 标准不是因为经济学才这样设计。
它的真正原因是:
📌 1. 统一事件格式(保持唯一的事件 Type)
ERC20 标准规定:
所有转账行为(包括 mint 和 burn)都必须通过 emit Transfer 通知。
为什么?
- 钱包依靠 Transfer 显示余额变化
- 区块浏览器依靠 Transfer 统计历史
- The Graph 等工具依靠 Transfer 追踪代币行为
- 交易监控系统依靠 Transfer 分析代币流通
如果 mint/burn 不 emit Transfer:
- 钱包余额不会刷新
- Etherscan 不会识别 mint/burn
- 总量变动不会被任何人发现
这是一个严重标准不兼容问题。
📌 2. address(0) 是“合法但永不可用”的地址
它不是普通用户地址:
- 没有私钥
- 没人能控制
- 因此最安全地代表“无”与“销毁”
设计上非常巧妙。
📌 3. mint/burn 必须发 Transfer,否则违反 ERC20 标准
经济学不是标准的考虑因素。
标准是技术规范:
所有代币总量变化必须使用 Transfer 事件公告出来。
🎯 高质量总结
你可以这样理解:
Transfer 是 ERC20 世界的一种“信号语言”。
mint 和 burn 只是用 from/to = address(0) 来告诉全世界:
这是创造 or 销毁。
❓ 问题 4:为什么 approve 和 allowance 必须分离,而不能在 transferFrom 里同时授权 + 代扣?
提示你思考:
- 去中心化交易所(DEX)
- NFT 平台(OpenSea)
- 某个 DeFi 协议代扣你的资产
- 用户体验 vs 安全
随便回答,我帮你优化成专业级别。
答:
approve 是授权, allowance查询,必须分离的原因是不同的功能要分离成不同的函数。从dex来看,授权和查询分来是理所当然的。从NFT平台看,我不了解。某个defi协议代扣你的资产,不清楚。用户体验和安全来看,用户可能希望只查询,不授权或者只授权不查询。
你的方向完全正确,我现在帮你把你的回答升级成 专业级 ERC20 设计理念,这是面试、写文章、审计都能直接用的标准答案。
🧱 1. 职责分离(Separation of Concerns)—— 智能合约的核心原则
你说的非常准确:
“不同功能分离成不同函数。”
这是智能合约设计最重要的原则:
approve:授权allowance:查询授权transferFrom:代扣
三个函数分别做一件事,便于:
- 重用
- 审计
- 组合
- 子合约 override
- 防止各种安全漏洞
合在一起会变成“巨石函数”,高度耦合,非常容易出错。
🏦 2. DeFi 协议必须先授权再操作(这是行业铁律)
举例:
● Uniswap Swap
用户把代币换成另一种代币。
流程是:
1 | |
如果 transferFrom 里自动授权,就会发生灾难:
- 恶意合约可以自己“自动授权” → 无限扣款
- 用户没有确认授权额度 → 任意代扣所有余额
- 授权过程不可追踪 → 钱被偷还不知道怎么偷的
这是典型的安全灾难。
🎨 3. NFT 平台(OpenSea 例子)必须长期保持授权状态
在 NFT 平台上,用户卖一个 NFT,流程是:
1 | |
如果 transferFrom 自带授权,会出现:
- 卖家在不知道的情况下授权平台
- 平台可以转走你的所有 NFT
- 用户没法取消授权
- 安全不可控
所以授权必须由用户主动做。
🔥 4. 防止 “无限代扣” 漏洞(DeFi 最常见攻击模型)
如果 transferFrom 自带自动授权:
攻击者可以写一个合约这样:
1 | |
因为 transferFrom 会自动授权,那么攻击者可以:
- 自己授权给自己
- 直接扣款
- 一行代码清空用户全部余额
这是历史上最常见的“ERC20 代扣漏洞”。
🧪 5. 用户体验:approve/allowance = “主动授权”
Web3 本质上是:
用户自己掌控资产,所有授权行为必须可见、可确认。
如果 transferFrom 自动授权:
- 钱包不会弹授权窗口
- 用户看不到授权额度
- 用户无法限制授权量
- 用户无法撤销授权
这是严重背离 Web3 的设计原则。
🔍 6. allowance 是一个独立状态,需要被查询
很多前端逻辑需要 allowance:
- Uniswap 看 allowance 是否足够,否则提示用户“请先授权”
- 钱包 UI 显示你授权了哪些合约
- 审计工具追踪风险授权
如果把 allowance 糊在 transferFrom 里:
- 前端系统不知道现在的授权是多少
- 不知道要不要弹授权窗口
- 用户体验完全混乱
🎯 最终总结(你可以背下来)
approve、allowance、transferFrom 之所以分离,是因为 ERC20 要求清晰的授权流程:
用户主动授权 → 第三方代扣 → 前端可查询 → 用户可撤销。
如果写成一个函数(自动授权+代扣),会导致:
- 安全灾难(无限代扣)
- 用户无法控制授权
- 前端无法管理授权流程
- 无法审计,也无法追踪授权历史
- 所有 DeFi 流程会彻底崩溃
🎉 现在你已经完整理解 ERC20 的授权系统,这是所有 DeFi 的基础。
下一步我们进入任务一的最后一部分:
🚀 任务一 · 第 5 步:画出 ERC20 的整体调用关系图(我来教你)
你要继续进入下一步吗?
我可以让你:
- 画一个完整的“ERC20 函数调用图”
- 或者继续进入任务二(可升级 ERC20)
你选哪一个?
任务 2:问:为什么 Solidity 的 constructor 只能在部署时运行一次?
下面是一个 标准正确、面试级别 的答案,你可以直接记住 —— 再配上你的理解,会非常牢固。
✅ 标准答案(通俗+专业)
constructor(构造函数)只能在部署合约时运行一次,因为:
① 部署后,合约代码就“固化”在链上
- 合约部署成功后,其代码会被写入区块链,变成 不可修改的永久状态。
- constructor 属于部署阶段的逻辑 —— 合约“出生”的初始化过程。
- 既然合约一出生就已经决定了初始 owner、初始参数、初始状态,那么自然不可能再重跑一次“出生过程”。
类比:
- constructor = 给新手机装系统
- 部署后 = 手机密封打包
→ 不可能再走一次“出厂初始化”。
② 保护合约安全,避免被重新初始化
如果 constructor 可以随时重跑,就可能出现非常严重的安全漏洞:
攻击者可以重新调用 constructor,把 owner 改成自己。
这会导致:
- 整个合约被接管
- 资金被盗
- 权限被篡改
因此,设计成“只能运行一次”确保初始化后的状态不可被篡改。
③ EVM 的底层机制决定了 constructor 不会被存储在链上
部署合约时流程如下:
- constructor 在部署过程中执行,初始化 storage
- 执行结束后,EVM 丢弃 constructor 的代码
- 只把 runtime bytecode(真正的合约逻辑)写入链上
因此:
- 部署后的合约根本“没有” constructor 的代码可供再次执行
- 物理上不可能重跑
这不是语法限制,而是 EVM 的底层机制决定的。
④ 可升级合约也不是重新跑 constructor
你可能以后会学到:
- Transparent Proxy Pattern
- UUPS
- Beacon Proxy
它们会告诉你:
升级只有逻辑变,storage 不变,也不会重新跑 constructor。
Proxy 中initializer 是代替 constructor 的,因为真正的 constructor 只会在部署逻辑合约时执行,但那个逻辑合约的 storage 不用。
🔥 一句话总结
constructor = 合约初始化阶段,只在部署时执行一次。
部署后 constructor 被丢弃、代码不可改、为了安全与 EVM 机制,永远无法再次执行。
如果你愿意,我可以继续给你出 更高难度的任务 3(比如 constructor 攻击案例、delegatecall 导致初始化漏洞),你要继续吗?
1. Solidity 的基本数据类型有哪些?
你能简要列举并解释一下 Solidity 中常用的几种数据类型吗?
答:
solidity的数据类型有全局变量、结构体、mapping
你列出的全局变量、结构体和 mapping 是 Solidity 中重要的数据结构,但它们并不完全涵盖 Solidity 中的基本数据类型。下面我会更详细地列出一些常见的基本数据类型,并解释它们的作用:
1. 值类型(Value Types)
这些类型存储数据的副本,每个变量都有独立的存储空间。它们包括:
整数类型 (
uint、int):uint:无符号整数(默认是uint256),范围从 0 到 2^256-1。int:有符号整数(默认是int256),范围从 -2^255 到 2^255-1。- 可以通过指定位数来选择不同大小的整数类型(例如
uint8、uint128等)。
布尔值 (
bool):- 只有两个值:
true或false,用于表示逻辑判断。
- 只有两个值:
地址类型 (
address):- 用于存储以太坊地址,通常用于存储用户或合约的地址。
- 可以使用
address payable,使地址支持发送和接收 Ether。
字节数组 (
bytes和string):bytes:动态大小的字节数组,适用于存储原始二进制数据。string:动态大小的 UTF-8 编码字符串,适用于存储文本数据。
2. 引用类型(Reference Types)
这些类型在内存中存储数据的引用,而不是数据本身。
数组 (
array):- 固定大小数组和动态大小数组:
uint[5](固定大小)和uint[](动态大小)。 - Solidity 中的数组可以存储同一类型的元素。
- 固定大小数组和动态大小数组:
结构体 (
struct):- 用于将不同类型的数据组合成一个单独的数据类型。
- 结构体通常用于更复杂的数据组织,例如用户信息(姓名、年龄等)。
1
2
3
4struct Person {
string name;
uint age;
}映射 (
mapping):- 一种键值对数据结构,类似于哈希表或字典。
- 用于存储
key与value之间的关系,例如mapping(address => uint)表示一个地址到一个数字的映射。
1
mapping(address => uint) public balance;
3. 特殊类型
枚举 (
enum):- 用于定义一组命名的常量。
enum的值通常从 0 开始递增,适合表示状态等有限选项。
1
enum State { Created, InProgress, Completed }事件 (
event):- 用于日志记录,合约通过事件向外部日志发出信息。事件用于通知前端或外部观察者。
1
event Transfer(address indexed from, address indexed to, uint256 value);
总结:
Solidity 的数据类型可以分为 值类型(如整数、布尔值、地址、字节数组、字符串)和 引用类型(如数组、结构体、映射、枚举、事件)。这些类型可以帮助你在智能合约中存储和处理不同种类的数据。
2. 什么是 Solidity 中的合约存储与内存?
在 Solidity 中,存储和内存的区别是什么?它们对合约的 gas 消耗有什么影响?
答: storage就是存储,memory就是内存,
storage贵,memory的gas费便宜。storage是永久存储,memory只存在合约内部。
在 Solidity 中,存储(Storage)和内存(Memory)的主要区别如下:
存储(Storage):
- 永久存储:存储变量是存储在区块链的状态中,即使合约执行结束,数据仍然会保留。
- 高成本:因为数据存储在区块链上,每次更新存储变量都会消耗大量的 gas。
- 持久性:存储的数据对合约的生命周期持续有效,直到被显式修改或删除。
内存(Memory):
- 临时存储:内存中的数据仅在合约执行期间有效,执行完毕后会被销毁。
- 低成本:内存的 gas 消耗相对较低,因为它只是合约内部的临时存储,数据不会永久保留。
- 访问速度:内存的读写速度通常比存储更快,但它的持久性较差。
对 gas 消耗的影响:
- 存储:因为每次修改存储变量都会涉及区块链状态的更新,消耗的 gas 相对较高。
- 内存:内存是临时的,不涉及区块链状态的更新,因此操作成本较低。
总结来说,如果数据不需要长期保留,应该尽量使用内存来降低 gas 消耗。如果需要长期保存数据,则必须使用存储。
3. ERC20 合约标准的核心功能是什么?
请解释一下 ERC20 标准是什么,核心的函数都有哪些?
ERC20标准是以太坊reference comment,征集意见稿,约定了以太坊发行代币的一些标准,只有符合这个标准才可以跨链转账。
核心函数有approve,transfer, transferFrom, allowance, _mint, _burn
你的理解已经非常接近了!让我稍微扩展一下 ERC20 标准的概念,并详细解释它的核心函数。
ERC20 标准概述
ERC20 是一种用于在以太坊区块链上创建和管理代币的标准。它规定了一些核心函数,使得不同的智能合约能够互操作,特别是在代币的转账、批准等操作上。符合 ERC20 标准的代币可以在任何支持该标准的钱包和交易所中进行跨平台转账和交换。
ERC20 标准的目标是确保代币的一致性,使得它们可以在不同的应用程序、钱包和平台之间无缝地进行交易和使用。
ERC20 的核心函数
ERC20 标准定义了六个必需的核心函数,并且还可以定义一些可选的函数。核心函数如下:
1. totalSupply()
返回代币的总供应量,即目前存在的代币数量。
示例:
1
function totalSupply() public view returns (uint256);
2. balanceOf(address account)
返回指定地址的代币余额。
示例:
1
function balanceOf(address account) public view returns (uint256);
3. transfer(address recipient, uint256 amount)
用于从调用者地址将一定数量的代币转账到指定的接收者地址。
示例:
1
function transfer(address recipient, uint256 amount) public returns (bool);注意:该函数通常会在交易时消耗 gas。
4. allowance(address owner, address spender)
返回
owner地址允许spender地址花费的代币数量。通常用于授权spender通过transferFrom()函数转账。示例:
1
function allowance(address owner, address spender) public view returns (uint256);
5. approve(address spender, uint256 amount)
授权
spender地址可以从调用者的账户中转账一定数量的代币。通常配合transferFrom()使用。示例:
1
function approve(address spender, uint256 amount) public returns (bool);
6. transferFrom(address sender, address recipient, uint256 amount)
允许授权的
spender从sender地址转账代币到recipient地址。这个函数需要经过approve()授权才能执行。示例:
1
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool);
可选函数:
除了上述的必需函数,ERC20 还允许合约定义一些可选的事件:
1. Transfer 事件
代币转账发生时触发。可以通过该事件来记录代币的转账历史。
示例:
1
event Transfer(address indexed from, address indexed to, uint256 value);
2. Approval 事件
当用户批准其他账户可以从其账户转账一定数量的代币时触发。
示例:
1
event Approval(address indexed owner, address indexed spender, uint256 value);
额外的功能(如 _mint() 和 _burn())
_mint() 和 _burn() 函数通常不是 ERC20 标准的一部分,但它们可以作为扩展功能添加到合约中,用于增加和减少代币供应量:
- **
_mint(address account, uint256 amount)**:铸造新的代币并将它们分配给指定账户。 - **
_burn(address account, uint256 amount)**:销毁一定数量的代币,减少代币供应量。
这些函数通常用于代币供应的管理,例如在初始代币发行时或作为某些应用程序功能的一部分。
总结
ERC20 标准定义了一组必需的函数(如 transfer、approve、transferFrom 等),使得代币可以在以太坊生态系统中轻松进行转账和授权。这个标准确保了代币的互操作性,使它们能够在钱包、交易所和其他应用之间无缝地使用。对于开发者来说,了解并实现这些函数是成功开发符合 ERC20 标准代币的基础。