solidity新手 大白话搞懂 极简 ERC20

  • 1_bit
  • 更新于 2022-10-10 21:26
  • 阅读 3972

用大白话搞懂一个 erc20 标准,搞懂每个接口最终实现一个最基本的ERC20代币合约。

一、什么是标准什么是ERC20标准

ERC20 是 eth 的一个标准,怎么理解标准一词呢?

标准是大家遵循的一个协议,根据这个协议大家都知道该怎么去做,例如去吃饭的时候人多,你就需要排队,然后去窗口跟阿姨说你要吃什么,阿姨就会帮你打;若你不准守这个标准,直接冲进后厨,翻开泔水,大喊着我要吃饭...这个时候就完全背离了这个标准,所以被赶走了。

以上所述以 开玩笑 的方式讲述了什么是标准,所以在我们要使用 ERC20 标准完成这个标准的结果时,就需要遵守这个标准。

ERC20 是以太坊上的一种代币合约标准,你要实现这个代币或者说你要发个币那么就得给这个代币一个名字、怎么转账、总量、授权 等这些功能(标准),否则别人拿你的币都不能转账,难道就是看嘛,所以标准我们得实现。

具体标准我们可以看这个:https://eips.ethereum.org/EIPS/eip-20

以上的标准简而言之就是实现一些接口就可以了,以下是一个标准的ERC20标准:

// SPDX-License-Identifier:MIT
pragma solidity 0.8.17;
interface IERC20 {
    //发行的代币总量
    function totalSupply() external view returns (uint256);
    //某地址余额
    function balanceOf(address account) external view returns (uint256);
    //从当前账户对某个地址转amount的钱
    function transfer(address account, uint256 amount) external returns (bool);
    //授权某个账户可以用你的钱(用多少钱是指定的)
    function approve(address spender, uint256 amount) external returns (bool);
    //你授权的账户还可以有多少你授权的钱可以用
    function allowance(address owner, address spender) external view returns (uint256);
    //授权用户的转账方法,只针对授权用户使用
    function transferFrom(address from,address to,uint256 amount) external returns (bool);
    //转账时触发转账事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    //授权时触发授权事件
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

用文字列出来,那么这些接口方法就包括:

