子货币示例
以下合约实现了最简单的加密货币形式。该合约仅允许其创建者创建新硬币(可能有不同的发行方案)。任何人都可以互相发送硬币,而无需使用用户名和密码进行注册,您所需要的只是一个以太坊密钥对。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
// The keyword "public" makes variables
// accessible from other contracts
address public minter;
mapping (address => uint) public balances;
// Events allow clients to react to specific
// contract changes you declare
event Sent(address from, address to, uint amount);
// Constructor code is only run when the contract
// is created
constructor() {
minter = msg.sender;
}
// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}
// Errors allow you to provide information about
// why an operation failed. They are returned
// to the caller of the function.
error InsufficientBalance(uint requested, uint available);
// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
这份合约引入了一些新概念,让我们一一来介绍。
该行address public minter;
声明了一个地址类型的状态变量。该address
类型是一个 160 位的值,不允许任何算术运算。它适用于存储合约的地址,或属于外部账户的密钥对的公共一半的哈希。
关键字public
自动生成一个函数,允许您从合约外部访问状态变量的当前值。如果没有这个关键字,其他合约就无法访问该变量。编译器生成的函数代码等价于以下(暂时忽略external
和view
):
function minter() external view returns (address) { return minter; }
你可以自己添加一个像上面这样的函数,但是你会有一个同名的函数和状态变量。您不需要这样做,编译器会为您计算出来。
下一行mapping (address => uint) public balances;
也创建了一个公共状态变量,但它是一个更复杂的数据类型。映射类型将地址映射到无符号整数。
映射可以看作是虚拟初始化的哈希表,这样每个可能的键从一开始就存在,并映射到一个字节表示全为零的值。但是,既不可能获得映射的所有键的列表,也不可能获得所有值的列表。记录您添加到映射中的内容,或在不需要的上下文中使用它。或者更好的是,保留一个列表,或者使用更合适的数据类型。
在映射的情况下,由关键字public
创建的getter 函数更复杂。它如下所示:
function balances(address account) external view returns (uint) {
return balances[account];
}
您可以使用该功能查询单个账户的余额。
该行event Sent(address from, address to, uint amount);
声明了一个“事件”,它在函数send
的最后一行触发 。以太坊客户端(例如 Web 应用程序)可以在无需太多成本的情况下侦听在区块链上发出的这些事件。一旦它发出,侦听器就会收到from
, to
和 amount
参数,这使得跟踪事务成为可能。
要监听此事件,您可以使用以下 JavaScript 代码,该代码使用web3.js创建Coin合约对象,并且任何用户界面都会调用balances
上面自动生成的函数:
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})
构造函数是一个特殊的函数,在合约创建过程中执行,之后不能调用。在这种情况下,它会永久存储创建合约的人的地址。msg
变量(连同tx
和block
)是一个 特殊的全局变量,包含允许访问区块链的属性。msg.sender
始终是当前(外部)函数调用的来源地址。
构成合约以及用户和合约可以调用的函数是mint
和send
。
该mint函数将一定数量的新创建的硬币发送到另一个地址。require函数调用定义了在未满足时还原所有更改的条件。在这个例子中,require(msg.sender == minter);
确保只有合约的创建者可以调用mint
. 一般来说,创建者可以铸造任意数量的代币,但在某些时候,这会导致一种称为“溢出”的现象。请注意,由于默认的Checked 算法,如果表达式 balances[receiver] += amount;
溢出,即任意精度算术中的balances[receiver] + amount
大于uint
(2**256 - 1
)的最大值时,事务将恢复。函数send中的语句balances[receiver] += amount;
也是如此。
错误允许您向调用者提供有关条件或操作失败原因的更多信息。错误与revert 语句一起使用 。revert
语句无条件中止并恢复与require
函数类似的所有更改,但它还允许您提供错误的名称和将提供给调用者(并最终提供给前端应用程序或块资源管理器)的附加数据,以便故障可以更容易地被调试或响应。
任何人(已经拥有其中一些硬币)都可以使用send
功能将硬币发送给其他任何人。如果发件人没有足够的硬币发送,则if条件评估为真。因此,revert
操作将导致操作失败,同时使用InsufficientBalance
错误向发送者提供错误详细信息。
笔记
如果您使用此合约将硬币发送到某个地址,当您在区块链浏览器上查看该地址时,您将看不到任何内容,因为您发送硬币的记录和更改的余额仅存储在该特定硬币的数据存储中合同。通过使用事件,您可以创建一个“区块链浏览器”来跟踪新硬币的交易和余额,但您必须检查硬币合约地址而不是硬币所有者的地址。