这篇文章详细介绍了如何创建一个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() 接受 to 和 amount 作为参数。它允许合约部署者将代币铸造到其他账户。为了简单起见,函数 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;
}
}
冒着过多信息给你的风险,有一个小的清理工作可以完成。注意到 transferFrom 和 transfer 中有重复的代码。我们该怎么解决呢?我们可以将余额更新的代码提取到一个单独的函数中,但我们需要确保该函数不是 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;
}
}
干得更漂亮了!
练习题
请查看 区块链训练营,学习更多关于智能合约开发和代币标准的信息。
- 原文链接: rareskills.io/learn-soli...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!