我们在前面Solidity学习代码示例的两小节文章中,学习到了Solidity的一些小案例。接下来这篇文章中,我将会带着大家手把手去学习ERC20代币合约。如果大家有任何的问题,欢迎大家评论区留言。
我们先来看看代码所要表达的意思是什么。首先打开 https://remix.ethereum.org/ ,我们新建一个文件叫做ERC20.sol,然后我们将代码放进文件中。
然后,我们来到编译到界面,可以自行打开自动编译或者手动编译到选择项。
合约如果没问题的话,编译的选项会有个绿色背景的打勾标志,如果有问题的话,会有红色背景并附上有问题的条数,如果有需要优化的部分,则会提示黄色背景的警告并附上警告的条数。上面我们的代码就没有问题了,编译通过了。通过了之后,我们就开始部署我们的合约了。
部署合约完成后,我们可以看到合约的所有的写入和读取数据的方法。其中,黄色代表的是写入数据,蓝色代表的是读取数据。部署合约完成后,我们也能看到合约的唯一地址,也就是合约地址,是一串哈希值。
讲完合约的编译部署后,我们来分析讲解下源代码。
**ERC20.sol**
```js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/Context.sol";
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;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function decimals() public view virtual override returns (uint8) {
return 18;
}
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
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;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
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");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountbalance <="totalSupply.
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
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);
}
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}
contract MyToken is ERC20 {
address public owner;
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
owner = msg.sender;
_mint(msg.sender, 100 * 10 ** uint(decimals()));
}
modifier Manager {
require(owner == msg.sender,"NOT OWNER!");
_;
}
function mint(uint amount) external Manager {
_mint(msg.sender, amount * 10 ** uint(decimals()));
}
}
```
##### ==================== 分割线===================
##### ==================== 分割线===================
```js
import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/utils/Context.sol";
```
OpenZeppelin是用于开发安全的智能合约库,其代码经过社区的审查并有着坚实的社区基础。实现了标准的代币的接口,我们可以放心的使用import功能来讲其导入到我们的代码中,我们就可以直接使用import导入进来的代码的接口,而不需要重复造轮子。当然了,如果我们想要导入自己的合约文件,只要输入正确的路径就可以了。
```js
contract ERC20 is Context, IERC20, IERC20Metadata {}
```
我们想要使用导入进来的合约文件,直接使用is来继承导入的文件即可。
```js
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
```
我们把变量声明为private,这样其他人都无法见到这个变量。通常,我们在声明一个变量或者一个方法,我们会把变量名或者方法名前面加一个下划线来表示。
```js
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
```
构造器传入了两个参数,这意味着我们在部署合约之前需要传入这两个参数的值。这样这两个值就直接初始化在我们的合约中了。
```js
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function decimals() public view virtual override returns (uint8) {
return 18;
}
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
```
合约中的这五个方法都是读取的方法,name返回的是代币的名称,symbol返回的是代币的标志,decimals返回的是代币的精度,totalSupply返回的是代币的总数量,balanceOf返回到是某个账户的余额。
这里有必要补充说明一下decimals精度的概念,通常代币都会有精度,因为以太坊代币的数量是有单位的。以太坊的最小单位位wei,之后是gwei,最大是ether。而1ether等于1000000000000000000wei,也就是1后面18个零。那我们在设置decimal的时候,为了方便做代币的计算,我们通常将decimals设置为18。
```js
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
```
转账方法。参数里的to代表的是对方的账户地址,amount代表的是需要转账的数量。
需要注意的是,区块链中,转账的数量默认单位是wei,所以,如果我们要转账1ether,就必须写成1后面跟18个零。
因为我们需要重写导入合约中的方法,所以函数跟着一个virtual和override关键字。
由于transfer方法是public的,我们通常不把重要的转账逻辑放在public接口来编写,通常我们将具体的逻辑写在私有函数里面,这个也是处于代码合约安全考虑的一部分,也是优化了代码。在这里我们直接使用当前合约里的_transfer方法的逻辑。
注意了,这里的转账者是当前执行这个方法的钱包账户,接收者是to。
```js
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
```
allowance方法的意思查询是spender账户还能在owner账户可以使用多少代币。
```js
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
```
approve方法是当前执行这个方法的账户,允许spender账户可以从它的账户里使用多少代币。
比如,当前的账户有10ETH,而spender拥有0ETH。那么当当前的账户执行这个方法时,传入spender的账户地址,传入可以让spender从自己账户可以使用的ETH的数量,比如此时传入amount为6,那么spender本没有ETH,但是它现在可以从给它ETH代币的账户手中花费6ETH的代币了。
```js
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
```
经过approve方法之后,spender就可以调用transferFrom方法,将from设置为给它ETH代币的账户地址,将to设置为需要转让的对方的地址,传入一个不大于6ETH的amount的值。这样,就可以将代币转出了。
```js
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
```
increaseAllowance方法,顾名思义,就是增加允许spender从自己账户转出的代币的数量。
```js
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
```
decreaseAllowance方法,就是减少允许spender从自己账户转出的代币的数量。
```js
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
```
_transfer方法是转账逻辑实现的具体代码实现。它声明为internal,这就意味着只有内部合约才能够调用次方法,而外部合约则访问不到。
require限制条件中,我们限制发送者的账户地址和接受者的账户地址均不能为0地址,address(0)表示的是0x0地址,也就是黑洞地址,是一个无效的地址。当钱转入到此地址的时候,就意味着再也找不回来了。所以,这里为了用户转到黑洞地址,这里一开始就将参数进行了校验。
_beforeTokenTransfer方法是转账前的状态。
require(fromBalance >= amount),表示发送者的数量要大于填入的amount的值。
unchecked方法不检查算术的溢出
由于我们这个方法是重写的方法,而导入的文件里已经有了Transfer这个event事件,所以我们可以使用emit监听事件。
最后使用_afterTokenTransfer方法来更新是转账后的状态。
```js
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;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
```
_mint方法是铸造代币的方法,我们前面代码中声明了一个_totalSupply代表代币总量的变量,以及_balances这个映射。当我们执行这个方法的逻辑的时候,总量会增加,并且给参数account增加的数量也会对应增加。这个方法是internal的,所以,我们不能直接使用,只能在写一个公共的方法来去调用这个方法实现接口。
```js
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");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountbalance <="totalSupply.
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
```
_burn方法与_mint方法则相反,它是将_totalSupply进行减法的操作。当执行这个方法的逻辑时,参数account的amount就会随之减少。同样,它是internal方法,所以,如果我们的需求里有销毁代币的功能,我们也是一样要写一个公共的方法来调用_burn这个接口。
```js
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);
}
```
_approve方法是上面approve方法的具体逻辑实现。
```js
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
```
_spendAllowance是transfeFrom方法的具体实现。
```js
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
```
这两种方法作为0.8版本写法的存在,是一定要有的。
```js
contract MyToken is ERC20 {}
```
上面介绍的ERC20合约的接口里面没有铸造的公共方法,这里我们又写一份新的合约来继承上面的ERC20合约。那么我们这一份合约里就声明了两个合约,我们就称MyToken合约入口就是我们的主合约,而ERC20合约是我们的从合约。当然,我们也可以将ERC20合约写在一个文件里,然后我们另写一份文件名为MyToken合约,然后我们使用import来导入ERC20合约,这样也是可以的。
```js
address public owner;
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
owner = msg.sender;
_mint(msg.sender, 100 * 10 ** uint(decimals()));
}
```
在这段代码块中,我们声明了一个owner,这个可以看成是我们部署合约的管理者,拥有最高权限。
在构造器里,我们继承了ERC20合约的构造器的接口。在合约部署之前,我们就将部署合约的用户地址赋予owner,并且初始化铸造出100ETH的代币。
```js
modifier Manager {
require(owner == msg.sender,"NOT OWNER!");
_;
}
function mint(uint amount) external Manager {
_mint(msg.sender, amount * 10 ** uint(decimals()));
}
```
在这段代码块中,我们写了一个修改器,要求owner的账户地址必须是部署合约的账户地址。
由于ERC20合约中的_mint方法是internal的,我们无法直接使用_mint方法。所以,我们写了一个mint方法,声明为external,并且加上了修改器的限制条件,说明只有owner用户,也就是部署合约的用户才能够执行mint方法。在mint方法内部,我们直接调用ERC20合约中的_mint方法,参数为代币的数量。这样我们就有了铸造代币的功能了。
至此,我们通过ERC20合约示例,又更全面的学习了Solidity这门语言,也对以太坊的了解更加深入了。以太坊的ERC20代币均是用这种技术实现部署的,ERC20代币也叫做同质化代币,也就意味着所有人持有的代币是没有区别的,这就好比你手中的编号为001的100块人民币跟我手中编号为002的100块人民币的价值是一样的。
下一节,我们来学习ERC721示例,ERC721也就是我们所说的NFT,也就是我们经常提及的元宇宙。如果感兴趣的话,那就赶紧关注我,新的一期内容更加精彩哦!
领取专属 10元无门槛券
私享最新 技术干货