以太坊开发入门-深度解析ERC20标准

  • EimJacky
  • 更新于 2024-08-23 10:27
  • 阅读 1063

本文深入解析了ERC-20标准,这是以太坊区块链上用于创建和管理代币的核心标准。文章详细介绍了ERC-20标准的主要功能、关键函数及其实现方式,同时探讨了在代币开发中可能遇到的挑战和安全问题

参考:github.com/AmazingAng/WTF-Solidity

1、ERC20标准规范

ERC20是以太坊上的代币标准,它实现了游戏代币转账的基本逻辑:

  • 账户余额(balanceOf())
  • 转账(transfer())
  • 授权转账(transferFrom())
  • 授权(approve())
  • 代币总供给(totalSupply())
  • 授权转账额度(allowance())
  • 代币信息(可选):名称(name()),代号(symbol()),小数位数(decimals())

2、编写ERC20函数接口

IERC20ERC20代币标准的接口合约,规定了ERC20代币需要实现的函数和事件。 在接口函数中,只需要定义函数名称,输入参数,输出参数。后续将在接口实现合约当中完成接口的代码业务逻辑。

2.1 定义ERC20事件

IERC20定义了2个事件:Transfer事件和Approval事件,分别在转账和授权时被释放

/**
 * @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
 * param (address , address , uint256)
 */
event Transfer(address indexed from, address indexed to, uint256 value);

/**
 * @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
 * param (address , address , uint256)
 */
event Approval(address indexed owner, address indexed spender, uint256 value);

2.2 定义函数接口

IERC20定义了6个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。

  • totalSupply()

用于返回代币总供给

/**
 * @dev 返回代币总供给.
 * param 
 * return uint256 代币总供给
 */
function totalSupply() external view returns (uint256);
  • balanceOf()

用于返回账户余额

/**
 * @dev 返回账户`account`所持有的代币数.
 * param address 账户地址 
 * return uint256 账户余额
 */
function balanceOf(address account) external view returns (uint256);
  • transfer()

代币转账

/**
 * @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`.
 * param  (address,uint256) (接收地址,转账金额) 
 * return bool
 * 如果成功,返回 `true`.
 *
 * 释放 {Transfer} 事件.
 */
function transfer(address to, uint256 amount) external returns (bool);
  • allowance()

返回授权额度

/**
 * @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。
 * param  (address  , address) (授权账户 , 接收账户 )
 * return uint256 授权额度
 * 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变.
 */
function allowance(address owner, address spender) external view returns (uint256);
  • approve()

代币授权

/**
 * @dev 调用者账户给`spender`账户授权 `amount`数量代币。
 * param  (address , uint256) (目标账户,授权额度)
 * return bool
 * 如果成功,返回 `true`.
 *
 * 释放 {Approval} 事件.
 */
function approve(address spender, uint256 amount) external returns (bool);
  • transferFrom()

授权转账

/**
 * @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。
 * param  (address , address , amount) (转账账户 , 目标账户,转账金额)
 * return bool
 * 如果成功,返回 `true`.
 *
 * 释放 {Transfer} 事件.
 */
function transferFrom(
    address from,
    address to,
    uint256 amount
) external returns (bool);

3、编写IERC20错误接口

IERC20Errors定义了6个错误,帮助我们在实现代码业务逻辑时捕获错误异常

  • ERC20InsufficientBalance错误

在转账错误时候触发,表明发送方余额不足

    /**
     * @dev 表示与 “发送方 ”当前 “余额 ”有关的错误。用于转账
     * param (address , uint256,uint256) (转账账户 , 账户余额 ,转账金额)
     */
    error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
  • ERC20InvalidSender错误

在转账错误时候触发,多用于零地址发送转账,即发送方为零地址

    /**
     * @dev 表示代币 “发送 ”失败。用于转账。
     * param (address) (转账地址)
     */
    error ERC20InvalidSender(address sender);
  • ERC20InvalidReceiver错误

在转账错误时候触发,多于与向零地址发送转账。即接收方为零地址

    /**
     * @dev 表示代币 “接收 ”失败。用于转账
     * param (address) (接收地址)
     */
    error ERC20InvalidReceiver(address receiver);
  • ERC20InsufficientAllowance错误

多用于检查授权额度时候触发,表明支出人spender的可支配额度不足以消耗此次转账

    /**
     * @dev 表示 “spender ”的 “allowance ”失败。用于转账.
     * param (address , uint256 , uint256) (支出人,授权额度,转账金额)
     */
    error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
  • ERC20InvalidApprover错误

在授权的时候触发,多用于零地址向其他账户授权token

    /**
     * @dev 表示授权token的 `approver` 失败。用于approve
     * param (address) (授权账户)
     */
    error ERC20InvalidApprover(address approver);
  • ERC20InvalidSpender错误

在授权的时候触发,多用于向零地址授权token

    /**
     * @dev 表示授权token的 `spender` 失败。用于approve。
     * param (address) (支出账户)
     */
    error ERC20InvalidSpender(address spender);

4、实现ERC20

现在我们写一个ERC20,将IERC20规定的函数实现。

4.1 状态变量

我们需要状态变量来记录账户余额,授权额度和代币信息。其中balanceOf, allowancetotalSupplypublic类型,会自动生成一个同名getter函数,实现IERC20规定的balanceOf(), allowance()totalSupply()。而name, symbol, decimals则对应代币的名称,代号和小数位数。

注意:用override修饰public变量,会重写继承自父合约的与变量同名的getter函数,比如IERC20中的balanceOf()函数。

mapping(address => uint256) public override balanceOf;

mapping(address => mapping(address => uint256)) public override allowance;

uint256 public override totalSupply;   // 代币总供给

string public name;   // 名称
string public symbol;  // 代号

