提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 简单代币合约 (使用ERC-20)
- [openzeppelin 实现ERC20](#openzeppelin 实现ERC20)
简单代币合约 (使用ERC-20)
什么是ERC-20
初学者可以简单的理解为这个是一个同质化代币的标准,ERC-20 提出了一个同质化代币的标准,换句话说,它们具有一种属性,使得每个代币都与另一个代币(在类型和价值上)完全相同。
以太坊文档
官方定义文档
简单代币合约
上一篇文章我们已经在WETH案例实现了简单的代表合约
ERC20核心函数
- 3 个查询
- balanceOf: 查询指定地址的 Token 数量
- allowance: 查询指定地址对另外一个地址的剩余授权额度
- totalSupply: 查询当前合约的 Token 总量
- 2 个交易
- transfer: 从当前调用者地址发送指定数量的 Token 到指定地址。(直接转账)
这是一个写入方法,所以还会抛出一个 Transfer 事件。 - transferFrom: 当向另外一个合约地址存款时,对方合约必须调用 transferFrom 才可以把 Token 拿到它自己的合约中。( 授权转账)
- transfer: 从当前调用者地址发送指定数量的 Token 到指定地址。(直接转账)
- 2 个事件
- Transfer
- Approval
- 1 个授权
- approve: 授权指定地址可以操作调用者的最大 Token 数量。
授权机制详解
授权机制的工作原理
为什么需要授权机制?
问题场景:
Alice想在Uniswap上用USDT购买ETH。这个过程中,Uniswap合约需要获取Alice的USDT。
为什么不能用transfer?
bash
// transfer只能由代币持有者自己调用
function transfer(address to, uint256 amount) public returns (bool) {
// msg.sender必须是代币持有者
balanceOf[msg.sender] -= amount;
// ...
}
// Uniswap合约无法调用Alice的transfer
// 因为msg.sender会是Uniswap合约地址,不是Alice
问题的核心:
智能合约无法主动获取用户的代币。如果没有授权机制,合约就无法代表用户操作代币。
授权机制的解决方案:
用户主动授权合约
合约代表用户操作
用户通过控制授权额度保持控制权
这是一种委托代理模式。
授权流程详解
让我们通过一个完整的场景来理解授权机制。
场景:Alice在Uniswap用USDT购买ETH
步骤1:Alice授权Uniswap
bash
// Alice调用USDT合约的approve函数
usdt.approve(uniswapAddress, 1000);
执行过程:
1. allowance[Alice][Uniswap] = 1000
2. 触发Approval事件
3. 返回true
状态变化:
- Alice的USDT余额:不变(仍然是2000)
- Uniswap的授权额度:1000
- 代币位置:仍在Alice账户中
关键点:approve只是设置授权额度,并不转移代币!
步骤2:Uniswap使用授权
bash
// Uniswap合约调用transferFrom
usdt.transferFrom(Alice, Pool, 500);
执行过程:
1. 检查:Alice余额 >= 500? ✓(2000 >= 500)
2. 检查:allowance[Alice][Uniswap] >= 500? ✓(1000 >= 500)
3. Alice余额 -= 500(2000 → 1500)
4. Pool余额 += 500
5. allowance[Alice][Uniswap] -= 500(1000 → 500)
6. 触发Transfer事件
7. 返回true
最终状态:
- Alice的USDT余额:1500(减少了500)
- Pool的USDT余额:500(增加了500)
- 剩余授权额度:500(被消耗了500)
关键点:授权额度会被消耗,不是一次性使用全部!
完整流程图
bash
初始状态:
Alice余额:2000 USDT
Pool余额:0 USDT
授权额度:0
↓ approve(Uniswap, 1000)
状态1:
Alice余额:2000 USDT
Pool余额:0 USDT
授权额度:1000 ←(已授权)
↓ transferFrom(Alice, Pool, 500)
最终状态:
Alice余额:1500 USDT ←(减少500)
Pool余额:500 USDT ←(增加500)
授权额度:500 ←(消耗500)
授权机制的实际应用
- 应用场景1:去中心化交易所(Uniswap)
- 应用场景2:流动性挖矿
- 应用场景3:NFT购买
- 应用场景4:借贷协议(Compound/Aave)
授权安全注意事项
bash
// 危险:授权最大值
token.approve(contract, type(uint256).max);
// 相当于把全部代币的控制权交给了合约
问题:
- 如果合约有漏洞,所有代币都可能被盗
- 授权一次永久有效,风险持续存在
- 恶意合约可以随时转走全部代币
bash
// 安全:只授权需要的数量
token.approve(uniswap, 100); // 只授权本次交易需要的100个
// 使用后撤销授权
token.approve(uniswap, 0); // 撤销授权
授权安全原则:
- 最小授权:只授权实际需要的数量
- 使用后撤销:完成操作后立即撤销授权
- 只授权可信合约:只对经过审计的知名合约授权
- 定期检查:定期检查并撤销不再需要的授权
- 使用授权管理工具:使用Revoke.cash等工具管理授权
真实案例:
许多用户因为无限授权损失了资金:
- 2021年某DeFi协议被攻击,用户损失数百万美元
- 攻击者利用用户的无限授权转走代币
- 只有撤销授权的用户幸免
教训:永远不要给不熟悉的合约无限授权!
openzeppelin 实现ERC20
简单理解openzeppelin是一个标准化实现的组织,提供一些标准化的包。下面是ERC20实现的一个例子,官网有其他demo也可以自己试试
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC20} from"@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
contract Mytoken is ERC20,ERC20Permit{
constructor () ERC20("Mytoken","MTK") ERC20Permit("Mytoken"){
_mint(msg.sender,100000);
}
}
这里还涉及两个之前没有提到内容,
- 导包 import
- 继承 contract Mytoken is ERC20,ERC20Permit
和其他语言实现类似,看一下基本就理解了
上面的例子实现了给自己铸造代币,我们可以按下面的步骤,导入自己的钱包看下
部署合约
部署合约,可以选择sepolia测试网,然后复制合约地址

