ERC20 代币

这篇文章详细介绍了如何创建一个ERC20代币,包括代币的基本构造、余额管理、铸造与转移功能的实现,以及如何使用允许机制进行代币转移。文章还引入了小数的概念,并提出了一个清理建议以简化代币转移代码,使其更加整洁。整体内容适合希望深入了解ERC20标准的读者。

我们现在准备好创建一个 ERC20 代币了!

ERC20 代币通常有一个 名称 和一个 符号。例如,ApeCoin 的名称是 “ApeCoin”,符号是 “APE”。代币的名称一般不会改变,因此我们会在构造函数中设置它,并且不提供任何函数让它在后续改变。我们将这些变量设为公开,以便任何人都可以查看合约的名称和符号。

contract ERC20 {
    string public name;
    string public symbol;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
    }
}

接下来,我们需要储存每个人的余额。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
    }
}

我们说 “balanceOf” 是因为这部分是 ERC20 的 规范。ERC20 作为一个规范意味着人们可以在你的合约上调用 “balanceOf” 函数,提供一个地址,并获取该地址拥有多少代币。

现在每个人的余额都是零,所以我们需要一种方法来创造代币。我们将允许一个特殊地址,即部署合约的人,自由创造代币。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        balanceOf[owner] += amount;
    }
}

通常,函数 mint() 接受 toamount 作为参数。它允许合约部署者将代币铸造到其他账户。为了简单起见,函数 mint() 只是允许部署者将代币铸造到他的账户中。

为了跟踪现存的代币数量,ERC20 规范要求有一个公共函数或变量叫 totalSupply,用来告诉我们已有多少代币被创造出来。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;

    uint256 public totalSupply;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }

    function transfer(address to, uint256 amount) public {
        require(balanceOf[msg.sender] >= amount, "you aint rich enough");
        require(to != address(0), "cannot send to address(0)");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
}

如果你在钱包中使用过 ERC20 代币,毫无疑问,你见过你拥有一定数量的代币的情况。即使无符号整数没有小数,这种情况是如何发生的呢?

一个 uint256 能表示的最大数字是

115792089237316195423570985008687907853269984665640564039457584007913129639935

让我们稍微简化这个数字,以便更清楚。

10000000000000000000000000000000000000000000000000000000000000000000000000000

为了能够描述 “decimals”,我们说右侧的 18 个零是代币的分数部分。

10000000000000000000000000000000000000000000000000000000000.000000000000000000

因此,如果我们的 ERC20 具有 18 个小数,我们最多可以拥有

10000000000000000000000000000000000000000000000000000000000

完整代币,右侧的零是小数。这是 10 八十亿(octodecillion)代币,或对于那些不熟悉如此庞大的数字的人,这相当于 1 千兆 x 1 千兆 x 1 千兆 x 1 万亿。

10 八十亿应该足够大多数应用程序使用,包括那些经历恶性通货膨胀的国家。

货币的 “单位” 仍然是整数,但单位现在是非常小的数值。

18 个小数位是相当标准的,但一些代币使用 6 个小数位。

代币的小数位不应改变,它只是一个返回代币有多少小数位的函数。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;
    uint8 public decimals;

    uint256 public totalSupply;

    constructor(string memory _name, string memory _symbol, uint8 decimals) {
        name = _name;
        symbol = _symbol;
        decimals = 18;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }
}

如果你在注意,我在这里给你抛出了一个难题。数字类型是 uint8,而不是 uint256。uint8 最多只能表示到 255。然而,uint256 有 77 个零(如果你想数一数上面的零,你可以验证这一点)。因此,如果你想要拥有一个完整的代币,最多不能有超过 77 位小数。因此,标准规定我们使用 uint8,因为小数的数量永远不会非常大。

转账

现在让我们把我们的转账函数加回来。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;
    uint8 public decimals;

    uint256 public totalSupply;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
        decimals = 18;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }

    function transfer(address to, uint256 amount) public {
        require(balanceOf[msg.sender] >= amount, "you aint rich enough");
        require(to != address(0), "cannot send to address(0)");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
}

啊,我们偷偷加了一行代码: require(to != address(0), "cannot send to address(0)")

这是为什么呢?因为没有人“拥有”零地址,所以发到零地址的代币是不可花费的。按照约定,将代币发送到零地址应该减少 totalSupply,所以我们希望有一个单独的函数来处理这个问题。

现在我们引入一个 allowance 的概念。

Allowance

Allowance 允许一个地址支出其他人的代币,最多到他们指定的限制。

为什么你要允许某人代替你花费代币?这是一个很长的故事,不过简单来说,想想你是如何“知道”某人转账给你 ERC20 代币的。发生的只是一个函数被执行,映射的值被改变。你并没有“收到”这些代币,它们只是与你的地址相关联。

然而,智能合约无法这样做。

智能合约作为接收转账的既定模式是允许智能合约拥有一定的 allowance,然后告知该智能合约从你的账户中提取余额。

