0%

OpenZeppelin ERC-20 详解

这是一段openzeppelin官方生成的代码

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
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract BobToken is ERC20, ERC20Pausable, AccessManaged, ERC20Permit {
constructor(address initialAuthority)
ERC20("BobToken", "BBK")
AccessManaged(initialAuthority)
ERC20Permit("BobToken")
{}

function pause() public restricted {
_pause();
}

function unpause() public restricted {
_unpause();
}

function mint(address to, uint256 amount) public restricted {
_mint(to, amount);
}

// The following functions are overrides required by Solidity.

function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}
}

这段代码定义了一个名为BobToken的智能合约,它继承了多个标准接口和合约,用于创建一个可暂停、可授权管理、可使用EIP-2612标准的许可(Permit)功能的代币。下面是对代码的详细解释:

继承的合约和接口

  1. ERC20: ERC20是一个标准接口,定义了代币的基本功能,如总供应量、余额查询、转账等。
  2. ERC20Pausable: 这个接口允许代币的转账操作被暂停,从而防止在特定情况下(如紧急情况)的代币转移。
  3. AccessManaged: 这个接口可能定义了访问控制功能,比如只有特定地址(如管理员)才能执行某些操作。
  4. ERC20Permit: 这个接口允许代币持有者通过签名授权来批准第三方转移代币,而不需要直接调用转账函数。

继承顺序

在Solidity中,合约的继承顺序是从左到右的,即最左边的合约是基类,最右边的合约是派生类。例如,在contract BobToken is ERC20, ERC20Pausable, AccessManaged, ERC20Permit {}中,BobToken继承了ERC20、ERC20Pausable、AccessManaged和ERC20Permit。

继承原则

  1. 从左到右的继承顺序:如上所述,继承顺序是从左到右的,这意味着基类在派生类之前被继承。
  2. 构造函数的调用:在派生类的构造函数中,必须显式地调用所有基类的构造函数。调用顺序必须遵循继承顺序,即从左到右。
  3. 函数覆盖:如果基类和派生类中有同名的函数,那么派生类的函数会覆盖基类的函数。在调用这些函数时,会优先调用派生类的函数。
  4. 库的使用:如果合约继承了一个库,那么库的函数会被覆盖。这是因为库的函数是内部函数,不能被覆盖。

构造函数

1
2
3
4
5
constructor(address initialAuthority)
ERC20("BobToken", "BBK")
AccessManaged(initialAuthority)
ERC20Permit("BobToken")
{}
  • initialAuthority: 初始化合约的管理权限地址。
  • 构造函数中调用了多个基类的构造函数,传递了必要的参数,如代币名称、符号、初始权限地址等。ERC20、AccessManaged和ERC20Permit的构造函数被显式地调用,并且按照继承顺序从左到右调用。

主要函数

  1. pause(): 暂停代币的转账操作。只有通过AccessManaged接口授权的地址才能调用。
  2. unpause(): 恢复代币的转账操作。同样,只有授权的地址才能调用。
  3. mint(address to, uint256 amount): 允许授权地址铸造新的代币,并将其发送给指定的地址。这通常用于代币发行。

内部函数

1
2
3
4
5
6
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}

_update函数是一个内部函数,用于更新代币的余额。它重写了ERC20ERC20Pausable中的同名函数,并在内部调用了super._update,确保了在更新余额时考虑了暂停状态。

注意事项

  • 访问控制: AccessManaged接口确保了只有授权的地址才能执行关键操作,如暂停、铸造等。
  • 暂停功能: ERC20Pausable接口提供了暂停和恢复转账操作的功能,这在处理紧急情况时非常有用。
  • EIP-2612 Permit标准: 通过实现ERC20Permit接口,用户可以授权第三方代币转移,而无需直接调用转账函数,这提高了安全性。

总的来说,这段代码实现了一个功能丰富的代币合约,结合了多种安全性和管理功能,适用于需要严格控制访问和操作的场景。接下来,我们按照继承顺序逐个来介绍一下。

Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}

