继续跟着 HackQuest 学习合约知识
HackQuest 自己的合约编辑器
确定代币的发行者
在合约中确定部署者的唯一方式是在构造函数中使用msg.sender语法,这是因为构造函数的特殊性:构造函数只能在合约部署时执行一次,执行时默认的调用者为合约的部署者。
所以我们需要定义一个构造函数以确定代币的发行者。
solidity
pragma solidity 0.8.17;
contract MyToken {
address private owner;
constructor(){
owner = msg.sender;
}
}
跟踪每个地址拥有多少代币
为了跟踪每个地址拥有多少代币,我们需要一种数据结构来存储这些信息。
mapping是一种特殊的数据结构,它允许我们创建一个键-值对的映射,其中键是地址( address ),值是代币余额( uint )。这意味着我们可以将每个地址与其代币余额相关联。
使用映射的好处是,它使得我们可以快速查找每个账户的余额,因为映射是一种高效的数据结构。
映射的高效性体现在以下几个方面:
1.快速查找:由于映射中的键是唯一的,因此可以通过键直接访问相应的值,不需要遍历整个数据结构。
2.动态增删:映射可以动态添加或删除键-值对,因此可以很方便地实现代币的转账功能。通过修改映射中的相应键值对,可以快速更新账户地址的余额。
3.空间占用小:映射只需要存储键和对应的值,相对于数组等其他数据结构来说,空间占用较小。
我们还需要一个变量来表示已经发行的代币的总额。
这样,在交易和其他操作中,我们就可以确保代币的供应量不会超过预设值,从而维护代币的价值和稳定性。
而在数据类型的选择上,我们使用uint256,因为代币的总发行量不可能为负数,同时为了使该变量能表示的数值更大,所以我们选择uint256。
发行总量( totalSupply )是与每个账户地址的余额( balances )变量密切相关的,因为它代表了代币的发行总量,也就是所有地址的余额之和。
solidity
pragma solidity 0.8.17;
contract MyToken {
mapping (address => uint256) private balances;
uint256 public totalSupply;
address private owner;
constructor(){
owner = msg.sender;
}
}
铸造函数 mint
到目前为止,我们已经将代币合约的所有变量定义完成,且通过构造函数确定了代币的发行者。现在就可以来定义 mint 函数了!
首先,我们需要理解该函数的作用。 该函数的目的是给目标地址铸造指定数量的代币。
因此,我们在定义函数时需要做的是指定铸造代币的数量和一个接收者地址。当然,因为 mint 函数功能的特殊性,权限控制是必不可少的。 我们将使用require语句检查调用者地址是否为 owner 记录的发行者,如果不是,则会调用失败,此次交易将回滚。
回滚意味着区块链的数据将恢复到此次交易发生前的状态。
在进行了权限控制后,我们就可以真正开始"铸造"了。
首先当然是更新 balances 映射中记录的代币余额,我们需要将代币的数量 amount 加到与 recipient 账户地址对应的余额中。
既然新铸造出来了一些代币,是不是应该更新一下总发行量 totalSupply
solidity
pragma solidity 0.8.17;
contract MyToken {
mapping (address => uint256) private balances;
uint256 public totalSupply;
address private owner;
constructor(){
owner = msg.sender;
}
function mint(address recipient, uint256 amount) public {
require(msg.sender == owner);
balances[recipient] += amount;
totalSupply += amount;
}
}
查询余额
查询包括这里的几个步骤:
1.首先我们需要接收一个账户地址作为入参。
2.我们将要在 balances 映射中查找该地址对应的余额。
3.将该余额作为函数返回值返回。
查询函数不应该修改合约的状态变量,因此该函数应该带有view修饰符。
在选择函数的可见性时,我们选择使用public,这样无论是在合约内部还是在合约外部,都可以访问该函数以查询余额。
由于是查询余额,因此在函数的返回值部分,我们需要返回余额。在前一节中提到,余额一定是非负的,因此我们选择使用uint256作为返回值类型。
solidity
function balanceOf(address account) public view returns (uint256) {
return balances[account];
}
转账
截至目前,我们已经完成了:
●代币的铸造
●代币余额的查询
我们的代币系统已经初见雏形了,但要成为一个真正意义上的代币系统,还差一个交易功能,也就是转账。
转账包括这里的几个步骤
1.首先我们需要判断转账者的余额是否大于要转账的金额
2.然后我们需要通过映射分别获取转账者和接收者余额
3.然后在转账者的映射上减去要转账的金额
4.在收款者的映射上加上转账的金额
5.更新所有与金额有关的变量后,转账就完成了。
要完成转账,我们需要知道:
- 转款者
- 收款者
- 转账金额
按理来说,我们需要三个入参,但是由于函数的调用者可以被认为是转款者,我们可以通过msg.sender语法获取。
所以实际上在 transfer 函数中我们只需要两个入参,收款者和转账金额。其中,收款者是一个地址类型,所以我们用address类型的变量;金额是一个非负的数,由于 solidity 不支持小数,所以我们只能选择无符号整型来表示。
函数的可见性我们选择public,因为我们希望无论是在合约内还是在合约外,都能访问该函数来进行转账。
solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract MyToken {
//mapping 类型的变量,用于存储每个地址对应的余额
mapping (address => uint256) private balances;
//uint256 类型的变量,用于存储 Token 的总发行量。定义为 public,可以被任何人查询。
uint256 public totalSupply;
//address 类型的变量,用于存储此 Token 的发行者。用于一些权限控制
address private owner;
constructor() {
owner = msg.sender;
}
//用于铸造 Token 的函数
function mint(address recipient, uint256 amount) public {
//权限控制
require(msg.sender == owner,"Only the owner can perform this action");
balances[recipient] += amount;
totalSupply += amount;
}
//用于查询某个地址对应的 Token 余额
function balanceOf(address account) public view returns (uint256) {
return balances[account];
}
//用于转账的函数
function transfer(address recipient, uint256 amount) public returns (bool) {
require(amount <= balances[msg.sender], "Not enough balance.");
balances[msg.sender] -= amount;
balances[recipient] += amount;
return true;
}
}