ERC-20 合约概览
简介
以太坊最常见的用途之一是由一个团队来打造一种可以交易的代币,在某种意义上是他们自己的货币。 这些代币通常遵循一个标准, ERC-20。 此标准使得人们能够以此来开发可以用于所有 ERC-20 代币的工具,如流动资金池和钱包。 在这篇文章中,我们将带领大家分析 OpenZeppelin Solidity ERC20 实现(opens in a new tab)以及 ERC20 接口定义(opens in a new tab)。
这里使用的是附加说明的源代码。 如果想要实现 ERC-20, 请阅读此教程(opens in a new tab)。
接口
像 ERC-20 这样的标准,其目的是允许符合标准的多种代币,都可以在应用程序之间进行互操作,例如钱包和分布式交易所。 为实现这个目的,我们要创建一个 接口(opens in a new tab)。 任何需要使用代币合约的代码 可以在接口中使用相同的定义,并且与使用它的所有代币合约兼容。无论是像 MetaMask 这样的钱包、 诸如 etherscan.io 之类的去中心化应用程序,或一种不同的合约,例如流动资金池。
如果你是一位经验丰富的程序员,你可能记得在 Java(opens in a new tab) 中,甚至在 C 头文件(opens in a new tab) 中看到过类似的构造。
这是来自 OpenZeppelin 的 ERC-20 接口(opens in a new tab) 的定义。 这是将人类可读标准(opens in a new tab)转换为 Solidity 代码。 当然, 接口本身并不定义如何做事。 这一点在下文合约的源代码中作了解释。
1// SPDX-License-Identifier: MIT复制
Solidity 文件中一般需要标识软件许可证。 你可以在这里看到许可证列表(opens in a new tab)。 如果需要不同的 许可证,只需在注释中加以说明。
1pragma solidity >=0.6.0 <0.8.0;复制
Solidity 语言仍在迅速地发展,新版本可能不适配旧的代码 (请点击此处查看(opens in a new tab))。 因此,最好不仅指定一个最低的 语言版本,也指定一个最高的版本,即测试过代码的最新版本。
1/**2 * @dev Interface of the ERC20 standard as defined in EIP.3 */复制
注释中的 @dev
是 NatSpec 格式(opens in a new tab)的一部分,用于 从源代码生成文档。
1interface IERC20 {复制
根据惯例,接口名称以 I
开头。
1 /**2 * @dev Returns the amount of tokens in existence.3 */4 function totalSupply() external view returns (uint256);复制
此函数标记为 external
,表示它只能从合约之外调用(opens in a new tab)。 它返回的是合约中代币的总供应量 这个值按以太坊中最常见的类型返回,即无符号的 256 位(256 位是 以太坊虚拟机的原生字长宽度)。 此函数也是视图 view
类型,这意味着它不会改变合约状态,这样它可以在单个节点上执行,而不需要在区块链的每个节点上执行。 这类函数不会生成交易,也不会消耗燃料。
注意:理论上讲,合约创建者可能会通过返回比实际数量少的总供应量来做骗局,让每个代币 比实际看起来更有价值。 然而,这种担忧忽视了区块链的真正内涵。 所有在区块链上发生的事情都要通过每个节点 进行验证。 为了实现这一点,每个合约的机器语言代码和存储都可以在每个节点上找到。 虽然无需发布你的合约代码,但这样其它人都不会认真对待你,除非你发布源代码和用于编译的 Solidity 版本,这样人们可以用它来验证你提供的机器语言代码。 例如,请查看此合约(opens in a new tab)。
1 /**2 * @dev Returns the amount of tokens owned by `account`.3 */4 function balanceOf(address account) external view returns (uint256);复制
顾名思义,balanceOf
返回一个帐户的余额。 以太坊帐户在 Solidity 中通过 address
类型识别,该类型有 160 位。 它也是 external
和 view
类型。
1 /**2 * @dev Moves `amount` tokens from the caller's account to `recipient`.3 *4 * Returns a boolean value indicating whether the operation succeeded.5 *6 * Emits a {Transfer} event.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);复制
transfer
函数将代币从调用者地址转移到另一个地址。 这涉及到状态的更改,所以它不是 view
类型。 当用户调用此函数时,它会创建交易并消耗燃料。 还会触发一个 Transfer
事件,以通知区块链上的所有人。
该函数有两种输出,对应两种不同的调用:
- 直接从用户接口调用函数的用户。 此类用户通常会提交一个交易 并且不会等待响应,因为响应可能需要无限期的时间。 用户可以查看交易收据 (通常通过交易哈希值识别)或者查看
Transfer
事件,以确定发生了什么。 - 将函数作为整个交易一部分调用的其他合约 这些合约可立即获得结果, 由于它们在相同的交易里运行,因此可以使用函数返回值。
更改合约状态的其他函数创建的同类型输出。
限额允许帐户使用属于另一位所有者的代币。 比如,当合约作为卖方时,这个函数就很实用。 合约无法 监听事件,如果买方要将代币直接转给卖方合约, 该合约无法知道已经获得付款。 因此,买方允许 卖方合约支付一定的额度,而让卖方转账相应金额。 这通过卖方合约调用的函数完成,这样卖方合约 可以知道是否成功。
1 /**2 * @dev Returns the remaining number of tokens that `spender` will be3 * allowed to spend on behalf of `owner` through {transferFrom}. This is4 * zero by default.5 *6 * This value changes when {approve} or {transferFrom} are called.7 */8 function allowance(address owner, address spender) external view returns (uint256);复制
allowance
函数允许任何人查询一个 地址 (owner
) 给另一个地址 (spender
) 的许可额度。
1 /**2 * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.3 *4 * Returns a boolean value indicating whether the operation succeeded.5 *6 * IMPORTANT: Beware that changing an allowance with this method brings the risk7 * that someone may use both the old and the new allowance by unfortunate8 * transaction ordering. One possible solution to mitigate this race9 * condition is to first reduce the spender's allowance to 0 and set the10 * desired value afterwards:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emits an {Approval} event.14 */15 function approve(address spender, uint256 amount) external returns (bool);显示全部复制
approve
函数创建了一个许可额度。 请务必阅读关于 如何避免函数被滥用的信息。 在以太坊中,你可以控制自己交易的顺序, 但无法控制其他方交易的执行顺序, 除非在看到其他方的交易发生之前 不提交你自己的交易。
1 /**2 * @dev Moves `amount` tokens from `sender` to `recipient` using the3 * allowance mechanism. `amount` is then deducted from the caller's4 * allowance.5 *6 * Returns a boolean value indicating whether the operation succeeded.7 *8 * Emits a {Transfer} event.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);显示全部复制
最后,消费者使用 transferFrom
函数用来使用许可额度。
12 /**3 * @dev Emitted when `value` tokens are moved from one account (`from`) to4 * another (`to`).5 *6 * Note that `value` may be zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by12 * a call to {approve}. `value` is the new allowance.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}显示全部复制
在 ERC-20 合约状态发生变化时就会激发这些事件。
实际合约
这是实现 ERC-20 标准的实际合约, 摘自此处(opens in a new tab)。 不能照原样使用,但可以 通过继承(opens in a new tab)将其扩展,使之可用。
1// SPDX-License-Identifier: MIT2pragma solidity >=0.6.0 <0.8.0;复制
导入声明
除了上述接口定义外,合约定义还要导入两个其他文件:
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";复制
GSN/Context.sol
是使用 OpenGSN(opens in a new tab) 所需的文件,该系统允许用户在没有以太币的情况下 使用区块链。 请注意,这里的文件是旧版本,如果需要集成 OpenGSN, 请使用此教程(opens in a new tab)。- SafeMath 库(opens in a new tab),用于 完成没有溢出问题的加法和减法。 这非常必要,否则会出现,用户仅有一个代币,花掉 两个代币后,反而有了 2^256-1 个代币。
这里的注释说明了合约的目的。
1/**2 * @dev Implementation of the {IERC20} interface.3 *4 * This implementation is agnostic to the way tokens are created. This means5 * that a supply mechanism has to be added in a derived contract using {_mint}.6 * For a generic mechanism see {ERC20PresetMinterPauser}.7 *8 * TIP: For a detailed writeup see our guide9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How10 * to implement supply mechanisms].11 *12 * We have followed general OpenZeppelin guidelines: functions revert instead13 * of returning `false` on failure. This behavior is nonetheless conventional14 * and does not conflict with the expectations of ERC20 applications.15 *16 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.17 * This allows applications to reconstruct the allowance for all accounts just18 * by listening to said events. Other implementations of the EIP may not emit19 * these events, as it isn't required by the specification.20 *21 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}22 * functions have been added to mitigate the well-known issues around setting23 * allowances. See {IERC20-approve}.24 */25显示全部复制
合约定义
1contract ERC20 is Context, IERC20 {复制
此行为 OpenGSN 指定继承,在本例中来自上面的 IERC20
和 Context
。
12 using SafeMath for uint256;3复制
此行将 SafeMath
库附加到 uint256
类型。 你可以在 此处(opens in a new tab)找到此程序库。
变量的定义
这些定义具体指定了合约的状态变量。 虽然声明这些变量为 private
,但 这只意味着区块链上的其他合约无法读取它们。 区块链上 没有秘密,所有节点上的软件在每个区块上 都有每个合约的状态。 根据惯例,状态变量名称为 _<something>
。
前两个变量是映射(opens in a new tab), 表示它们的结果与关联数组(opens in a new tab)相同, 不同之处在于关键词为数值。 存储空间仅分配给数值不同于 默认值(零)的条目。
1 mapping (address => uint256) private _balances;复制
第一个映射,_balances
,是代币地址和对应的余额。 要查看 余额,请使用此语法:_balances[<address>]
。
1 映射 (address => mapping (address => uint256)) private _allowances;复制
此变量,_allowances
存储之前提到过的许可限额。 第一个索引是 代币的所有者,第二个索引是获得许可限额的合约。 要查询地址 A 可以 从地址 B 帐户中支出的额度,请使用 _allowances[B][A]
。
1 uint256 private _totalSupply;复制
顾名思义,此变量记录代币供应总量。
1 string private _name;2 string private _symbol;3 uint8 private _decimals;复制
这三个变量用于提高可读性。 前两项的含义不言自明,但 _decimals
并非如此。
一方面,以太坊不具有浮点数或分数变量。 另一方面, 人们希望能够拆分代币。 人们选择将黄金做为货币的一个原因是 当有人想要购买一只牛的一小部分时,就很难找零。
解决方案是保持整数值,但是计数时使用一个价值非常小的分数代币, 而不是真正的代币。 就以太币而言,分数代币称为 wei,10^18 个 wei 等于一个 以太币。 在撰写本文时,10,000,000,000,000 wei 约等于一美分或欧分。
应用程序需要知道如何显示代币余额。 如果某位用户有 3,141,000,000,000,000,000 wei,那是否是 3.14 个以太币? 31.41 个以太币? 还是 3,141 个以太币? 对于以太币,10^18 个 wei 等于 1 个以太币,但对于你的 代币,你可以选择一个不同的值。 如果无法合理拆分代币,你可以将 _decimals
值设为零。 如果想要使用与以太币相同的标准,请使用 18。
构造函数
1 /**2 * @dev Sets the values for {name} and {symbol}, initializes {decimals} with3 * a default value of 18.4 *5 * To select a different value for {decimals}, use {_setupDecimals}.6 *7 * All three of these values are immutable: they can only be set once during8 * construction.9 */10 constructor (string memory name_, string memory symbol_) public {11 _name = name_;12 _symbol = symbol_;13 _decimals = 18;14 }显示全部复制
构造函数在首次创建合约时调用。 根据惯例,函数参数名为 <something>_
。
用户接口函数
1 /**2 * @dev Returns the name of the token.3 */4 function name() public view returns (string memory) {5 return _name;6 }78 /**9 * @dev Returns the symbol of the token, usually a shorter version of the10 * name.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**17 * @dev Returns the number of decimals used to get its user representation.18 * For example, if `decimals` equals `2`, a balance of `505` tokens should19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).20 *21 * Tokens usually opt for a value of 18, imitating the relationship between22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is23 * called.24 *25 * NOTE: This information is only used for _display_ purposes: it in26 * no way affects any of the arithmetic of the contract, including27 * {IERC20-balanceOf} and {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }显示全部复制
这些函数,name
、symbol
和 decimals
帮助用户界面了解合约,从而正常演示合约。
返回类型为 string memory
,意味着返回在内存中存储的字符串。 变量,如 字符串,可以存储在三个位置:
有效时间 | 合约访问 | 燃料成本 | |
---|---|---|---|
内存 | 函数调用 | 读/写 | 几十到几百不等(距离越远费用越高) |
调用数据 | 函数调用 | 只读 | 不可用作返回类型,只可用作函数参数 |
存储 | 直到被修改 | 读/写 | 高(读取需要 800,写入需要 2万) |
在这种情况下,memory
是最好的选择。
读取代币信息
这些是提供代币信息的函数,不管是总量还是 帐户余额。
1 /**2 * @dev See {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }复制
totalSupply
函数返回代币的总量。
1 /**2 * @dev See {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }复制
读取一个帐户的余额。 请注意,任何人都可以查看他人帐户的余额。 试图隐藏此信息没有意义,因为它在每个节点上 都是可见的。 区块链上没有秘密
代币转账
1 /**2 * @dev See {IERC20-transfer}.3 *4 * Requirements:5 *6 * - `recipient` cannot be the zero address.7 * - the caller must have a balance of at least `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {显示全部复制
调用 transfer
函数以从发送人的帐户转移代币到另一个帐户。 注意 虽然函数返回的是布尔值,但那个值始终为真实值。 如果转账失败, 合约会撤销调用。
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }复制
_transfer
函数完成了实际工作。 这是一个私有函数,只能由 其他合约函数调用。 根据常规,私人函数名为 _<something>
,与状态 变量相同。
在 Solidity 中,我们通常使用 msg.sender
代表信息发送人。 然而,这会破坏 OpenGSN(opens in a new tab) 的规则。 如果我们想使用代币进行交易而不用以太币,我们 需要使用 _msgSender()
。 对于正常交易,它返回 msg.sender
,但是对于没有以太币的交易, 则返回原始签名而不是传递信息的合约。
许可额度函数
这些是实现许可额度功能的函数:allowance
、approve
、transferFrom
和 _approve
。 此外,除基本标准外,OpenZeppelin 实现还包含了一些能够提高 安全性的功能:increaseAllowance
和 decreaseAllowance
。
许可额度函数
1 /**2 * @dev See {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }复制
allowance
函数使每个人都能检查任何许可额度。
审批函数
1 /**2 * @dev See {IERC20-approve}.3 *4 * Requirements:5 *6 * - `spender` cannot be the zero address.7 */8 function approve(address spender, uint256 amount) public virtual override returns (bool) {复制
调用此函数以创建许可额度。 它与上述 transfer
函数相似:
- 该函数仅调用一个完成真正工作的内部函数(本例中为
_approve
)。 - 函数要么返回
true
(如果成功),要么撤销(如果失败)。
1 _approve(_msgSender(), spender, amount);2 return true;3 }复制
我们使用内部函数尽量减少发生状态变化之处。 任何可以改变状态的 函数都是一种潜在的安全风险,需要对其安全性进行审核。 这样我们就能减少出错的机会。
TransferFrom 函数
这个函数被消费者用于使用许可额度。 这里需要两步操作:将消费的金额转账, 并在许可额度中减去这笔金额。
1 /**2 * @dev See {IERC20-transferFrom}.3 *4 * Emits an {Approval} event indicating the updated allowance. This is not5 * required by the EIP. See the note at the beginning of {ERC20}.6 *7 * Requirements:8 *9 * - `sender` and `recipient` cannot be the zero address.10 * - `sender` must have a balance of at least `amount`.11 * - the caller must have allowance for ``sender``'s tokens of at least12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);显示全部复制
a.sub(b, "message")
函数调用做了两件事。 首先,它计算了 a-b
,这是新的许可额度。 之后,它检查这一结果是否为负数。 如果结果为负,将撤销调用,并发出相应的信息。 请注意,撤销调用后,之前在调用中完成的任何处理都会被忽略,所以我们不需要 撤消 _transfer
。
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: transfer amount exceeds allowance"));3 return true;4 }复制
OpenZeppelin 安全加法
将许可额度从一个非零值设定为另一个非零值是有危险的, 因为你只能控制自己的交易顺序,而无法控制其他人的交易顺序。 假设现在有两个用户,天真的 Alice 和不诚实的 Bill。 Alice 想要从 Bill 处获取一些服务, 她认为值五个代币,所以她给了 Bill 五个代币的许可额度。
之后有了一些变化,Bill 的价格提高到了十个代币。 Alice 仍然想要购买服务,就发送了一笔交易,将 Bill 的许可额度设置为 10。 当 Bill 在交易池中看到这个新的交易时, 他就会发送一笔交易,以花费 Alice 的五个代币,并且设定高得多的 燃料价格,这样就会更快挖矿。 这样的话,Bill 可以先花五个代币,然后 当 Alice 的新许可额度放款后,他就可以再花费十个代币,这样总共花费了 15 个代币, 超过了 Alice 本欲授权的金额。 这种技术叫做 抢先交易(opens in a new tab)
Alice 的交易 | Alice 的随机数 | Bill 的交易 | Bill 的随机数 | Bill 的许可额度 | Bill 从 Alice 处获得的总收入 |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
approve(Bill, 10) | 11 | 10 | 5 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
为了避免这个问题,有两个函数(increaseAllowance
和 decreaseAllowance
)使你 能够修改指定数额的许可额度。 所以,如果 Bill 已经花费了五个代币, 他就只能再花五个代币。 根据时间的不同,有两种方法可以生效, 这两种方法都会使 Bill 最终只得到十个代币:
A:
Alice 的交易 | Alice 的随机数 | Bill 的交易 | Bill 的随机数 | Bill 的许可额度 | Bill 从 Alice 处获得的总收入 |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
Alice 的交易 | Alice 的随机数 | Bill 的交易 | Bill 的随机数 | Bill 的许可额度 | Bill 从 Alice 处获得的总收入 |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev Atomically increases the allowance granted to `spender` by the caller.3 *4 * This is an alternative to {approve} that can be used as a mitigation for5 * problems described in {IERC20-approve}.6 *7 * Emits an {Approval} event indicating the updated allowance.8 *9 * Requirements:10 *11 * - `spender` cannot be the zero address.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }显示全部复制
a.add(b)
函数是一个安全加法。 在罕见的情况下,a
+b
>=2^256
,不会发生 普通加法会出现的溢出错误。
12 /**3 * @dev Atomically decreases the allowance granted to `spender` by the caller.4 *5 * This is an alternative to {approve} that can be used as a mitigation for6 * problems described in {IERC20-approve}.7 *8 * Emits an {Approval} event indicating the updated allowance.9 *10 * Requirements:11 *12 * - `spender` cannot be the zero address.13 * - `spender` must have allowance for the caller of at least14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }显示全部复制
修改代币信息的函数
这些是完成实际工作的四个函数:_transfer
、_mint
、_burn
和 _approve
。
_transfer 函数 {#_transfer}
1 /**2 * @dev Moves tokens `amount` from `sender` to `recipient`.3 *4 * This is internal function is equivalent to {transfer}, and can be used to5 * e.g. implement automatic token fees, slashing mechanisms, etc.6 *7 * Emits a {Transfer} event.8 *9 * Requirements:10 *11 * - `sender` cannot be the zero address.12 * - `recipient` cannot be the zero address.13 * - `sender` must have a balance of at least `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {显示全部复制
_transfer
这个函数将代币从一个帐户转到另一个帐户。 有两个函数调用它,分别是 transfer
(从发送人本人帐户发送)和 transferFrom
(使用许可额度,从其他人的帐户发送)。
1 require(sender != address(0), "ERC20: transfer from the zero address");2 require(recipient != address(0), "ERC20: transfer to the zero address");复制
实际上以太坊中没有人拥有零地址(即不存在对应公钥可以转换为零地址的私钥)。 有人使用该地址时,通常是一个软件漏洞,所以 如果将零地址用作发送人或接收人,交易将失败。
1 _beforeTokenTransfer(sender, recipient, amount);2复制
使用该合约有两种方法:
- 将其作为模板,编写自己的代码
- 从它继承(opens in a new tab)一个合约,并且重写你需要修改的函数
第二种方法要好得多,因为 OpenZeppelin ERC-20 代码已经过审核,其安全性也已得到证实。 当你的合约继承它时, 可以清楚地表明修改了哪些函数,只需要审核这些特定的函数,人们就会信任你的合约。
代币每次易手时,通常都需要调用一个函数。 然而,_transfer
是一个非常重要的函数, 重新编写可能会不安全(见下文),所以最好不要重写。 解决方案是重写 _beforeTokenTransfer
函数,这是一个挂钩函数(opens in a new tab)。 你可以重写此函数,之后每次转账都会调用它。
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _balances[recipient] = _balances[recipient].add(amount);复制
这些是实际实现转账的代码。 请注意,将转账金额从发送人帐户上扣除,然后加到接收人帐户之间, 不得有任何动作。 这很重要,因为如果 中间调用不同的合约,可能会被用来骗过这个合约。 目前转账为最小操作单元,即中间什么都不会发生。
1 emit Transfer(sender, recipient, amount);2 }复制
最后,激发一个 Transfer
事件。 智能合约无法访问事件,但区块链外运行的代码 可以监听事件并对其作出反应。 例如,钱包可以跟踪所有者获得更多代币事件。
_mint 和 _burn 函数 {#_mint-and-_burn}
这两个函数(_mint
和 _burn
)修改代币的总供应量。 它们都是内部函数,在原有合约中没有任何调用它们的函数。 因此,仅通过继承合约并添加你自己的逻辑, 来决定在什么条件下可以铸造新代币或消耗现有代币时, 它们才是有用的。
注意:每一个 ERC-20 代币都通过自己的业务逻辑来决定代币管理。 例如,一个固定供应总量的合约可能只在构造函数中调用 _mint
,而从不调用 _burn
。 一个销售代币的合约 将在支付时调用 _mint
,并大概在某个时间点调用 _burn
, 以避免过快的通货膨胀。
1 /** @dev Creates `amount` tokens and assigns them to `account`, increasing2 * the total supply.3 *4 * Emits a {Transfer} event with `from` set to the zero address.5 *6 * Requirements:7 *8 * - `to` cannot be the zero address.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }显示全部复制
当代币总数发生变化时,请务必更新 _totalSupply
。
1 /**2 * @dev Destroys `amount` tokens from `account`, reducing the3 * total supply.4 *5 * Emits a {Transfer} event with `to` set to the zero address.6 *7 * Requirements:8 *9 * - `account` cannot be the zero address.10 * - `account` must have at least `amount` tokens.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");1415 _beforeTokenTransfer(account, address(0), amount);1617 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }显示全部
_burn
函数与 _mint
函数几乎完全相同,但它们的方向相反。
_approve 函数 {#_approve}
这是实际设定许可额度的函数。 请注意,它允许所有者指定 一个高于所有者当前余额的许可额度。 这是允许的,因为在转账时 会核查余额,届时可能不同于 创建许可额度时的金额。
1 /**2 * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.3 *4 * This internal function is equivalent to `approve`, and can be used to5 * e.g. set automatic allowances for certain subsystems, etc.6 *7 * Emits an {Approval} event.8 *9 * Requirements:10 *11 * - `owner` cannot be the zero address.12 * - `spender` cannot be the zero address.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");1718 _allowances[owner][spender] = amount;显示全部复制
激发一个 Approval
事件。 根据应用程序的编写, 消费者合约可以从代币所有者或监听事件的服务器获知审批结果。
1 emit Approval(owner, spender, amount);2 }3复制
修改小数点设置变量
123 /**4 * @dev Sets {decimals} to a value other than the default one of 18.5 *6 * WARNING: This function should only be called from the constructor. Most7 * applications that interact with token contracts will not expect8 * {decimals} to ever change, and may work incorrectly if it does.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }显示全部复制
此函数修改了 >_decimals
变量,此变量用于设置用户接口如何计算金额。 你应该从构造函数里面调用。 在之后的任何时候调用都是不正当的, 应用程序一般不会处理。
钩子
12 /**3 * @dev Hook that is called before any transfer of tokens. This includes4 * minting and burning.5 *6 * Calling conditions:7 *8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens9 * will be to transferred to `to`.10 * - when `from` is zero, `amount` tokens will be minted for `to`.11 * - when `to` is zero, `amount` of ``from``'s tokens will be burned.12 * - `from` and `to` are never both zero.13 *14 * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}显示全部复制
这是转账过程中要调用的挂钩函数。 该函数是空的,但如果你需要 它做一些事情,只需覆盖它即可。
总结
复习一下,这些是我认为此合约中最重要的概念(你们的看法可能与我不同)
- 区块链上没有秘密 智能合约可以访问的任何信息 都可以提供给全世界。
- 你可以控制自己交易的订单,但在其他人的交易发生时, 则不能控制。 这就是为什么更改许可额度时会有风险,因为它 允许消费者花掉这两个许可额度的总和。
uint256
类型值的溢出。 换言之,0-1=2^256-1。 如果这不是预期的 行为,你必须自行检查(或使用 SafeMath 库执行该服务)。 请注意, Solidity 0.8.0(opens in a new tab) 中对此进行了更改。- 将特定类型变量的状态改变放在一个特定的地方,这样可以使审核更容易。 这就是我们使用以下等函数的原因,例如
_approve
函数,它可以被approve
、transferFrom
、increaseAllowance
和decreaseAllowance
调用。 - 状态更改应为最小操作单元,其中没有任何其他动作 (如在
_transfer
中所见)。 这是因为在状态更改期间,会出现不一致的情况。 例如, 在减少发送人的余额,和增加接收人的余额之间, 代币总量会小于应有总量。 如果在这两个时刻之间有任何操作, 特别是调用不同的合约,则可能出现滥用。
现在你已经了解了 OpenZeppelin ERC-20 合约是怎么编写的, 尤其是如何使之更加安全,你即可编写自己的安全合约和应用程序。
上次修改时间: @nhsz(opens in a new tab), 2024年2月18日