当你想将代币转移到智能合约时,通常的方法是首先批准智能合约从你的帐户中提取一定数量的代币。然后,你指示智能合约从你的帐户中提取已批准数量的代币。这是在智能合约中用于启用代币转移到合约的常见模式。

让我们添加 allowance 跟踪器,以及给其他用户授权的方式。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;
    uint8 public decimals;

    uint256 public totalSupply;

    // owner -> spender -> allowance
    // 这使得一个拥有者可以给予多个地址的授权
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
        decimals = 18;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }

    function transfer(address to, uint256 amount) public {
        require(balanceOf[msg.sender] >= amount, "you aint rich enough");
        require(to != address(0), "cannot send to address(0)");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }

    // 刚刚添加的
    function approve(address spender, uint256 amount) public {
        allowance[msg.sender][spender] = amount;
    }
}

在这一行 allowance[msg.sender][spender] = amount; 中,spender 指的是正被 msg.sender 授权的账户地址。msg.sender 是在其账户中给 spender 花费一定数量代币的权限。

因此,msg.sender 是代币的拥有者,而 spender 是被拥有者批准可以代表他们消费一定数量代币的帐户。

但是,我们没有办法实际使用我们所给予的授权,授权就停在那里!这就是 transferFrom 的作用。

transferFrom

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;
    uint8 public decimals;
    uint256 public totalSupply;

    // owner -> spender -> allowance
    // 这使得一个拥有者可以给予多个地址的授权
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
        decimals = 18;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }

    function transfer(address to, uint256 amount) public {
        require(balanceOf[msg.sender] >= amount, "you aint rich enough");
        require(to != address(0), "cannot send to address(0)");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }

    function approve(address spender, uint256 amount) public {
        allowance[msg.sender][spender] = amount;
    }

    // 刚刚添加的
    function transferFrom(address from, address to, uint256 amount) public {
        require(balanceOf[from] >= amount, "not enough money");

        if (msg.sender != from) {
            require(allowance[from][msg.sender] >= amount, "not enough allowance");
            allowance[from][msg.sender] -= amount;
        }

        balanceOf[from] -= amount;
        balanceOf[to] += amount;
    }
}

让我们详细拆解一下我们刚才做的事情。

首先,代币的拥有者是可以调用 transferFrom 的。在这种情况下,allowance 是没有意义的,因此我们不必检查 allowance 映射,而是相应地更新余额。

否则,我们需要检查花费者是否获得足够的 allowance,然后减少他们的支出。如果我们不去减去他们的支出,我们将拥有无限的支出能力。

还有最后一个清理工作。如果我们查看 EIP 20 的原始规范,它说明 approve、transfer 和 transferFrom 必须在成功后返回 true。所以我们来加上这一点。

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;
    uint8 public decimals;

    uint256 public totalSupply;

    // owner -> spender -> allowance
    // 这使得一个拥有者可以给予多个地址的授权
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
        decimals = 18;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "you aint rich enough");
        require(to != address(0), "cannot send to address(0)");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;

        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "not enough money");
        require(to != address(0), "cannot send to address(0)");

        if (msg.sender != from) {
            require(allowance[from][msg.sender] >= amount, "not enough allowance");
            allowance[from][msg.sender] -= amount;
        }

        balanceOf[from] -= amount;
        balanceOf[to] += amount;

        return true;
    }
}

冒着过多信息给你的风险,有一个小的清理工作可以完成。注意到 transferFromtransfer 中有重复的代码。我们该怎么解决呢?我们可以将余额更新的代码提取到一个单独的函数中,但我们需要确保该函数不是 public 的以免有人盗取代币!

contract ERC20 {
    string public name;
    string public symbol;

    mapping(address => uint256) public balanceOf;
    address public owner;
    uint8 public decimals;

    uint256 public totalSupply;

    // owner -> spender -> allowance
    // 这使得一个拥有者可以给予多个地址的授权
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
        decimals = 18;

        owner = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "only owner can create tokens");
        totalSupply += amount;
        balanceOf[owner] += amount;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        return helperTransfer(msg.sender, to, amount);
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        if (msg.sender != from) {
            require(allowance[from][msg.sender] >= amount, "not enough allowance");
            allowance[from][msg.sender] -= amount;
        }

        return helperTransfer(from, to, amount);
    }

    function helperTransfer(address from, address to, uint256 amount) internal returns (bool) {
        require(balanceOf[from] >= amount, "not enough money");
        require(to != address(0), "cannot send to address(0)");
        balanceOf[from] -= amount;
        balanceOf[to] += amount;

        return true;
    }
}

干得更漂亮了!

练习题

  • 修改上述代码,使其不允许超过 100 万代币流通,即使拥有者尝试铸造更多。

学习更多

请查看 区块链训练营,学习更多关于智能合约开发和代币标准的信息。

  • 原文链接: rareskills.io/learn-soli...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/