这是一段openzeppelin
官方生成的代码
1 | // SPDX-License-Identifier: MIT |
这段代码定义了一个名为BobToken
的智能合约,它继承了多个标准接口和合约,用于创建一个可暂停、可授权管理、可使用EIP-2612标准的许可(Permit)功能的代币。下面是对代码的详细解释:
继承的合约和接口
- ERC20: ERC20是一个标准接口,定义了代币的基本功能,如总供应量、余额查询、转账等。
- ERC20Pausable: 这个接口允许代币的转账操作被暂停,从而防止在特定情况下(如紧急情况)的代币转移。
- AccessManaged: 这个接口可能定义了访问控制功能,比如只有特定地址(如管理员)才能执行某些操作。
- ERC20Permit: 这个接口允许代币持有者通过签名授权来批准第三方转移代币,而不需要直接调用转账函数。
继承顺序
在Solidity中,合约的继承顺序是从左到右的,即最左边的合约是基类,最右边的合约是派生类。例如,在contract BobToken is ERC20, ERC20Pausable, AccessManaged, ERC20Permit {}中,BobToken继承了ERC20、ERC20Pausable、AccessManaged和ERC20Permit。
继承原则
- 从左到右的继承顺序:如上所述,继承顺序是从左到右的,这意味着基类在派生类之前被继承。
- 构造函数的调用:在派生类的构造函数中,必须显式地调用所有基类的构造函数。调用顺序必须遵循继承顺序,即从左到右。
- 函数覆盖:如果基类和派生类中有同名的函数,那么派生类的函数会覆盖基类的函数。在调用这些函数时,会优先调用派生类的函数。
- 库的使用:如果合约继承了一个库,那么库的函数会被覆盖。这是因为库的函数是内部函数,不能被覆盖。
构造函数
1 | constructor(address initialAuthority) |
initialAuthority
: 初始化合约的管理权限地址。- 构造函数中调用了多个基类的构造函数,传递了必要的参数,如代币名称、符号、初始权限地址等。ERC20、AccessManaged和ERC20Permit的构造函数被显式地调用,并且按照继承顺序从左到右调用。
主要函数
- pause(): 暂停代币的转账操作。只有通过
AccessManaged
接口授权的地址才能调用。 - unpause(): 恢复代币的转账操作。同样,只有授权的地址才能调用。
- mint(address to, uint256 amount): 允许授权地址铸造新的代币,并将其发送给指定的地址。这通常用于代币发行。
内部函数
1 | function _update(address from, address to, uint256 value) |
_update
函数是一个内部函数,用于更新代币的余额。它重写了ERC20
和ERC20Pausable
中的同名函数,并在内部调用了super._update
,确保了在更新余额时考虑了暂停状态。
注意事项
- 访问控制:
AccessManaged
接口确保了只有授权的地址才能执行关键操作,如暂停、铸造等。 - 暂停功能:
ERC20Pausable
接口提供了暂停和恢复转账操作的功能,这在处理紧急情况时非常有用。 - EIP-2612 Permit标准: 通过实现
ERC20Permit
接口,用户可以授权第三方代币转移,而无需直接调用转账函数,这提高了安全性。
总的来说,这段代码实现了一个功能丰富的代币合约,结合了多种安全性和管理功能,适用于需要严格控制访问和操作的场景。接下来,我们按照继承顺序逐个来介绍一下。
Context
1 | // SPDX-License-Identifier: MIT |
这段代码定义了一个名为Context
的抽象合约。这个合约主要用于提供关于当前执行上下文的信息,包括交易的发送者和数据。在处理元交易(meta-transactions)时,发送和支付执行费用的账户可能不是实际的应用程序发送者。
实现原理
_msgSender()
函数:这个函数返回当前交易的发送者地址。在大多数情况下,这可以通过msg.sender
直接获取,但在处理元交易时,这个值可能不是实际的发送者。因此,这个函数提供了一个抽象层,允许子合约重写它以提供正确的发送者地址。_msgData()
函数:这个函数返回当前交易的输入数据。同样,在大多数情况下,这可以通过msg.data
直接获取,但在处理元交易时,这个值可能不是实际的输入数据。因此,这个函数提供了一个抽象层,允许子合约重写它以提供正确的输入数据。_contextSuffixLength()
函数:这个函数返回一个长度值,表示上下文后缀的长度。这个函数在处理元交易时可能有用,但在这个合约中默认返回0,表示没有上下文后缀。
用途
Context
合约的主要用途是为那些需要访问当前执行上下文信息的合约提供基础。例如,如果一个合约需要知道是谁发送了交易,或者交易中包含了哪些数据,它可以继承Context
合约并使用_msgSender()
和_msgData()
函数。
注意事项
- 继承:任何需要使用
Context
合约功能的合约都应该继承它。如果子合约需要提供不同的发送者或数据,它应该重写_msgSender()
和_msgData()
函数。 - 元交易:处理元交易时,发送和支付执行费用的账户可能不是实际的发送者。因此,任何依赖于
msg.sender
或msg.data
的合约都需要能够处理这种情况。 - 安全性:在重写
_msgSender()
和_msgData()
函数时,需要确保提供的信息是安全的,以防止潜在的安全漏洞。
ERC20
1 | // SPDX-License-Identifier: MIT |
定义了一个名为 ERC20 的抽象合约,它继承了四个接口:Context、IERC20、IERC20Metadata 和 IERC20Errors。
- abstract contract ERC20
- abstract contract 表示这是一个抽象合约,不能直接实例化。它通常用于定义接口和共享逻辑,其他合约可以继承它。
- ERC20 是合约的名称。
- is Context, IERC20, IERC20Metadata, IERC20Errors
- Context 是一个包含上下文相关函数(如 msg.sender)的接口,通常用于简化合约编写。
- IERC20 是 ERC20 标准的接口,定义了 ERC20 代币的基本功能,如 totalSupply、balanceOf、transfer 等。
- IERC20Metadata 是 ERC20 标准的扩展接口,定义了代币的元数据,如 name、symbol 和 decimals。
- IERC20Errors 是一个自定义的接口,通常用于定义合约中可能出现的错误,如 TransferFailedError、InsufficientBalanceError 等。
合约成员
1 | mapping(address account => uint256) private _balances; |
_balances:
- 这是一个映射(mapping),用于存储每个账户的代币余额。
address account
是账户地址,uint256
是该地址对应的代币数量。private
关键字表示这个映射只能在合约内部访问,外部无法直接访问。- 这个映射用于跟踪每个账户的代币余额。
_allowances:
- 这是一个嵌套的映射,用于存储每个账户授权给其他账户的代币数量。
address account
是账户地址,mapping(address spender => uint256)
是一个映射,表示该账户授权给其他账户的代币数量。address spender
是被授权的账户地址,uint256
是被授权的代币数量。private
关键字表示这个映射只能在合约内部访问,外部无法直接访问。- 这个映射用于跟踪每个账户授权给其他账户的代币数量,通常用于实现代币的转账授权功能。
-
_totalSupply:
- 这是一个无符号整数(uint256),用于存储代币的总供应量。
private
关键字表示这个变量只能在合约内部访问,外部无法直接访问。- 这个变量用于跟踪代币的总供应量。
-
_name:
- 这是一个字符串变量,用于存储代币的名称。
private
关键字表示这个变量只能在合约内部访问,外部无法直接访问。- 这个变量用于存储代币的名称,例如 “MyToken”。
_symbol:
- 这是一个字符串变量,用于存储代币的符号。
private
关键字表示这个变量只能在合约内部访问,外部无法直接访问。- 这个变量用于存储代币的符号,例如 “MTK”。
transfer & transferFrom
1 | function transfer(address to, uint256 value) public virtual returns (bool) { |
- 所有的转让动作最终都由
_transfer
方法来完成。 _transfer
方法中统一判断地址的有效性,使用revert
比require
更节省gas。ERC20InvalidSender
和ERC20InvalidReceiver
都来自IERC20Errors
接口。
_update
_update
为内部虚函数,用于更新代币合约中的余额和总供应量。它主要用于处理代币的转移操作,确保在转移过程中不会发生溢出或不足的情况。
参数说明:
from
:代币的发送者地址。to
:代币的接收者地址。value
:转移的代币数量。
更新总供应量:
- 如果
from
是address(0)
,表示这是一个铸造操作(minting),那么_totalSupply
将增加value
。1
2
3
4
5
6function _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
。
- 如果
更新接收者的余额:
- 如果
to
是address(0)
,表示这是一个销毁操作(burning),那么_totalSupply
将减少value
。1
2
3
4
5
6function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
} - 如果
to
不是address(0)
,表示这是一个转移操作,那么将value
加到to
的余额中。
- 如果
防止溢出:
- 使用
unchecked
关键字来处理可能的溢出情况,因为代码逻辑已经确保了不会发生溢出。 - 使用
unchecked
可以节省gas
- 使用
触发事件:
- 最后,通过
emit Transfer(from, to, value);
触发一个Transfer
事件,记录这次转移操作。
- 最后,通过
approve
1 | function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { |
_approve
函数
_approve
函数用于更新一个账户对另一个账户的授权额度。如果emitEvent
参数为true
,还会触发一个Approval
事件。
- `owner`:授权额度所有者的地址。
- `spender`:被授权的账户地址。
- `value`:授权的额度。
- `emitEvent`:一个布尔值,决定是否触发`Approval`事件。
流程
- 首先检查
owner
和spender
是否为address(0)
,如果是,则抛出异常ERC20InvalidApprover
或ERC20InvalidSpender
。 - 更新
_allowances
映射,将owner
对spender
的授权额度设置为value
。 - 如果
emitEvent
为true
,则触发Approval
事件。
_spendAllowance
函数
_spendAllowance
函数用于消耗owner
对spender
的授权额度。它不会更新授权额度,除非授权额度是有限的。如果授权额度不足,则会抛出异常。
- `owner`:授权额度所有者的地址。
- `spender`:被授权的账户地址。
- `value`:要消费的额度。
流程
- 获取
owner
对spender
的当前授权额度。 - 如果当前授权额度不是无限(即不等于
type(uint256).max
),则检查当前授权额度是否足够消费value
。 - 如果足够,则更新
owner
对spender
的授权额度,减去value
,并触发Approval
事件(如果emitEvent
为true
)。
decimals
1 | function decimals() public view virtual returns (uint8) { |
默认值是18, 如果想改变,只能重写decimals()
这个方法。这种方法直接返回固定值的相比于使用成员变量存储的方式更省gas。
ERC20Pausable
1 | // SPDX-License-Identifier: MIT |
ERC20Pausable
是个抽象合约,它继承了 ERC20
和 Pausable
两个合约。ERC20
是一个标准的代币合约接口,而 Pausable
是一个用于暂停合约操作的合约。
实现原理
继承关系:
ERC20Pausable
继承了ERC20
和Pausable
合约,这意味着它继承了ERC20
的所有功能(如代币的创建、转移、余额查询等)以及Pausable
的功能(如暂停和恢复合约操作)。抽象合约:
ERC20Pausable
是一个抽象合约,因为它包含了一个未实现的函数_update
。函数重写:
_update
函数重写了ERC20
合约中的_update
函数。这个函数在执行代币转移操作之前,会检查合约是否处于暂停状态。如果合约处于暂停状态,则不允许执行转移操作。whenNotPaused
修饰器:whenNotPaused
是Pausable
合约中的一个修饰器,用于确保在调用被修饰的函数时,合约不是暂停状态。如果合约是暂停状态,调用将被 revert(回滚)。
用途
ERC20Pausable
合约的主要用途是提供一个可暂停的代币合约,允许合约所有者或管理员在紧急情况下暂停代币的转移操作,以防止潜在的滥用或攻击。
注意事项
实现
_update
函数:任何继承ERC20Pausable
的合约都必须实现_update
函数,以确保在执行代币转移操作时能够正确地检查合约是否处于暂停状态。合约暂停:合约暂停后,所有与代币转移相关的操作都将被阻止,直到合约被恢复。因此,合约所有者或管理员在暂停合约时应谨慎操作,以避免造成不必要的损失。
安全性:虽然
ERC20Pausable
合约提供了一种暂停合约的方法,但它并不能防止所有潜在的安全问题。开发者仍需确保合约的其他部分(如访问控制、逻辑错误等)是安全的。
AccessManaged
1 | // SPDX-License-Identifier: MIT |
AccessManaged
是个抽象合约,它继承了 Context
和 IAccessManaged
接口。这个合约的主要目的是管理合约的访问权限,确保只有授权的地址才能调用特定的函数。下面是对代码的详细解释:
合约结构
状态变量:
_authority
:存储当前合约的授权地址。_consumingSchedule
:一个布尔值,用于指示是否正在执行预定的操作。
构造函数:
constructor(address initialAuthority)
:初始化合约,设置初始的授权地址。
修饰器:
modifier restricted()
:限制函数的访问,确保只有授权的地址才能调用。这个修饰器通过调用_checkCanCall
函数来检查调用者是否有权限。
接口实现:
function authority() public view virtual returns (address)
:返回当前的授权地址。function setAuthority(address newAuthority) public virtual
:设置新的授权地址。只有当前的授权地址才能调用这个函数。function isConsumingScheduledOp() public view returns (bytes4)
:检查合约是否正在执行预定的操作。
内部函数:
function _setAuthority(address newAuthority) internal virtual
:设置新的授权地址。这个函数没有访问限制,允许直接设置新的授权地址。function _checkCanCall(address caller, bytes calldata data) internal virtual
:检查调用者是否有权限调用当前函数。如果调用者没有权限,则抛出异常。
实现原理
- 权限管理:通过
_authority
状态变量和restricted
修饰器实现。只有_authority
指定的地址才能调用被restricted
修饰的函数。 - 授权地址的设置:通过
setAuthority
函数设置新的授权地址,只有当前的授权地址才能调用这个函数。 - 预定操作:通过
_consumingSchedule
和isConsumingScheduledOp
函数管理预定的操作。如果合约正在执行预定的操作,isConsumingScheduledOp
函数会返回一个特定的函数选择器。
用途
这个合约的主要用途是提供一个框架,用于管理合约的访问权限。通过设置授权地址,可以确保只有授权的地址才能调用合约的特定函数。这对于需要严格控制访问权限的合约非常有用,例如智能合约钱包或权限管理合约。
注意事项
- 安全性:在实现权限管理时,需要特别注意不要在
receive()
或fallback()
函数上使用restricted
修饰器,因为这些函数的调用方式可能导致权限检查失败。 - 权限转移:通过
setAuthority
函数可以转移合约的权限,但只有当前的授权地址才能执行这个操作,这确保了权限的转移是可控的。 - 预定操作:通过
isConsumingScheduledOp
函数可以检查合约是否正在执行预定的操作,这对于需要同步执行多个操作的合约非常有用。
ERC20Permit
1 | // SPDX-License-Identifier: MIT |
ERC20Permit
是一个抽象合约,它结合了 ERC-20 标准和 ERC-2612 标准的功能。ERC-2612 标准允许使用签名来批准代币转移,而无需直接调用 approve
函数。下面是对代码的详细解释:
实现原理
继承关系:
ERC20Permit
继承了ERC20
、IERC20Permit
、EIP712
和Nonces
四个合约。ERC20
实现了 ERC-20 标准的代币功能。IERC20Permit
定义了 ERC-2612 标准的接口。EIP712
用于生成和验证 EIP-712 签名。Nonces
用于管理每个账户的签名计数器。
常量:
PERMIT_TYPEHASH
是一个哈希值,用于在 EIP-712 签名中标识Permit
结构。
错误:
ERC2612ExpiredSignature
:当签名过期时抛出。ERC2612InvalidSigner
:当签名者与账户不匹配时抛出。
构造函数:
- 初始化
EIP712
的域分隔符,使用传入的name
参数,并设置版本号为"1"
。
- 初始化
permit 函数:
- 验证签名是否过期。
- 生成
Permit
结构的哈希值。 - 使用 EIP-712 签名验证机制恢复签名者地址。
- 检查签名者是否与
owner
匹配。 - 调用
_approve
函数批准代币转移。
nonces 函数:
- 返回指定账户的签名计数器值。
DOMAIN_SEPARATOR 函数:
- 返回 EIP-712 的域分隔符。
用途
ERC20Permit
合约允许用户通过签名来批准代币转移,而无需直接调用 approve
函数。这对于提高安全性(避免重放攻击)和用户体验(无需每次都手动批准)非常有用。
注意事项
- 安全性:确保正确实现 EIP-712 签名验证,以防止签名被滥用。
- 时间戳:使用
block.timestamp
来检查签名是否过期,确保安全性。 - nonce:使用
nonce
来防止重放攻击,确保每个签名只被使用一次。 - 兼容性:确保合约与所有相关接口和标准兼容,以避免潜在的问题。
ERC-2612标准
ERC-2612 标准提供了一种安全、高效的方式来授权代币转移,是 ERC-20 标准的一个扩展。它允许代币持有者通过签名授权第三方转移代币,而不需要直接调用 approve
函数。这个标准基于 EIP-712,它提供了一种使用签名来验证和执行操作的方法。
ERC-2612 标准的主要功能
Permit 函数:允许代币持有者通过签名授权第三方转移代币。这个函数需要三个参数:
owner
(代币持有者的地址)、spender
(被授权的地址)和value
(被授权的代币数量)。EIP-712 签名:使用 EIP-712 签名来验证
Permit
函数的调用。EIP-712 是一种用于生成和验证签名的标准,它允许在签名中包含额外的元数据,如链 ID 和域分隔符。Nonce:每个账户都有一个关联的
nonce
值,用于防止重放攻击。在Permit
函数中,nonce
值会自动增加,确保每个签名只被使用一次。
ERC-2612 标准的实现
要实现 ERC-2612 标准,代币合约需要包含以下部分:
Permit 函数:实现
Permit
函数,用于处理签名授权。EIP-712 签名:实现 EIP-712 签名生成和验证机制。
Nonce:管理每个账户的
nonce
值。DOMAIN_SEPARATOR:生成 EIP-712 签名的域分隔符。
ERC-2612 标准的用途
ERC-2612 标准的主要用途是提高代币的安全性。通过使用签名来授权代币转移,可以防止重放攻击,并简化用户界面。此外,它还可以提高交易效率,因为用户不需要每次都手动批准代币转移。