概述
ERC-20 提供了一个同质化代币的标准,换句话说,每个代币与另一个代币(在类型和价值上)完全相同。 例如,一个 ERC-20 代币就像 ETH 一样,意味着一个代币会并永远会与其他代币一样。
ERC20简单理解成以太坊上的一个代币协议,所有基于以太坊开发的代币合约都遵守这个协议。遵守这些协议的代币我们可以认为是标准化的代币,而标准化带来的好处是兼容性好。这些标准化的代币可以被各种以太坊钱包支持,用于不同的平台和项目。
ERC20协议标准
ERC20标准规定,一共包括:6个函数,2个event,3个变量。
//查询代币发行总量
function totalSupply() public constant returns (uint);
//查询某个账户余额
function balanceOf(address tokenOwner) public constant returns (uint balance);
//查询某个账户可转账金额。用于控制代币的交易
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
//从当前账户,实现代币交易
function transfer(address to, uint tokens) public returns (bool success);
//授权,允许某个账户花费此地址可用的代币数
function approve(address spender, uint tokens) public returns (bool success);
//实现用户之间的代币交易
function transferFrom(address from, address to, uint tokens) public returns (bool success);
//当代币交易时会触发此函数
event Transfer(address indexed from, address indexed to, uint tokens);
//当成功调用approve函数后会触发此函数
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
string public constant name = "Zarten Token"; //代币名称
string public constant symbol = "ZAR"; //代币简称
uint8 public constant decimals = 18; // 18 is the most common number of decimal places
// 0.0000000000000000001 个代币
//返回token使用的小数点后几位。比如设置为3,就是支持0.001表示。一般为18位
}
ERC20工作原理
变量及函数实现
定义变量
一般定义两个映射变量:保存每个地址对应的余额。
mapping (address => uint256) public balances
两层映射。保存着某个地址A允许另一个地址B可操作的金额。最外层映射为某个地址A,内层映射为另一个地址B,值为可操作(发起交易)金额总量。
mapping(address => mapping(address =>uint256)) public allowed
函数实现
balanceOf()
从映射变量balances中取出某个地址的余额。
return balances[tokenOwner];
}
transfer()
当前账户转账操作。
msg.sender为保留字,指这个函数的地址。
sub:减 add:加
首先从当前账户减去相应金额。
同时往对方账户加上对应金额。
并调用Transfer函数做通知。
balances[msg.sender] = balances[msg.sender].sub(tokens);
balances[to] = balances[to].add(tokens);
Transfer(msg.sender, to, tokens);
return true;
}
transferFrom()
用户之间账户转账操作。由from地址发起转账交易。
from地址账户减去相应金额。
from从msg.sender总共可操作金额减少相应金额。
to地址账户增加相应金额。
调用Transfer函数做通知。
approve后可以转移对方代币给自己
balances[from] = balances[from].sub(tokens);
allowed[from][msg.sender] = allowed[from][msg.sender].sub(tokens);
balances[to] = balances[to].add(tokens);
Transfer(from, to, tokens);
return true;
}
approve()
设置某账户spender可操控msg.sender的代币数。
设置spender地址从msg.sender可使用的代币数。
调用Approval函数做通知。
allowed[msg.sender][spender] = tokens;
Approval(msg.sender, spender, tokens);
return true;
}
ERC20高级功能代码
ERC20代币有时需要其他一些额外的高级功能,比如代币管理、代币增发、空投代币、代币冻结、销毁代币、代币兑换等。
代币管理
有时代币需要有一个管理者功能。使用onlyOwner修饰的接口只能有合约所有者调用,否则会抛出异常。
添加一个owned合约,如下:
address public owner;
constructor() public {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) onlyOwner public {
owner = newOwner;
}
}
代币增发
代币增发可使代币总供应量增加,可以指定某个账户的代币增加,同时总供应量也随之增加。
使用onlyOwner修饰器,只能owner调用。this表示当前合约。
balances[target] += mintedAmount;
_totalSupply += mintedAmount;
emit Transfer(address(0), address(this), mintedAmount);
emit Transfer(address(this), target, mintedAmount);
}
代币销毁
首先添加一个通知客户端代币消费的事件。
销毁代币非为销毁管理者代币和销毁用户代币。此时需要管理者去进行销毁。
销毁管理者代币
require(balances[owner] >= _value);
balances[owner] -= _value;
_totalSupply -= _value;
emit Burn(owner, _value);
return true;
}
销毁用户代币
销毁之前需要判断用户代币的数量是否大于销毁数量,并且判断allowed可以使用的代币大于需要销毁的代币数量。
require(balances[_from] >= _value);
require(_value <= allowed[_from][owner]);
balances[_from] -= _value;
allowed[_from][owner] -= _value;
_totalSupply -= _value;
emit Burn(_from, _value);
return true;
}
代币冻结
有时需要冻结账户代币,也就是此账户不能转账操作。
1.首先添加一个账户冻结代币的映射
mapping (address => bool) public frozenAccount;
2.添加冻结的通知函数
event FrozenFunds(address target, bool frozen);
3..添加冻结的函数
frozenAccount[target] = freeze;
emit FrozenFunds(target, freeze);
}
4.在转账函数中判断涉及账户是否为冻结账户,否则不允许转账操作
require(!frozenAccount[to]);
批量代币空投
有时需要往很多地址空投一些代币,这样可以使用批量转账。
假设从管理员账户空投。关键字memory为声明内存型的,存储的内容会在函数被调用(包括外部函数)时擦除,所以其使用开销相对较小。
function AirDrop(address[] memory _recipients, uint _values) onlyOwner public returns (bool) {
require(_recipients.length > 0);
for(uint j = 0; j < _recipients.length; j++){
transfer(_recipients[j], _values);
}
return true;
}
当然在实际应用中空投的数量并不是绝对的,大部分业务根据持有的代币比例或者NFT来计算空投。
代币兑换
有时代币需要与其他货币(Ether)进行兑换。
msg.value表示随消息发送的wei的数量,payable修饰函数表示允许从调用中接收以太币。
1.设置买卖价格的变量
uint256 public buyPrice;
2.设置价格函数
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}
3.接收以太币进行买操作
uint amount = msg.value / buyPrice;
emit Transfer(address(this), owner, amount);
}
4.卖操作
require(address(this).balance >= amount * sellPrice);
emit Transfer(owner, address(this), amount);
owner.transfer(amount * sellPrice);
}
当然在实际应用中的价格由dex或者uniswap等去中心化交易所,通过AMM协议的交易对计算得出。
openzeppelin ERC-20实现
我们在写合约业务的时候可以选择自己实现ERC20的协议,当然也可以使用openzeppelin的第三方库。openzeppelin中有完善的ERC20实现。
首先我们来看下一下openzeppelin官方文档中对于ERC20的描述:
ERC20 代币合约跟踪可替代代币:任何一种代币都完全等同于任何其他代币;没有任何代币具有与之相关的特殊权利或行为。这使得 ERC20 代币可用于交换货币、投票权、质押等媒介。
使用openzeppelin来构建erc20-token也十分的方便:
我们在hardhat工程中通过 npm install @openzeppelin/contracts 来安装openzeppelin的支持,就可以import进来使用:
我们的合约经常通过继承使用,在这里我们将重用ERC20基本标准实现以及name,symbol和decimals可选扩展。此外,我们正在创建一个initialSupply令牌,它将分配给部署合约的地址。
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract GLDToken is ERC20 {
constructor(uint256 initialSupply) public ERC20("Gold", "GLD") {
_mint(msg.sender, initialSupply);
}
}
就是这样!部署后,我们将能够查询部署者的余额:
> GLDToken.balanceOf(deployerAddress)
1000000000000000000000
我们也可以将这些代币转移到其他账户:
> GLDToken.transfer(otherAddress, 300000000000000000000)
> GLDToken.balanceOf(otherAddress)
300000000000000000000
> GLDToken.balanceOf(deployerAddress)
700000000000000000000
关于decimals
通常,您希望能够将您的代币分成任意数量:例如,如果您拥有5 GLD,您可能想发送1.5 GLD给朋友,并留给3.5 GLD自己。不幸的是,Solidity 和 EVM 不支持这种行为:只能使用整数(整数)。为了解决这个问题,ERC20提供了一个decimals字段,用于指定令牌有多少个小数位。为了能够转移1.5 GLD,decimals必须至少1,因为该数字有一个小数位。就像ETH的最小单位是wei。1eth = 10^18wei。
在openzeppelin中默认情况下,ERC20使用值18for decimals。要使用不同的值,您需要在构造函数中调用_setupDecimals。
因此,如果您想5使用 18 位小数的代币合约发送代币,调用的方法实际上是:
transfer(recipient, 5 * 10^18);