uint8 public decimals = 18; // 小数位数

4.2 函数

  • 构造函数:初始化代币名称、代号。
constructor(string memory name_, string memory symbol_){
    name = name_;
    symbol = symbol_;
}
  • update()函数:在转账时候更新代币状态变量

    fromto 转移 value 数量的代币,或者,如果 from(或 to 是零地址),则进行mint(或burn)。或 to)为零地址时,则可使用mint(或burn)。

function _update(address from , address to , uint256 value) internal {
        if (from == address(0)){
            //from 为零地址 代表代币新铸造 非用户之间转账
            //溢出检查 
            totalSupply += value;
        }else {
            uint256 fromBalance = balanceOf[from];
            if (fromBalance < value){
                //发送方余额小于转账金额 触发ERC20InsufficientBalance错误
                revert ERC20InsufficientBalance(from,fromBalance,value);
            }
            //溢出已检查 发送方余额足够 使用unchecked节省gas费
            unchecked{
                balanceOf[from] = fromBalance - value;
            }
        }
        if (to == address(0)){
            //溢出已在from代码校验检查
            unchecked{
                totalSupply -= value;
            }
        }else {

            unchecked{
                balanceOf[to] += value;
            }
        }
        //触发转账事件
        emit Transfer(from, to, value);
    }
  • _mint()函数:铸造代币
function _mint(address account , uint256 value)internal {
        //地址检查 mint铸造接收方不应该是零地址 捕获错误ERC20InvalidReceiver()
        if (account == address(0)){
            revert ERC20InvalidReceiver(address(0));
        }
        //mint也可视作代币之间的转账 由零地址向接收方转账,所以底层调用update()更新代币状态
        _update(address(0), account, value);
    }
  • _burn()函数:销毁代币
function _burn(address account , uint256 value)internal {
        //地址检查 burn销毁发送方不应是零地址 捕获错误ERC20InvalidSender()
        if (account == address(0)){
            revert ERC20InvalidSender(account);
        }
        //burn也可视作代币之间的转账,由发送方向零地址转账,代币进入黑洞,底层调用_update()更新代币状态
        _update(account, address(0), value);
    }
  • _transfer()函数:底层转账函数
function _transfer(address from , address to , uint256 value) internal {
        //transfer底层逻辑,地址校验
        if ( from == address(0)){
            revert ERC20InvalidSender(address(0));
        }
        if (to == address (0)){
            revert ERC20InvalidReceiver(address(0));
        }
        _update(from, to, value);
    }
  • _approve()函数:底层授权函数

授权函数即更改授权其他用户可供支配自己账户余额的额度

    function _approve(address owner , address spender , uint256 value , bool emitEvent)internal  {
        //地址校验
        if (owner == address(0)){
             revert ERC20InvalidApprover(address(0));
        }
        if (spender == address(0)){
            revert ERC20InvalidSpender(address(0));
        }
        allowance[owner][spender] = value;
        //判断是否需要释放授权事件
        if (emitEvent){
            emit Approval(owner, spender, value);
        }

注意:如果是spender在消耗owner的余额,是不用释放授权事件的,所以在这里引入emitEvent参数区分不同场景。

  • _spendAllowance()函数:消耗授权额度
    function _spendAllowance(address owner , address spender , uint256 value)internal {
        //获取当前授权额度
        uint256 currentAllowance = allowance[owner][spender];
        if (currentAllowance != type(uint256).max){
            //额度校验 捕获ERC20InsufficientAllowance异常
            if (currentAllowance < value){
                revert ERC20InsufficientAllowance(spender , currentAllowance , value);
            }
            //更新授权额度 不用释放授权事件
            unchecked {
                _approve(owner, spender, currentAllowance - value, false);
            }
        }
    }
  • transferFrom()函数:spender转账owner代币
function transferFrom(address from , address to , uint256 amount)public returns(bool){
        address spender = msg.sender;
        //更新授权额度
        _spendAllowance(from, spender, amount);
        //执行转账逻辑
        _transfer(from, to, amount);
        return  true;
    }
  • transfer()函数:owner转账代币
    function transfer(address to , uint256 amount) public   returns (bool){
        address owner = msg.sender;
        //执行转账逻辑
        _transfer(owner, to, amount);
        return  true;
    }
  • approve()函数:owner授权spender代币
    function approve(address spender , uint256 value) public  returns (bool){
        address owner = msg.sender;
        //执行授权逻辑
        _approve(owner, spender, value, true);
        return  true;
    }

5、发行代币合约

新建Token.sol文件,继承ERC20合约

contract Token is ERC20 {

    address private  _owner ; 

    constructor(address owner_,string memory name_, string memory symbol_)ERC20(name_,symbol_){
        _owner = owner_;
    }

    modifier onlyOwner() {
        require(msg.sender == _owner);
        _;
    }
    function mint(address to , uint256 amount)public  onlyOwner{
        _mint(to, amount);
    }
    function burn(address to , uint256 amount) public  onlyOwner{
        _burn(to, amount);
    }
}

<!--StartFragment--> 总结: 文章全面解析ERC-20标准,介绍了该标准的核心功能和实现细节。ERC-20标准作为以太坊区块链上的通用代币接口,定义了一系列规范,使得代币可以在不同的应用和平台之间互操作。文章深入探讨了ERC-20标准中的关键函数及其作用,并强调了代币开发中需要注意的安全性和兼容性问题。通过对这些内容的理解,读者可以更好地掌握如何在以太坊上创建和管理自己的代币,以及如何在实际应用中有效利用ERC-20标准。

<!--EndFragment-->

点赞 2
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
EimJacky
EimJacky
0x1a8b...2e23
狂热的区块链爱好者 Long Way To Do in Blockchain Study