function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}

function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}

这段代码定义了一个名为Context的抽象合约。这个合约主要用于提供关于当前执行上下文的信息,包括交易的发送者和数据。在处理元交易(meta-transactions)时,发送和支付执行费用的账户可能不是实际的应用程序发送者。

实现原理

  1. _msgSender()函数:这个函数返回当前交易的发送者地址。在大多数情况下,这可以通过msg.sender直接获取,但在处理元交易时,这个值可能不是实际的发送者。因此,这个函数提供了一个抽象层,允许子合约重写它以提供正确的发送者地址。

  2. _msgData()函数:这个函数返回当前交易的输入数据。同样,在大多数情况下,这可以通过msg.data直接获取,但在处理元交易时,这个值可能不是实际的输入数据。因此,这个函数提供了一个抽象层,允许子合约重写它以提供正确的输入数据。

  3. _contextSuffixLength()函数:这个函数返回一个长度值,表示上下文后缀的长度。这个函数在处理元交易时可能有用,但在这个合约中默认返回0,表示没有上下文后缀。

用途

Context合约的主要用途是为那些需要访问当前执行上下文信息的合约提供基础。例如,如果一个合约需要知道是谁发送了交易,或者交易中包含了哪些数据,它可以继承Context合约并使用_msgSender()_msgData()函数。

注意事项

  • 继承:任何需要使用Context合约功能的合约都应该继承它。如果子合约需要提供不同的发送者或数据,它应该重写_msgSender()_msgData()函数。
  • 元交易:处理元交易时,发送和支付执行费用的账户可能不是实际的发送者。因此,任何依赖于msg.sendermsg.data的合约都需要能够处理这种情况。
  • 安全性:在重写_msgSender()_msgData()函数时,需要确保提供的信息是安全的,以防止潜在的安全漏洞。

ERC20

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";

abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
...
}

定义了一个名为 ERC20 的抽象合约,它继承了四个接口:Context、IERC20、IERC20Metadata 和 IERC20Errors。

  1. abstract contract ERC20
  • abstract contract 表示这是一个抽象合约,不能直接实例化。它通常用于定义接口和共享逻辑,其他合约可以继承它。
  • ERC20 是合约的名称。
  1. is Context, IERC20, IERC20Metadata, IERC20Errors
  • Context 是一个包含上下文相关函数(如 msg.sender)的接口,通常用于简化合约编写。
  • IERC20 是 ERC20 标准的接口,定义了 ERC20 代币的基本功能,如 totalSupply、balanceOf、transfer 等。
  • IERC20Metadata 是 ERC20 标准的扩展接口,定义了代币的元数据,如 name、symbol 和 decimals。
  • IERC20Errors 是一个自定义的接口,通常用于定义合约中可能出现的错误,如 TransferFailedError、InsufficientBalanceError 等。

合约成员

1
2
3
4
5
6
7
8
mapping(address account => uint256) private _balances;

mapping(address account => mapping(address spender => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;
  1. _balances:

    • 这是一个映射(mapping),用于存储每个账户的代币余额。
    • address account 是账户地址,uint256 是该地址对应的代币数量。
    • private 关键字表示这个映射只能在合约内部访问,外部无法直接访问。
    • 这个映射用于跟踪每个账户的代币余额。
  2. _allowances:

    • 这是一个嵌套的映射,用于存储每个账户授权给其他账户的代币数量。
    • address account 是账户地址,mapping(address spender => uint256) 是一个映射,表示该账户授权给其他账户的代币数量。
    • address spender 是被授权的账户地址,uint256 是被授权的代币数量。
    • private 关键字表示这个映射只能在合约内部访问,外部无法直接访问。
    • 这个映射用于跟踪每个账户授权给其他账户的代币数量,通常用于实现代币的转账授权功能。
      -
  3. _totalSupply:

    • 这是一个无符号整数(uint256),用于存储代币的总供应量。
    • private 关键字表示这个变量只能在合约内部访问,外部无法直接访问。
    • 这个变量用于跟踪代币的总供应量。
      -
  4. _name:

    • 这是一个字符串变量,用于存储代币的名称。
    • private 关键字表示这个变量只能在合约内部访问,外部无法直接访问。
    • 这个变量用于存储代币的名称,例如 “MyToken”。
  5. _symbol:

    • 这是一个字符串变量,用于存储代币的符号。
    • private 关键字表示这个变量只能在合约内部访问,外部无法直接访问。
    • 这个变量用于存储代币的符号,例如 “MTK”。

transfer & transferFrom

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
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}

function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}

