跳转至主要内容

ERC-20 合约概览

solidityerc-20
初学者
Ori Pomerantz
2021年3月9日
35 分钟阅读 minute read

简介

以太坊最常见的用途之一是由一个团队来打造一种可以交易的代币,在某种意义上是他们自己的货币。 这些代币通常遵循一个标准, 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 之类的去中心化应用程序,或一种不同的合约,例如流动资金池。

ERC-20 接口说明

如果您是一位经验丰富的程序员,您可能记得在 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 */
复制

注释中的 @devNatSpec 格式(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 位。 它也是 externalview 类型。

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 be
3 * allowed to spend on behalf of `owner` through {transferFrom}. This is
4 * 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 risk
7 * that someone may use both the old and the new allowance by unfortunate
8 * transaction ordering. One possible solution to mitigate this race
9 * condition is to first reduce the spender's allowance to 0 and set the
10 * desired value afterwards:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
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 the
3 * allowance mechanism. `amount` is then deducted from the caller's
4 * 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 函数用来使用许可额度。

1
2 /**
3 * @dev Emitted when `value` tokens are moved from one account (`from`) to
4 * another (`to`).
5 *
6 * Note that `value` may be zero.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * 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: MIT
2pragma solidity >=0.6.0 <0.8.0;
复制

导入声明

除了上述接口定义外,合约定义还要导入两个其他文件:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
复制

这里的注释说明了合约的目的。

1/**
2 * @dev Implementation of the {IERC20} interface.
3 *
4 * This implementation is agnostic to the way tokens are created. This means
5 * 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 guide
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
10 * to implement supply mechanisms].
11 *
12 * We have followed general OpenZeppelin guidelines: functions revert instead
13 * of returning `false` on failure. This behavior is nonetheless conventional
14 * 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 just
18 * by listening to said events. Other implementations of the EIP may not emit
19 * 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 setting
23 * allowances. See {IERC20-approve}.
24 */
25
显示全部
复制

合约定义

1contract ERC20 is Context, IERC20 {
复制

此行为 OpenGSN 指定继承,在本例中来自上面的 IERC20Context

1
2 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} with
3 * 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 during
8 * 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 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
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 should
19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).
20 *
21 * Tokens usually opt for a value of 18, imitating the relationship between
22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is
23 * called.
24 *
25 * NOTE: This information is only used for _display_ purposes: it in
26 * no way affects any of the arithmetic of the contract, including
27 * {IERC20-balanceOf} and {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
显示全部
复制

这些函数,namesymboldecimals 帮助用户界面了解合约,从而正常演示合约。

返回类型为 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,但是对于没有以太币的交易, 则返回原始签名而不是传递信息的合约。

许可额度函数

这些是实现许可额度功能的函数:allowanceapprovetransferFrom_approve。 此外,除基本标准外,OpenZeppelin 实现还包含了一些能够提高 安全性的功能:increaseAllowancedecreaseAllowance

许可额度函数

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 not
5 * 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 least
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 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)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

为了避免这个问题,有两个函数(increaseAllowancedecreaseAllowance)使您 能够修改指定数额的许可额度。 所以,如果 Bill 已经花费了五个代币, 他就只能再花五个代币。 根据时间的不同,有两种方法可以生效, 这两种方法都会使 Bill 最终只得到十个代币:

A:

Alice 的交易Alice 的随机数Bill 的交易Bill 的随机数Bill 的许可额度Bill 从 Alice 处获得的总收入
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Alice 的交易Alice 的随机数Bill 的交易Bill 的随机数Bill 的许可额度Bill 从 Alice 处获得的总收入
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
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 for
5 * 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,不会发生 普通加法会出现的溢出错误。

1
2 /**
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 for
6 * 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 least
14 * `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 to
5 * 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
复制

使用该合约有两种方法:

  1. 将其作为模板,编写自己的代码
  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`, increasing
2 * 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 the
3 * 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");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _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 to
5 * 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");
17
18 _allowances[owner][spender] = amount;
显示全部
复制

激发一个 Approval 事件。 根据应用程序的编写, 消费者合约可以从代币所有者或监听事件的服务器获知审批结果。

1 emit Approval(owner, spender, amount);
2 }
3
复制

修改小数点设置变量

1
2
3 /**
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. Most
7 * applications that interact with token contracts will not expect
8 * {decimals} to ever change, and may work incorrectly if it does.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
显示全部
复制

此函数修改了 >_decimals 变量,此变量用于设置用户接口如何计算金额。 您应该从构造函数里面调用。 在之后的任何时候调用都是不正当的, 应用程序一般不会处理。

钩子

1
2 /**
3 * @dev Hook that is called before any transfer of tokens. This includes
4 * minting and burning.
5 *
6 * Calling conditions:
7 *
8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
9 * 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 函数,它可以被approvetransferFromincreaseAllowancedecreaseAllowance 调用。
  • 状态更改应为最小操作单元,其中没有任何其他动作 (如在 _transfer 中所见)。 这是因为在状态更改期间,会出现不一致的情况。 例如, 在减少发送人的余额,和增加接收人的余额之间, 代币总量会小于应有总量。 如果在这两个时刻之间有任何操作, 特别是调用不同的合约,则可能出现滥用。

现在您已经了解了 OpenZeppelin ERC-20 合约是怎么编写的, 尤其是如何使之更加安全,您即可编写自己的安全合约和应用程序。

上次修改时间: @mdranger(opens in a new tab), 2023年8月15日

本教程对你有帮助吗?