文章前言
ERC-20为以太坊智能合约提供了一套编写规范,而IERC-20则规定了一个Token需要实现的基本接口,本篇文章将对此进行解读。
IERC-20
首先,我们来看一个IERC-20规范——一个合约需要实现的接口:
#FUNCTIONS
totalSupply()
balanceOf(account)
transfer(recipient, amount)
allowance(owner, spender)
approve(spender, amount)
transferFrom(sender, recipient, amount)
#EVENTS
Transfer(from, to, value)
Approval(owner, spender, value)
接口作用域、接口参数类型、接口返回值类型、参数与返回值个数等具体信息如下:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller\\\'s account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller\\\'s tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender\\\'s allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller\\\'s
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
ERC-20
ERC-20是对IERC-20接口的实现,具体代码如下所示:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.8.0;
import \\\"./IERC20.sol\\\";
import \\\"./extensions/IERC20Metadata.sol\\\";
import \\\"../../utils/Context.sol\\\";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* For a generic mechanism see {ERC20PresetMinterPauser}.
*
* TIP: For a detailed writeup see our guide
* https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* We have followed general OpenZeppelin guidelines: functions revert instead
* of returning `false` on failure. This behavior is nonetheless conventional
* and does not conflict with the expectations of ERC20 applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn\\\'t required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract ERC20 is Context, IERC20, IERC20Metadata {
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* The defaut value of {decimals} is 18. To select a different value for
* {decimals} you should overload it.
*
* All three of these values are immutable: they can only be set once during
* construction.
*/
constructor (string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5,05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the value {ERC20} uses, unless this function is
* overloaded;
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `recipient` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), recipient, amount);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
_approve(_msgSender(), spender, amount);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* Requirements:
*
* - `sender` and `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
* - the caller must have allowance for ``sender``\\\'s tokens of at least
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(currentAllowance >= amount, \\\"ERC20: transfer amount exceeds allowance\\\");
_approve(sender, _msgSender(), currentAllowance - amount);
return true;
}
/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
return true;
}
/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
uint256 currentAllowance = _allowances[_msgSender()][spender];
require(currentAllowance >= subtractedValue, \\\"ERC20: decreased allowance below zero\\\");
_approve(_msgSender(), spender, currentAllowance - subtractedValue);
return true;
}
/**
* @dev Moves tokens `amount` from `sender` to `recipient`.
*
* This is internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), \\\"ERC20: transfer from the zero address\\\");
require(recipient != address(0), \\\"ERC20: transfer to the zero address\\\");
_beforeTokenTransfer(sender, recipient, amount);
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, \\\"ERC20: transfer amount exceeds balance\\\");
_balances[sender] = senderBalance - amount;
_balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
}
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `to` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), \\\"ERC20: mint to the zero address\\\");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
}
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), \\\"ERC20: burn from the zero address\\\");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, \\\"ERC20: burn amount exceeds balance\\\");
_balances[account] = accountBalance - amount;
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), \\\"ERC20: approve from the zero address\\\");
require(spender != address(0), \\\"ERC20: approve to the zero address\\\");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``\\\'s tokens
* will be to transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``\\\'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
}
合约解读
下面我们对OpenZeppelin官方给出的ERC-20标准源码中的合约进行解读:
合约继承
在以上合约中的L31行我们可以清晰的看到以下内容:
contract ERC20 is Context, IERC20, IERC20Metadata {
上面代码的意思是当前合约为ERC20,当前合约继承自Context、IERC20、IERC20Metadata这三个合约,这里的继承关系和JAVA中的类的继承关系类似,子类可以调用父类的方法也可以重写父类的方法。
基本变量
Token作为一个代币的称谓,自然少不了标识符(symbol)、名称(name)、发行总量(totalSupply):
uint256 private _totalSupply; //发行总量
string private _name; //代币名称
string private _symbol; //代币标识符
构造函数
智能合约中的构造函数与JAVA中构造函数类似,都是用于初始化操作,智能合约中的构造函数有两种写法,一种是通过定义一个和合约名称一致的public型函数,另一种是通过关键字constructor来声明:
constructor (string memory name_, string memory symbol_) {
_name = name_; //初始化代币名称
_symbol = symbol_; //初始化代币标识符
}
查询函数
增删改查作为数据的基本操作在智能合约中也是必不可少的,在ERC-20中提供了代币名称查询、代币标识符查询、代币精度查询、用户资产查询:
/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5,05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the value {ERC20} uses, unless this function is
* overloaded;
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
转账逻辑
转账逻辑时ERC-20中的关键所在,也是项目方关注的重点,下面是ERC-20中对于转账的规范性示例:
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `recipient` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), recipient, amount);
return true;
}
可以看到这里调用transfer函数转账时需要提供以下两个参数:
-
recipient:代币接受地址
-
amount:要转账的数量
继续向下看可以看到这里继续调用了合约内的_transfer函数来实现转账操作,我们继续跟进查看一番:
/**
* @dev Moves tokens `amount` from `sender` to `recipient`.
*
* This is internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), \\\"ERC20: transfer from the zero address\\\");
require(recipient != address(0), \\\"ERC20: transfer to the zero address\\\");
_beforeTokenTransfer(sender, recipient, amount);
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, \\\"ERC20: transfer amount exceeds balance\\\");
_balances[sender] = senderBalance - amount;
_balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
}
这里的sender即为函数的调用者,也就是要发送代币的用户,可以看到在_transfer函数中会首先检查发送者地址与代币接受者地址是否为空(zero address),之后通过\\”_balances[sender]\\”来查询当前发送者用户所持有的资产数量是多少,之后再检查发送者所持有的token数量是否大于等于要发送的token数量,即检查余额是否充足,之后在L23更新发送者用户资产数量,在L24更新代币接受地址资产数量,之后使用emit关键字触发事件。
授权转账
除了转账逻辑还有一种业务逻辑就是\\”授权转账\\”逻辑,与转账逻辑不同的是\\”授权转账\\”逻辑虽然也是转账操作,但是它并非直接从A账户转账到C账户,而是从A账户到B账户,再由B账户到C账户,下面是具体的代码实现:
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* Requirements:
*
* - `sender` and `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
* - the caller must have allowance for ``sender``\\\'s tokens of at least
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(currentAllowance >= amount, \\\"ERC20: transfer amount exceeds allowance\\\");
_approve(sender, _msgSender(), currentAllowance - amount);
return true;
}
可以看到transferFrom函数与transfer函数首先不同的就是参数的个数不同,这里的transferFrom需要调用这提供三个参数:
-
sender——授权账户地址
-
recipient——代币接受地址
-
amount——要转的代币数量
之后我们可以看到这里首先调用了_transfer函数来实现转账操作:
/**
* @dev Moves tokens `amount` from `sender` to `recipient`.
*
* This is internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), \\\"ERC20: transfer from the zero address\\\");
require(recipient != address(0), \\\"ERC20: transfer to the zero address\\\");
_beforeTokenTransfer(sender, recipient, amount);
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, \\\"ERC20: transfer amount exceeds balance\\\");
_balances[sender] = senderBalance - amount;
_balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
}
之后检查sender账户授权给函数调用者转账的额度,且转账额度需要大于等于当前转账的额度,否则回滚交易,不过从逻辑上来说这里应该先要检查授权转账的额度是否大于转账的额度,之后再进行转账操作以规避先转账后检查不通过导致的gas费用消耗:
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(currentAllowance >= amount, \\\"ERC20: transfer amount exceeds allowance\\\");
授权额度
上面的transferFrom中我们提到了授权转账额度检查,那么如何去设置授权转账额度呢?可以通过以下approve来实现:
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), \\\"ERC20: approve from the zero address\\\");
require(spender != address(0), \\\"ERC20: approve to the zero address\\\");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
该函数需要两个参数:
-
owner——转交权限的地址
-
spender——接受权限的地址
-
amout——授权额度
之后对转交权限的地址以及接受权限的地址进行非0检查,之后通过_allowances映射表来指定owner给spender地址用户赋予了转移多少代币的权限,之后通过关键词emit触发事件。
通过查看ERC-20源码标准不难发现还有两个用于增加和减少授权额度的函数——increaseAllowance、decreaseAllowance,这两个函数主要用于在原有的授权基础之上再增加授权额度或者减少授权额度的操作,具体实现代码如下所示:
/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
return true;
}
/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
uint256 currentAllowance = _allowances[_msgSender()][spender];
require(currentAllowance >= subtractedValue, \\\"ERC20: decreased allowance below zero\\\");
_approve(_msgSender(), spender, currentAllowance - subtractedValue);
return true;
}
铸币操作
ERC-20中也提供了铸币函数,该函数主要用于增发代币操作,具体代码如下所示:
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `to` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), \\\"ERC20: mint to the zero address\\\");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
}
函数的调用者需要传递两个参数:
-
account———铸币时代币接受地址
-
amount———铸币数量
在这里首先会检查铸币时代币的接受地址是否为null,之后增加代币总量,之后向地址account增发amount数量的代币,当然很多懂安全的人可能会说这里存在整形溢出风险,因为没有做溢出检查,我们这里暂且不讨论这一话题,当然如果要规避溢出问题也可以从以下两个方面实现:
-
数值操作前后溢出检查
-
使用SafeMath函数进行数值运算
代币销毁
既然有代币增发逻辑,那么也就自然而然的有了代币销毁逻辑,具体实现代码如下所示:
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), \\\"ERC20: burn from the zero address\\\");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, \\\"ERC20: burn amount exceeds balance\\\");
_balances[account] = accountBalance - amount;
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
}
这里的函数调用者需要传递两个参数:
-
account——要销毁代币的地址
-
amount——要销毁代币的数量
在_burn函数中首先会对要销毁代币的地址进行一次非零检查,之后获取当前要销毁代币的地址账户可用的代币总量,之后检查可用余额是否大于要销毁的代币总量,之后再L19更新account地址账户的代币总量并在L20更新发行的token总量,之后通过emit触发事件。
文末小结
至此,我们对ERC-20标准的解读已完结,这里的代币销毁、增发代币逻辑在IERC-20中并没有具体的定义,在这里我们可以看做是ERC-20的功能扩展,同时也表明基于ERC-20我们可以自定义跟多的功能函数来实现不用的业务逻辑。
参考链接
https://docs.openzeppelin.com/contracts/4.x/api/token/erc20
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34248.html