function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}

function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}

if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}

emit Transfer(from, to, value);
}
  • 所有的转让动作最终都由_transfer方法来完成。
  • _transfer方法中统一判断地址的有效性,使用revertrequire更节省gas。
  • ERC20InvalidSenderERC20InvalidReceiver都来自IERC20Errors接口。

_update

_update 为内部虚函数,用于更新代币合约中的余额和总供应量。它主要用于处理代币的转移操作,确保在转移过程中不会发生溢出或不足的情况。

  1. 参数说明

    • from:代币的发送者地址。
    • to:代币的接收者地址。
    • value:转移的代币数量。
  2. 更新总供应量

    • 如果 fromaddress(0),表示这是一个铸造操作(minting),那么 _totalSupply 将增加 value
      1
      2
      3
      4
      5
      6
      function _mint(address account, uint256 value) internal {
      if (account == address(0)) {
      revert ERC20InvalidReceiver(address(0));
      }
      _update(address(0), account, value);
      }
    • 如果 from 不是 address(0),表示这是一个转移操作(transfer),那么首先检查 from 的余额是否足够,如果足够,则从 from 的余额中减去 value
  3. 更新接收者的余额

    • 如果 toaddress(0),表示这是一个销毁操作(burning),那么 _totalSupply 将减少 value
      1
      2
      3
      4
      5
      6
      function _burn(address account, uint256 value) internal {
      if (account == address(0)) {
      revert ERC20InvalidSender(address(0));
      }
      _update(account, address(0), value);
      }
    • 如果 to 不是 address(0),表示这是一个转移操作,那么将 value 加到 to 的余额中。
  4. 防止溢出

    • 使用 unchecked 关键字来处理可能的溢出情况,因为代码逻辑已经确保了不会发生溢出。
    • 使用 unchecked 可以节省gas
  5. 触发事件

    • 最后,通过 emit Transfer(from, to, value); 触发一个 Transfer 事件,记录这次转移操作。

approve

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
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}

function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}

function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}

_approve函数

_approve函数用于更新一个账户对另一个账户的授权额度。如果emitEvent参数为true,还会触发一个Approval事件。

- `owner`:授权额度所有者的地址。
- `spender`:被授权的账户地址。
- `value`:授权的额度。
- `emitEvent`:一个布尔值,决定是否触发`Approval`事件。

流程

  1. 首先检查ownerspender是否为address(0),如果是,则抛出异常ERC20InvalidApproverERC20InvalidSpender
  2. 更新_allowances映射,将ownerspender的授权额度设置为value
  3. 如果emitEventtrue,则触发Approval事件。

_spendAllowance函数

_spendAllowance函数用于消耗ownerspender的授权额度。它不会更新授权额度,除非授权额度是有限的。如果授权额度不足,则会抛出异常。

- `owner`:授权额度所有者的地址。
- `spender`:被授权的账户地址。
- `value`:要消费的额度。

流程

  1. 获取ownerspender的当前授权额度。
  2. 如果当前授权额度不是无限(即不等于type(uint256).max),则检查当前授权额度是否足够消费value
  3. 如果足够,则更新ownerspender的授权额度,减去value,并触发Approval事件(如果emitEventtrue)。

decimals

1
2
3
function decimals() public view virtual returns (uint8) {
return 18;
}