  • 代币总量 totalSupply
  • 某地址余额 balanceOf
  • 转账 transfer
  • 授权 approve
  • 查看授权账户余额 allowance
  • 授权用户转账 transferFrom
  • 转账事件 Transfer
  • 授权事件Approval

二、简单 ERC20标准实现

2.1 父合约

实现 ERC20 标准首先我们创建一个合约,在合约中将第一点的接口作为需要继承的对象,当然你也可以另起一个文件进行 import:

// SPDX-License-Identifier:MIT
pragma solidity 0.8.17;
interface IERC20 {
    //发行的代币总量
    function totalSupply() external view returns (uint256);
    //某地址余额
    function balanceOf(address account) external view returns (uint256);
    //从当前账户对某个地址转amount的钱
    function transfer(address account, uint256 amount) external returns (bool);
    //授权某个账户可以用你的钱(用多少钱是指定的)
    function approve(address spender, uint256 amount) external returns (bool);
    //你授权的账户还可以有多少你授权的钱可以用
    function allowance(address owner, address spender) external view returns (uint256);
    //授权用户的转账方法,只针对授权用户使用
    function transferFrom(address from,address to,uint256 amount) external returns (bool);
    //转账时触发转账事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    //授权时触发授权事件
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

2.2 创建合约

接着编写一个合约叫做 BitCoinDemo:

contract BitCoinDemo is IERC20{

}

2.3 代币名称、总量、余额等状态变量编写

接下来是不是应该到我们需要用到一些变量来存储这个代币名称、总量以及余额了?

此时创建一个变量用来存储一个地址与余额的关系,那么使用 map 类型的数据:

//余额
mapping(address => uint256)public balances;

那么接下来就是对应的总量了:

//总量
uint256 public total = 100000000;

那么就还有你的代币全名、简称和小数了:

//代币名
string public constant name = "1BITCOINERC20DEMO";
//简称
string public constant symbol = "1BitCoin";
//小数点
uint8 public constant decimals = 18;

其中小数点位置我们为 18就好,一般都是写18。

当然,你这些内容都可以在合约部署的时候再传入,在这里我就简单编写了。

2.4 构造函数给自己代币

接着,我们可以编写一个构造函数,将即将我们要创建的代币给与当前合约的创建者:

constructor() {
    balances[msg.sender] = total;
}

毕竟这个关系就是某个地址又多少余额,那么我总量是 total,那不就是给当前创建合约的人所有余额就好了。

2.5 实现 totalSupply() 方法

接着开始实现 totalSupply() 方法,我们只需要返回当前合约的总量即可,那么就可以写成:

//返回总量
function totalSupply()override public view returns (uint256) {
    return total;
}

记得,一定要写 override,毕竟是父接口的方法重写。

2.6 实现 转账 方法

既然我有了币,那么接下来就应该有一个转账方法,我们实现 transfer 方法:

//转账代币
function transfer(address account, uint256 amount) public override returns (bool) {
    require(balances[msg.sender]>amount);//判断钱够不够
    balances[msg.sender] = balances[msg.sender]-amount;//原账户减去给自己
    balances[account] = balances[account]+amount;//给别人账户加上转账的钱
    emit Transfer(msg.sender, account, amount);//响应这个事件
    return true;
}

以上的转账方法很简单,接收两个参数,一个是你要转给谁的账户 account,还有一个你要转多少钱 amount,最后返回 bool 是否转账成功

在方法中首先判断钱是否足够,够的话就给原账户减去转出去的钱,别人账户加上转出去的钱就ok了。

2.7 查看余额

既然已经转账到别人账户了,那么此时还需要对应的查看一下别人的余额,那么此时就实现 balanceOf 方法:

//余额查看
function balanceOf(address account)override public view returns (uint256) {
    return balances[account];
}

直接返回那个 balances 的映射结果就得到余额了。

2.8 指定授权账户

授权账户需要一个 map 来存储,但是跟之前的 map 不太一样,如下:

//授权用户及余额
mapping(address => mapping (address => uint256)) appbalances;

为啥要这样写这个 map 呢?那是因为你授权肯定是 A账户 授权给了 B账户 多少钱,所以此时就是两个 address,最外层的 address 就是授权人,这个授权人下的 address 就是授权给的某人,二 对应的 uint256 数据则是授权的金额,方法如下:

//授权方法
function approve(address spender, uint256 amount)override public returns (bool) {
    appbalances[msg.sender][spender] = amount;
    emit Approval(msg.sender, spender, amount);
    return true;
}

以上这个方法呢 spender 就是需要授权给的地址,amount 就是给这个地址授权的金额数量,那么 appbalances[msg.sender][spender] = amount; 就表示在这个 appbalances 授权金额 map 中添加一个记录,msg.sender 是我自己的地址,那意思就是我自己授权给了 spender 一个金额,这个金额是 amount。

随后再用 emit 触发一个事件。

2.9 指定授权账户

查看授权账户余额也很简单了,传入两个地址,一个地址是授权人,另一个是被授权人,返回对应的 appbalances 数据,那么就得到值了,那么这个方法编写如下:

//查看授权账户余额
function allowance(address owner, address spender)override public view returns (uint) {
    return appbalances[owner][spender];
}

2.10 授权用户的专用转账方法

授权用户是有一个专用的转账方法的,毕竟这两者存储的结构都不一样,那么此时就实现最后一个需要实现的授权用户的转账方法:

//授权用户的转账方法
function transferFrom(address from, address to, uint256 amount)override public returns (bool) {
    require(amount <= balances[from]);
    require(amount <= appbalances[from][msg.sender]);

    balances[from] = balances[from]-amount;
    appbalances[from][msg.sender] = appbalances[from][msg.sender]-amount;
    balances[to] = balances[to]+amount;
    emit Transfer(from, to, amount);
    return true;
}

以上代码中的第一行,为啥要判断 balances 非授权用户的余额呢?那是因为我授权给你的钱那也是我的钱,那么肯定我的钱都不够,你肯定也不够了。要注意这个关系,是授权而不是转账给你。

知道以后那就明白为什么要写 require(amount <= balances[from]); 了,那么判断完授权用户的余额后开始判断被授权用户的余额是否足够,足够了就继续往下走。

首先得从授权账户的余额里面扣除要支出的部分 balances[from] = balances[from]-amount;,接着再从被授权的人那里扣除支出部分 appbalances[from][msg.sender] = appbalances[from][msg.sender]-amount;;有些同学可能会问不是已经从授权账户扣除了为什么还要从被授权账户扣除呢?这是因为这是被授权用户发起的支出,而不是授权账户发起的支出,所以被授权账户要减去已经授权的余额。

接下来就直接导 balances 中为获得方添加余额即可,记住这个金额不是授权金额,所以直接 balances 进行添加即可。

最后响应一个事件及解决。

2.11 完整代码

那么此时的完整代码如下:

// SPDX-License-Identifier:MIT
pragma solidity 0.8.17;
interface IERC20 {
    //发行的代币总量
    function totalSupply() external view returns (uint256);
    //某地址余额
    function balanceOf(address account) external view returns (uint256);
    //从当前账户对某个地址转amount的钱
    function transfer(address account, uint256 amount) external returns (bool);
    //授权某个账户可以用你的钱(用多少钱是指定的)
    function approve(address spender, uint256 amount) external returns (bool);
    //你授权的账户还可以有多少你授权的钱可以用
    function allowance(address owner, address spender) external view returns (uint256);
    //授权用户的转账方法,只针对授权用户使用
    function transferFrom(address from,address to,uint256 amount) external returns (bool);
    //转账时触发转账事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    //授权时触发授权事件
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract BitCoinDemo is IERC20{

    //余额
    mapping(address => uint256)public balances;
    //总量
    uint256 public total = 10000000000000000;
    //代币名
    string public constant name = "1BITCOINERC20DEMO";
    //简称
    string public constant symbol = "1BitCoin";
    //小数点
    uint8 public constant decimals = 18;
    //授权用户及余额
    mapping(address => mapping (address => uint256)) appbalances;

    constructor() {
        balances[msg.sender] = total;
    }

    //返回总量
    function totalSupply()override public view returns (uint256) {
        return total;
    }
    //转账代币
    function transfer(address account, uint256 amount) public override returns (bool) {
        require(balances[msg.sender]>amount);//判断钱够不够
        balances[msg.sender] = balances[msg.sender]-amount;//原账户减去给自己
        balances[account] = balances[account]+amount;//给别人账户加上转账的钱
        emit Transfer(msg.sender, account, amount);//响应这个事件
        return true;
    }
    //余额查看
    function balanceOf(address account)override public view returns (uint256) {
        return balances[account];
    }

    //授权方法
    function approve(address spender, uint256 amount)override public returns (bool) {
        appbalances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    //查看授权账户余额
    function allowance(address owner, address spender)override public view returns (uint) {
        return appbalances[owner][spender];
    }

    //授权用户的转账方法
    function transferFrom(address from, address to, uint256 amount)override public returns (bool) {
        require(amount <= balances[from]);
        require(amount <= appbalances[from][msg.sender]);

        balances[from] = balances[from]-amount;
        appbalances[from][msg.sender] = appbalances[from][msg.sender]-amount;
        balances[to] = balances[to]+amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

三、新增功能

3.1 增发铸币

此时我们还可以对合约增加一些新的功能,例如铸币功能。你可以理解为铸币就是对代币进行增发(不是增头发),通过铸币可以创造出更多的代币,但是在代币总量上我们需要对其进行增加,否则你的发行跟实际记录不符,这样你的合约将会不可信。

//增发
function mint(address account, uint256 amount) external virtual {
   total += amount;
   balances[account] += amount;
   emit Transfer(address(0), account, amount);
}

以上是一个增发方法,通过传入增发后代币增加的账户地址,以及一个增发量,再到方法内对总量进行增加,并且记录增发的代币存储到哪一个账户之中,当然也要响应一个对应的交易事件(你也可以再搞一个增发事件)。

3.2 代币销毁

既然有了代币增发,那就来一个代币销毁。代表销毁可以销毁指定账户的代币,当然你也可以设定为只有“owner”账户,最开始存储的账户代币都行,在此只是做一个示例。

//销毁
function burn(address account, uint256 amount) external virtual {
    require(balances[account] >= amount, "burn error");
    balances[account] = balances[account] - amount;
    total -= amount;
    emit Transfer(account, address(0), amount);
}

代币销毁的方式跟增发的方式相反,当然还需要判断你指定的账户的余额否大于或等于需要销毁的量,接着就是往对应的 balances 里面去减去对应的 amount 了,总量也要对应的减去值,最后触发一个 Transfer 事件。

在此我们可以看到我们通过 发送方或者是接收方 为 0 地址表示销毁和增加,发送方为 0 地址则是增发,接收方为 0 地址则是销毁。

四、优化合约

通过以上的最基础的 ERC20 合约内容大概已经明白了怎么玩了,接下来我们为其新增一点内容,循序渐进感觉挺棒。

4.1 增发及销毁条件

增发及销毁条件需要满足是否是合约的 owner 调用,否则任意一个人都可以增发和销毁就乱套了,在此我们增加对应的 require:

require(owner==msg.sender,"sender error");

在此我们还需要创建一个状态变量存储当前的 owner:

address owner;
owner=msg.sender;

在这里插入图片描述

4.2 销毁代币优化

销毁代币之前是随便一个账户指定了就可以销毁那个账户代币,这明显就不对,不然我的钱就非常不安全了,在此我们设置只能够销毁自己的代码: 在这里插入图片描述

还需要把多余的参数删除哟。

4.3 完整代码

其他情况就增加判断是否是 0 地址就ok了,毕竟本篇文章只是介绍基础的 ERC20,并不是做一个“完善”的ERC20 合约: 在这里插入图片描述 接着我们部署完毕后(测试网)导入代币: 在这里插入图片描述

在这里插入图片描述

接着我们来个增发,输入地址和增加量:

在这里插入图片描述

等待交易完成: 在这里插入图片描述 现在钱多多了,并且使 Mint 方法发送的。

在测试一下销毁:

在这里插入图片描述 销毁成功: 在这里插入图片描述 基本上都操作正常: 在这里插入图片描述

最终代码完整如下:

// SPDX-License-Identifier:MIT
pragma solidity 0.8.17;
interface IERC20 {
    //发行的代币总量
    function totalSupply() external view returns (uint256);
    //某地址余额
    function balanceOf(address account) external view returns (uint256);
    //从当前账户对某个地址转amount的钱
    function transfer(address account, uint256 amount) external returns (bool);
    //授权某个账户可以用你的钱(用多少钱是指定的)
    function approve(address spender, uint256 amount) external returns (bool);
    //你授权的账户还可以有多少你授权的钱可以用
    function allowance(address owner, address spender) external view returns (uint256);
    //授权用户的转账方法,只针对授权用户使用
    function transferFrom(address from,address to,uint256 amount) external returns (bool);
    //转账时触发转账事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    //授权时触发授权事件
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract BitCoinDemo is IERC20{

    //余额
    mapping(address => uint256)public balances;
    //总量
    uint256 public total = 10000000000000000;
    //代币名
    string public constant name = "1BITCOINERC20DEMO";
    //简称
    string public constant symbol = "1BitCoin";
    //小数点
    uint8 public constant decimals = 18;
    //授权用户及余额
    mapping(address => mapping (address => uint256)) appbalances;
    //拥有者
    address owner;

    constructor() {
        owner=msg.sender;
        balances[msg.sender] = total;
    }

    //返回总量
    function totalSupply()override public view returns (uint256) {
        return total;
    }
    //转账代币
    function transfer(address account, uint256 amount) public override returns (bool) {
        require(account==address(0),"sender error");
        require(balances[msg.sender]>amount);//判断钱够不够
        balances[msg.sender] = balances[msg.sender]-amount;//原账户减去给自己
        balances[account] = balances[account]+amount;//给别人账户加上转账的钱
        emit Transfer(msg.sender, account, amount);//响应这个事件
        return true;
    }
    //余额查看
    function balanceOf(address account)override public view returns (uint256) {
        return balances[account];
    }

    //授权方法
    function approve(address spender, uint256 amount)override public returns (bool) {
        require(spender==address(0),"sender error");
        appbalances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    //查看授权账户余额
    function allowance(address owner, address spender)override public view returns (uint) {
        return appbalances[owner][spender];
    }

    //授权用户的转账方法
    function transferFrom(address from, address to, uint256 amount)override public returns (bool) {
        require(amount <= balances[from]);
        require(amount <= appbalances[from][msg.sender]);
        require(to==address(0),"sender error");

        balances[from] = balances[from]-amount;
        appbalances[from][msg.sender] = appbalances[from][msg.sender]-amount;
        balances[to] = balances[to]+amount;
        emit Transfer(from, to, amount);
        return true;
    }

    //增发
    function mint(address account, uint256 amount) external virtual {
        require(owner==msg.sender,"sender error");
        total += amount;
        balances[account] += amount;
        emit Transfer(address(0), account, amount);
    }

    //销毁
    function burn(uint256 amount) external virtual {
        require(owner==msg.sender,"sender error");
        require(balances[owner] >= amount, "burn error");
        balances[owner] = balances[owner] - amount;
        total -= amount;
        emit Transfer(owner, address(0), amount);
    }
}
点赞 7
收藏 8
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

4 条评论

请先 登录 后评论
1_bit
1_bit
转区块链中(求职)... InfoQ签约作者 蓝桥签约作者 CSDN、51、InfoQ专家、 2020年博客之星TOP5 CSDN第二季新星评委 CSDN新星导师 2021年博客新星评委 自媒体程序员 2021Infoq社区年度社区荣誉共建奖