在钱包添加代币
使用上面负责的合约地址,添加代币

等一下显示后点击导入即可,成功后如下图所示

尝试使用代币其他功能
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 导入 OpenZeppelin 官方合约
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MyToken2
* @dev 带权限控制的 ERC20 代币:
* - 部署时向部署者铸造初始供应量(自动乘以 18 位小数)
* - 仅合约拥有者可增发代币
* - 任意用户可销毁自己的代币
*/
contract MyToken2 is ERC20, Ownable {
// 代币小数位(显式声明,默认 18,可按需修改)
uint8 public constant DECIMALS = 18;
/**
* @dev 构造函数
* @param initialSupply 初始供应量("枚"数,自动转换为最小单位)
*/
constructor(uint256 initialSupply)
ERC20("My Token", "MTK")
Ownable(msg.sender) // 显式指定初始拥有者(OpenZeppelin v5+ 必需)
{
// 初始铸造:将"枚"数转换为最小单位(18 位小数)
_mint(msg.sender, initialSupply * 10 ** DECIMALS);
}
/**
* @dev 增发代币(仅拥有者可调用)
* @param to 接收增发代币的地址
* @param amount 增发数量("枚"数,自动转换为最小单位)
*/
function mint(address to, uint256 amount) public onlyOwner {
// 校验:增发数量不能为 0
require(amount > 0, "Mint amount cannot be zero");
// 转换为最小单位后铸造
_mint(to, amount * 10 ** DECIMALS);
}
/**
* @dev 销毁自己的代币
* @param amount 销毁数量("枚"数,自动转换为最小单位)
*/
function burn(uint256 amount) public {
// 校验:销毁数量不能为 0
require(amount > 0, "Burn amount cannot be zero");
// 校验:余额足够(ERC20 的 _burn 会自动校验,此处显式提示更友好)
require(balanceOf(msg.sender) >= amount * 10 ** DECIMALS, "Insufficient balance to burn");
// 转换为最小单位后销毁
_burn(msg.sender, amount * 10 ** DECIMALS);
}
// 可选:重写 decimals 函数(若需修改小数位,比如 6 位)
// function decimals() public view override returns (uint8) {
// return DECIMALS;
// }
}