默认值是18, 如果想改变,只能重写decimals()这个方法。这种方法直接返回固定值的相比于使用成员变量存储的方式更省gas。

ERC20Pausable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Pausable.sol)

pragma solidity ^0.8.20;

import {ERC20} from "../ERC20.sol";
import {Pausable} from "../../../utils/Pausable.sol";

abstract contract ERC20Pausable is ERC20, Pausable {
/**
* @dev See {ERC20-_update}.
*
* Requirements:
*
* - the contract must not be paused.
*/
function _update(address from, address to, uint256 value) internal virtual override whenNotPaused {
super._update(from, to, value);
}
}

ERC20Pausable 是个抽象合约,它继承了 ERC20Pausable 两个合约。ERC20 是一个标准的代币合约接口,而 Pausable 是一个用于暂停合约操作的合约。

实现原理

  1. 继承关系ERC20Pausable 继承了 ERC20Pausable 合约,这意味着它继承了 ERC20 的所有功能(如代币的创建、转移、余额查询等)以及 Pausable 的功能(如暂停和恢复合约操作)。

  2. 抽象合约ERC20Pausable 是一个抽象合约,因为它包含了一个未实现的函数 _update

  3. 函数重写_update 函数重写了 ERC20 合约中的 _update 函数。这个函数在执行代币转移操作之前,会检查合约是否处于暂停状态。如果合约处于暂停状态,则不允许执行转移操作。

  4. whenNotPaused 修饰器whenNotPausedPausable 合约中的一个修饰器,用于确保在调用被修饰的函数时,合约不是暂停状态。如果合约是暂停状态,调用将被 revert(回滚)。

用途

ERC20Pausable 合约的主要用途是提供一个可暂停的代币合约,允许合约所有者或管理员在紧急情况下暂停代币的转移操作,以防止潜在的滥用或攻击。

注意事项

  1. 实现 _update 函数:任何继承 ERC20Pausable 的合约都必须实现 _update 函数,以确保在执行代币转移操作时能够正确地检查合约是否处于暂停状态。

  2. 合约暂停:合约暂停后,所有与代币转移相关的操作都将被阻止,直到合约被恢复。因此,合约所有者或管理员在暂停合约时应谨慎操作,以避免造成不必要的损失。

  3. 安全性:虽然 ERC20Pausable 合约提供了一种暂停合约的方法,但它并不能防止所有潜在的安全问题。开发者仍需确保合约的其他部分(如访问控制、逻辑错误等)是安全的。

AccessManaged

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
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/manager/AccessManaged.sol)

pragma solidity ^0.8.20;

import {IAuthority} from "./IAuthority.sol";
import {AuthorityUtils} from "./AuthorityUtils.sol";
import {IAccessManager} from "./IAccessManager.sol";
import {IAccessManaged} from "./IAccessManaged.sol";
import {Context} from "../../utils/Context.sol";

abstract contract AccessManaged is Context, IAccessManaged {
address private _authority;

bool private _consumingSchedule;
constructor(address initialAuthority) {
_setAuthority(initialAuthority);
}

modifier restricted() {
_checkCanCall(_msgSender(), _msgData());
_;
}

/// @inheritdoc IAccessManaged
function authority() public view virtual returns (address) {
return _authority;
}

/// @inheritdoc IAccessManaged
function setAuthority(address newAuthority) public virtual {
address caller = _msgSender();
if (caller != authority()) {
revert AccessManagedUnauthorized(caller);
}
if (newAuthority.code.length == 0) {
revert AccessManagedInvalidAuthority(newAuthority);
}
_setAuthority(newAuthority);
}

/// @inheritdoc IAccessManaged
function isConsumingScheduledOp() public view returns (bytes4) {
return _consumingSchedule ? this.isConsumingScheduledOp.selector : bytes4(0);
}

function _setAuthority(address newAuthority) internal virtual {
_authority = newAuthority;
emit AuthorityUpdated(newAuthority);
}

function _checkCanCall(address caller, bytes calldata data) internal virtual {
(bool immediate, uint32 delay) = AuthorityUtils.canCallWithDelay(
authority(),
caller,
address(this),
bytes4(data[0:4])
);
if (!immediate) {
if (delay > 0) {
_consumingSchedule = true;
IAccessManager(authority()).consumeScheduledOp(caller, data);
_consumingSchedule = false;
} else {
revert AccessManagedUnauthorized(caller);
}
}
}
}

