概述

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);