AccessManaged 是个抽象合约,它继承了 ContextIAccessManaged 接口。这个合约的主要目的是管理合约的访问权限,确保只有授权的地址才能调用特定的函数。下面是对代码的详细解释:

合约结构

  1. 状态变量

    • _authority:存储当前合约的授权地址。
    • _consumingSchedule:一个布尔值,用于指示是否正在执行预定的操作。
  2. 构造函数

    • constructor(address initialAuthority):初始化合约,设置初始的授权地址。
  3. 修饰器

    • modifier restricted():限制函数的访问,确保只有授权的地址才能调用。这个修饰器通过调用 _checkCanCall 函数来检查调用者是否有权限。
  4. 接口实现

    • function authority() public view virtual returns (address):返回当前的授权地址。
    • function setAuthority(address newAuthority) public virtual:设置新的授权地址。只有当前的授权地址才能调用这个函数。
    • function isConsumingScheduledOp() public view returns (bytes4):检查合约是否正在执行预定的操作。
  5. 内部函数

    • function _setAuthority(address newAuthority) internal virtual:设置新的授权地址。这个函数没有访问限制,允许直接设置新的授权地址。
    • function _checkCanCall(address caller, bytes calldata data) internal virtual:检查调用者是否有权限调用当前函数。如果调用者没有权限,则抛出异常。

实现原理

  • 权限管理:通过 _authority 状态变量和 restricted 修饰器实现。只有 _authority 指定的地址才能调用被 restricted 修饰的函数。
  • 授权地址的设置:通过 setAuthority 函数设置新的授权地址,只有当前的授权地址才能调用这个函数。
  • 预定操作:通过 _consumingScheduleisConsumingScheduledOp 函数管理预定的操作。如果合约正在执行预定的操作,isConsumingScheduledOp 函数会返回一个特定的函数选择器。

用途

这个合约的主要用途是提供一个框架,用于管理合约的访问权限。通过设置授权地址,可以确保只有授权的地址才能调用合约的特定函数。这对于需要严格控制访问权限的合约非常有用,例如智能合约钱包或权限管理合约。

注意事项

  • 安全性:在实现权限管理时,需要特别注意不要在 receive()fallback() 函数上使用 restricted 修饰器,因为这些函数的调用方式可能导致权限检查失败。
  • 权限转移:通过 setAuthority 函数可以转移合约的权限,但只有当前的授权地址才能执行这个操作,这确保了权限的转移是可控的。
  • 预定操作:通过 isConsumingScheduledOp 函数可以检查合约是否正在执行预定的操作,这对于需要同步执行多个操作的合约非常有用。

ERC20Permit

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
74
75
76
77
78
79
80
81
82
83
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Permit.sol)

pragma solidity ^0.8.20;

import {IERC20Permit} from "./IERC20Permit.sol";
import {ERC20} from "../ERC20.sol";
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
import {Nonces} from "../../../utils/Nonces.sol";

/**
@dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

/**
* @dev Permit deadline has expired.
*/
error ERC2612ExpiredSignature(uint256 deadline);

/**
* @dev Mismatched signature.
*/
error ERC2612InvalidSigner(address signer, address owner);

/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
*
* It's a good idea to use the same `name` that is defined as the ERC-20 token name.
*/
constructor(string memory name) EIP712(name, "1") {}

/**
* @inheritdoc IERC20Permit
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}

bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

bytes32 hash = _hashTypedDataV4(structHash);

address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) {
revert ERC2612InvalidSigner(signer, owner);
}

_approve(owner, spender, value);
}

/**
* @inheritdoc IERC20Permit
*/
function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}

/**
* @inheritdoc IERC20Permit
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
return _domainSeparatorV4();
}
}

ERC20Permit 是一个抽象合约,它结合了 ERC-20 标准和 ERC-2612 标准的功能。ERC-2612 标准允许使用签名来批准代币转移,而无需直接调用 approve 函数。下面是对代码的详细解释:

实现原理

  1. 继承关系

    • ERC20Permit 继承了 ERC20IERC20PermitEIP712Nonces 四个合约。
    • ERC20 实现了 ERC-20 标准的代币功能。
    • IERC20Permit 定义了 ERC-2612 标准的接口。
    • EIP712 用于生成和验证 EIP-712 签名。
    • Nonces 用于管理每个账户的签名计数器。
  2. 常量

    • PERMIT_TYPEHASH 是一个哈希值,用于在 EIP-712 签名中标识 Permit 结构。
  3. 错误

    • ERC2612ExpiredSignature:当签名过期时抛出。
    • ERC2612InvalidSigner:当签名者与账户不匹配时抛出。
  4. 构造函数

    • 初始化 EIP712 的域分隔符,使用传入的 name 参数,并设置版本号为 "1"
  5. permit 函数

    • 验证签名是否过期。
    • 生成 Permit 结构的哈希值。
    • 使用 EIP-712 签名验证机制恢复签名者地址。
    • 检查签名者是否与 owner 匹配。
    • 调用 _approve 函数批准代币转移。
  6. nonces 函数

    • 返回指定账户的签名计数器值。
  7. DOMAIN_SEPARATOR 函数

    • 返回 EIP-712 的域分隔符。

用途

ERC20Permit 合约允许用户通过签名来批准代币转移,而无需直接调用 approve 函数。这对于提高安全性(避免重放攻击)和用户体验(无需每次都手动批准)非常有用。

注意事项

  1. 安全性:确保正确实现 EIP-712 签名验证,以防止签名被滥用。
  2. 时间戳:使用 block.timestamp 来检查签名是否过期,确保安全性。
  3. nonce:使用 nonce 来防止重放攻击,确保每个签名只被使用一次。
  4. 兼容性:确保合约与所有相关接口和标准兼容,以避免潜在的问题。

ERC-2612标准

ERC-2612 标准提供了一种安全、高效的方式来授权代币转移,是 ERC-20 标准的一个扩展。它允许代币持有者通过签名授权第三方转移代币,而不需要直接调用 approve 函数。这个标准基于 EIP-712,它提供了一种使用签名来验证和执行操作的方法。

ERC-2612 标准的主要功能

  1. Permit 函数:允许代币持有者通过签名授权第三方转移代币。这个函数需要三个参数:owner(代币持有者的地址)、spender(被授权的地址)和 value(被授权的代币数量)。

  2. EIP-712 签名:使用 EIP-712 签名来验证 Permit 函数的调用。EIP-712 是一种用于生成和验证签名的标准,它允许在签名中包含额外的元数据,如链 ID 和域分隔符。

  3. Nonce:每个账户都有一个关联的 nonce 值,用于防止重放攻击。在 Permit 函数中,nonce 值会自动增加,确保每个签名只被使用一次。

ERC-2612 标准的实现

要实现 ERC-2612 标准,代币合约需要包含以下部分:

  1. Permit 函数:实现 Permit 函数,用于处理签名授权。

  2. EIP-712 签名:实现 EIP-712 签名生成和验证机制。

  3. Nonce:管理每个账户的 nonce 值。

  4. DOMAIN_SEPARATOR:生成 EIP-712 签名的域分隔符。

ERC-2612 标准的用途

ERC-2612 标准的主要用途是提高代币的安全性。通过使用签名来授权代币转移,可以防止重放攻击,并简化用户界面。此外,它还可以提高交易效率,因为用户不需要每次都手动批准代币转移。