在上一篇文章中,我们简要介绍了如何在Aptos网络上开发一个Hello World程序。从现在开始,我们将更深入地探讨DeFi应用的开发及其安全问题。一如既往,我们希望从一些基础但重要的概念开始。本文将重点关注Aptos Coin(即Aptos中的同质化代币[1]),包括其开发、管理和交互。
摘要
本文将告诉你:
- 什么是Aptos Coin?
- 如何创建和管理你的Coin?
- 如何与你的Coin进行交互?
0x1. 关于Aptos Coin
作为DeFi的原子,代币(或硬币)已广泛应用于区块链生态系统中。它们可以用来表示各种事物,包括电子货币、质押份额以及组织管理的投票权。在某种程度上,DeFi的日常活动可以被简单地看作是跨区块链系统的巨量代币流动。
以太坊为代币开发了一套标准。其中最知名的是ERC20,它规定了标准ERC20代币需要遵守的接口。ERC20是同质化代币标准,同时也有非同质化代币标准,例如ERC721。
与其他区块链系统类似,Aptos也有其代币标准[2],该标准定义了如何在各自的区块链上创建和使用数字资产。具体来说,在Aptos中,同质化代币称为Coin,而非同质化代币(即NFT)称为Token。接下来,我们将讨论创建、管理和交互Aptos Coin的方式。
0x2. 创建和管理你的第一个Coin
Aptos提供了一个官方标准模块(类似于ERC20):coin.move。通过调用该模块的API,任何用户都可以轻松创建自己的Coin。此外,coin.move还提供了权限机制来管理Coin,这对于构建复杂的DeFi应用来说很重要且有用。接下来,我们将演示如何基于此模块创建Coin。
如上一篇文章所述,你可以通过输入以下命令来创建一个项目:
aptos move init --name my_coin
然后,你需要在sources文件夹下创建一个新的Move文件。现在,让我们用以下示例代码填充它,该代码定义了一个名为bsc的模块,用于创建和管理一个名为BSC的标准Coin。
module BlockSec::bsc{
use aptos_framework::coin;
use aptos_framework::event;
use aptos_framework::account;
use aptos_std::type_info;
use std::string::{utf8, String};
use std::signer;
struct BSC{}
struct CapStore has key{
mint_cap: coin::MintCapability<BSC>,
freeze_cap: coin::FreezeCapability<BSC>,
burn_cap: coin::BurnCapability<BSC>
}
struct BSCEventStore has key{
event_handle: event::EventHandle<String>,
}
fun init_module(account: &signer){
let (burn_cap, freeze_cap, mint_cap) = coin::initialize<BSC>(account, utf8(b"BSC"), utf8(b"BSC"), 6, true);
move_to(account, CapStore{mint_cap: mint_cap, freeze_cap: freeze_cap, burn_cap: burn_cap});
}
public entry fun register(account: &signer){
let address_ = signer::address_of(account);
if(!coin::is_account_registered<BSC>(address_)){
coin::register<BSC>(account);
};
if(!exists<BSCEventStore>(address_)){
move_to(account, BSCEventStore{event_handle: account::new_event_handle(account)});
};
}
fun emit_event(account: address, msg: String) acquires BSCEventStore{
event::emit_event<String>(&mut borrow_global_mut<BSCEventStore>(account).event_handle, msg);
}
public entry fun mint_coin(cap_owner: &signer, to_address: address, amount: u64) acquires CapStore, BSCEventStore{
let mint_cap = &borrow_global<CapStore>(signer::address_of(cap_owner)).mint_cap;
let mint_coin = coin::mint<BSC>(amount, mint_cap);
coin::deposit<BSC>(to_address, mint_coin);
emit_event(to_address, utf8(b"minted BSC"));
}
public entry fun burn_coin(account: &signer, amount: u64) acquires CapStore, BSCEventStore{
let owner_address = type_info::account_address(&type_info::type_of<BSC>());
let burn_cap = &borrow_global<CapStore>(owner_address).burn_cap;
let burn_coin = coin::withdraw<BSC>(account, amount);
coin::burn<BSC>(burn_coin, burn_cap);
emit_event(signer::address_of(account), utf8(b"burned BSC"));
}
public entry fun freeze_self(account: &signer) acquires CapStore, BSCEventStore{
let owner_address = type_info::account_address(&type_info::type_of<BSC>());
let freeze_cap = &borrow_global<CapStore>(owner_address).freeze_cap;
let freeze_address = signer::address_of(account);
coin::freeze_coin_store<BSC>(freeze_address, freeze_cap);
emit_event(freeze_address, utf8(b"freezed self"));
}
public entry fun emergency_freeze(cap_owner: &signer, freeze_address: address) acquires CapStore, BSCEventStore{
let owner_address = signer::address_of(cap_owner);
let freeze_cap = &borrow_global<CapStore>(owner_address).freeze_cap;
coin::freeze_coin_store<BSC>(freeze_address, freeze_cap);
emit_event(freeze_address, utf8(b"emergency freezed"));
}
public entry fun unfreeze(cap_owner: &signer, unfreeze_address: address) acquires CapStore, BSCEventStore{
let owner_address = signer::address_of(cap_owner);
let freeze_cap = &borrow_global<CapStore>(owner_address).freeze_cap;
coin::unfreeze_coin_store<BSC>(unfreeze_address, freeze_cap);
emit_event(unfreeze_address, utf8(b"unfreezed"));
}
}
0x2.1 基本设计
首先,我们来看结构部分。总共定义了三个结构。
BSC结构,用作Coin的唯一标识符。因此,可以通过BlockSec::bsc::BSC路径唯一确定此Coin。CapStore结构,用于存储从aptos_framework::coin模块获取的一些能力。这些能力对应于一些特殊操作的权限,稍后将进行解释。BSCEventStore结构,用于记录用户事件。
在这些结构之后,有一个init_module函数,用于初始化模块,并且在模块发布到链上时只会被调用一次。在此函数中,模块调用coin::initialize<BSC>将BlockSec::bsc::BSC注册为新Coin的唯一标识符。
public fun initialize<CoinType>(
account: &signer,
name: string::String,
symbol: string::String,
decimals: u8,
monitor_supply: bool,
): (BurnCapability<CoinType>, FreezeCapability<CoinType>, MintCapability<CoinType>) {
initialize_internal(account, name, symbol, decimals, monitor_supply, false)
}
/// Capability required to mint coins.
struct MintCapability<phantom CoinType> has copy, store {}
/// Capability required to freeze a coin store.
struct FreezeCapability<phantom CoinType> has copy, store {}
/// Capability required to burn coins.
struct BurnCapability<phantom CoinType> has copy, store {}
在注册之后,aptos_framework::coin模块中所有带有BlockSec::bsc::BSC类型的泛型函数都将操作此Coin。此注册过程将返回三种能力,即MintCapability、FreezeCapability和BurnCapability。这些能力分别用于铸造Coin、冻结用户账户和销毁Coin。在某种程度上,这种能力结构的功能类似于一把钥匙,用于打开特定权限的锁。如果有人拥有钥匙,他就可以获得相应的权限。在这里,我们将这些能力存储在CapStore结构中(由该模块的管理员/发布者拥有)以供将来使用。
同时,在注册过程中,会在管理员地址下存储一个CoinInfo结构,以记录相关信息:
/// Information about a specific coin type. Stored on the creator of the coin's account.
struct CoinInfo<phantom CoinType> has key {
name: string::String,
/// Symbol of the coin, usually a shorter version of the name.
/// For example, Singapore Dollar is SGD.
symbol: string::String,
/// Number of decimals used to get its user representation.
/// For example, if `decimals` equals `2`, a balance of `505` coins should
/// be displayed to a user as `5.05` (`505 / 10 ** 2`).
decimals: u8,
/// Amount of this coin type in existence.
supply: Option<OptionalAggregator>,
}
调用init_module函数后,你的Coin就已在链上注册。然而,由于目前没有任何流通,没有人可以使用这个Coin。为了使Coin可用,需要支持一些操作,包括发行、分配和销毁。这些操作需要我们在注册Coin时获得的能力。
0x2.2 Coin管理
该Coin设计为遵循以下规则:
- 只有管理员(admin)可以铸造Coin。
- 用户可以随时销毁自己的Coin。
- 用户可以随时冻结/解冻自己的账户。
因此,我们定义了五个管理函数,即mint_coin、burn_coin、freeze_self、emergency_freeze和unfreeze。前两个函数分别负责铸造Coin和销毁Coin;而后三个函数则用于冻结和解冻账户。
铸造Coin
在我们的模块中,mint_coin函数用于铸造Coin。因为只有管理员可以铸造Coin,我们必须在此函数中验证相应的能力。
public entry fun mint_coin(cap_owner: &signer, to_address: address, amount: u64) acquires CapStore, BSCEventStore{
let mint_cap = &borrow_global<CapStore>(signer::address_of(cap_owner)).mint_cap;
let mint_coin = coin::mint<BSC>(amount, mint_cap);
coin::deposit<BSC>(to_address, mint_coin);
emit_event(to_address, utf8(b"minted BSC"));
}
此函数需要三个参数:
cap_owner是&signer类型,即交易的发起者。to_address指示铸造的Coin将存入的地址。amount指示铸造的Coin数量。
它包括三个步骤:获取铸造Coin的能力,铸造Coin,以及存入Coin。
首先,在mint_coin函数的开头,可以通过signer::address_of(cap_owner)获取交易发起者的账户地址。之后,使用borrow_global<CapStore>来确认账户是否拥有CapStore,以验证它是否是模块的管理员。通过这样做,我们可以保证只有管理员可以铸造Coin,而其他用户将在这一步失败。
其次,mint_coin函数将调用aptos_framework::coin模块的mint函数来铸造Coin。
public fun mint<CoinType>(
amount: u64,
_cap: &MintCapability<CoinType>,
): Coin<CoinType> acquires CoinInfo {
if (amount == 0) {
return zero<CoinType>()
};
let maybe_supply = &mut borrow_global_mut<CoinInfo<CoinType>>(coin_address<CoinType>()).supply;
if (option::is_some(maybe_supply)) {
let supply = option::borrow_mut(maybe_supply);
optional_aggregator::add(supply, (amount as u128));
};
Coin<CoinType> { value: amount }
}
这里需要MintCapability。具体来说,需要将一个名为_cap的参数作为MintCapability的引用传递。然后,与之关联的权限将相应地转移。虽然没有明确的访问控制,但验证是由Move语言强制执行的。
第三,mint_coin函数将调用deposit函数将铸造的Coin存入指定的to_address。
技巧#1:可以使用类似的方法来验证特权账户的访问控制。
销毁Coin
销毁Coin的过程与铸造Coin不同。具体来说,只有管理员可以调用mint_coin函数,而任何用户都可以调用burn_coin函数。为此,burn_coin函数必须暂时提升权限,即为这些用户获取BurnCapability能力。
public entry fun burn_coin(account: &signer, amount: u64) acquires CapStore, BSCEventStore{
let owner_address = type_info::account_address(&type_info::type_of<BSC>());
let burn_cap = &borrow_global<CapStore>(owner_address).burn_cap;
let burn_coin = coin::withdraw<BSC>(account, amount);
coin::burn<BSC>(burn_coin, burn_cap);
emit_event(signer::address_of(account), utf8(b"burned BSC"));
}
此函数需要两个参数:
account是&signer类型,即交易的发起者。amount指示要销毁的Coin数量。
它也包括三个步骤:获取销毁Coin的能力,提取Coin,以及销毁Coin。
显然,aptos_framework::coin模块的burn函数要求调用者传递BurnCapability的引用,但此能力存储在管理员的CapStore中。因此,我们必须允许普通用户获取此能力来销毁他们持有的Coin。
public fun burn<CoinType>(
coin: Coin<CoinType>,
_cap: &BurnCapability<CoinType>,
) acquires CoinInfo {
let Coin { value: amount } = coin;
assert!(amount > 0, error::invalid_argument(EZERO_COIN_AMOUNT));
let maybe_supply = &mut borrow_global_mut<CoinInfo<CoinType>>(coin_address<CoinType>()).supply;
if (option::is_some(maybe_supply)) {
let supply = option::borrow_mut(maybe_supply);
optional_aggregator::sub(supply, (amount as u128));
}
}
为了实现这一目标,我们可以使用Move语言提供的borrow_global运算符。它用于从账户的不可变全局存储中读取特定数据类型。通过使用此运算符,模块可以**借出**管理员拥有的能力给其他用户。即,需要管理员地址来获取我们想要的能力。
然而,burn_coin函数的交易发起者是用户而不是管理员。因此,无法像mint_coin函数那样通过signer获取管理员地址。幸运的是,可以通过aptos_std::type_info并使用BSC来获取定义此结构的模块的地址。由于该模块是在管理员地址下发布的,我们可以进一步相应地获取管理员地址,并最终获得BurnCapability能力。
技巧#2:
_borrow_global_运算符可用于临时获取模块的能力。
在获得BurnCapability后,模块可以从用户那里提取指定数量的Coin,并使用该能力销毁Coin。
冻结和解冻Coin账户
基于以上讨论,现在我们可以轻松地完成Coin账户的管理。具体来说,我们提供了freeze_self函数供用户冻结其Coin账户。这里我们也提供了emergency_freeze函数用于紧急冻结,只能由管理员使用。此外,由于存在紧急冻结机制,不应允许用户自行解冻。因此,unfreeze函数也需要管理员来解冻用户账户。
public entry fun freeze_self(account: &signer) acquires CapStore, BSCEventStore{
let owner_address = type_info::account_address(&type_info::type_of<BSC>());
let freeze_cap = &borrow_global<CapStore>(owner_address).freeze_cap;
let freeze_address = signer::address_of(account);
coin::freeze_coin_store<BSC>(freeze_address, freeze_cap);
emit_event(freeze_address, utf8(b"freezed self"));
}
public entry fun emergency_freeze(cap_owner: &signer, freeze_address: address) acquires CapStore, BSCEventStore{
let owner_address = signer::address_of(cap_owner);
let freeze_cap = &borrow_global<CapStore>(owner_address).freeze_cap;
coin::freeze_coin_store<BSC>(freeze_address, freeze_cap);
emit_event(freeze_address, utf8(b"emergency freezed"));
}
public entry fun unfreeze(cap_owner: &signer, unfreeze_address: address) acquires CapStore, BSCEventStore{
let owner_address = signer::address_of(cap_owner);
let freeze_cap = &borrow_global<CapStore>(owner_address).freeze_cap;
coin::unfreeze_coin_store<BSC>(unfreeze_address, freeze_cap);
emit_event(unfreeze_address, utf8(b"unfreezed"));
}
技巧#3:通过函数返回能力时要小心!获取这些能力的恶意用户可能会滥用它们对Coin持有者造成损害!
0x3. 与Coin交互
这里我们专注于与Coin交互的方式。
0x3.1 注册
你可能已经注意到模块中存在一个register函数:
public entry fun register(account: &signer){
let address_ = signer::address_of(account);
if(!coin::is_account_registered<BSC>(address_)){
coin::register<BSC>(account);
};
if(!exists<BSCEventStore>(address_)){
move_to(account, BSCEventStore{event_handle: account::new_event_handle(account)});
};
}
此函数用于帮助用户注册Coin使用权和事件记录器。为了使用某个Coin,aptos_framework::coin模块规定用户必须首先通过aptos_framework::coin::register函数显式注册使用Coin的权利。
public fun register<CoinType>(account: &signer) {
let account_addr = signer::address_of(account);
assert!(
!is_account_registered<CoinType>(account_addr),
error::already_exists(ECOIN_STORE_ALREADY_PUBLISHED),
);
account::register_coin<CoinType>(account_addr);
let coin_store = CoinStore<CoinType> {
coin: Coin { value: 0 },
frozen: false,
deposit_events: account::new_event_handle<DepositEvent>(account),
withdraw_events: account::new_event_handle<WithdrawEvent>(account),
};
move_to(account, coin_store);
}
用户只有通过此函数注册了该类型的Coin,才能正常持有该Coin。也就是说,如果你不希望持有某个Coin,其他人就不能在未经你同意的情况下将该Coin存入你的账户。注册实际上是将一个CoinStore结构(目标Coin类型)放入你的账户。这个CoinStore结构包含一个Coin结构来记录你的余额。
技巧#4:与以太坊代币不同,Aptos Coin在用户显式注册之前无法持有和操作。
0x3.2 转账
假设你现在拥有一些BSC Coin,那么你可以通过调用aptos_framework::coin模块的transfer函数来转账这些Coin。
public entry fun transfer<CoinType>(
from: &signer,
to: address,
amount: u64,
) acquires CoinStore {
let coin = withdraw<CoinType>(from, amount);
deposit(to, coin);
}
请注意,这是coin模块提供的一个入口函数。其逻辑包括调用两个公共函数,即withdraw和deposit。withdraw函数需要&signer权限,用于从你的账户提取一定数量的资产到Coin中。deposit函数可以向任何已注册的账户存入Coin。此函数不需要额外的权限,并将指定的Coin存入账户地址。最后,转账的Coin将自动与存储在目标地址的CoinStore结构中的Coin合并。
技巧#5:提取后,Coin中的资产由当前的
_transfer_函数控制。此函数可以在不获取额外权限的情况下将这些资产传递给_deposit_函数。
0x3.3 分割和合并
与以太坊代币不同,Coin的流通不能通过修改用户余额来更新。相反,它可以通过在coin模块中提取Coin结构来实现。通过这样做,用户通过将此结构传递给其他模块来实现资产流通。由于一个结构只能由定义它的模块操作,coin模块提供了一些接口来操作Coin结构,包括将Coin分割成更小的单位以及合并多个Coin以满足不同场景的需求。
extract函数用于分割Coin。它接收一个Coin结构,从中提取一部分资产以生成一个新的Coin结构,并返回新结构。
public fun extract<CoinType>(coin: &mut Coin<CoinType>, amount: u64): Coin<CoinType> {
assert!(coin.value >= amount, error::invalid_argument(EINSUFFICIENT_BALANCE));
coin.value = coin.value - amount;
Coin { value: amount }
}
extract_all函数用于提取原始Coin结构的全部价值并将其存入一个新的Coin结构。结果是原始Coin结构的价值将变为零(即zero_coin)。zero_coin结构可以通过调用destroy_zero函数来销毁。
public fun extract_all<CoinType>(coin: &mut Coin<CoinType>): Coin<CoinType> {
let total_value = coin.value;
coin.value = 0;
Coin { value: total_value }
}
public fun destroy_zero<CoinType>(zero_coin: Coin<CoinType>) {
let Coin { value } = zero_coin;
assert!(value == 0, error::invalid_argument(EDESTRUCTION_OF_NONZERO_TOKEN))
}
merge函数用于合并Coin。你可以将两个Coin结构(source_coin和dst_coin)的价值合并到dst_coin结构中,并销毁source_coin结构。
public fun merge<CoinType>(dst_coin: &mut Coin<CoinType>, source_coin: Coin<CoinType>) {
spec {
assume dst_coin.value + source_coin.value <= MAX_U64;
};
dst_coin.value = dst_coin.value + source_coin.value;
let Coin { value: _ } = source_coin;
}
zero函数用于生成一个zero_coin结构。
public fun zero<CoinType>(): Coin<CoinType> {
Coin<CoinType> {
value: 0
}
}
0x4. 测试Coin
为了快速测试此Coin,你可以先使用以下命令将其发布(别忘了在Move.toml中设置你的发布者账户地址!)
$ aptos move publish --package-dir ./
Compiling, may take a little while to download git dependencies...
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_coin
package size 2751 bytes
Do you want to submit a transaction for a range of [868300 - 1302400] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
{
"Result": {
"transaction_hash": "xxx",
...
这样,Coin就成功发布到链上了,但它没有任何流通。你需要注册你的账户才能接收铸造的Coin。
$ aptos move run --function-id default::bsc::register
Do you want to submit a transaction for a range of [153100 - 229600] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
{
"Result": {
"transaction_hash": "xxx",
...
请注意,第二个命令中的**Your-address需要替换为你自己的地址(请参阅.aptos/config.yaml中的account)。在浏览器中输入你的地址,然后点击Resources**选项卡,你就可以看到该账户现在拥有100个BSC Coin。


如果你想将Coin转账给另一个账户,请不要忘记让该账户注册以使用该Coin。因为transfer函数是一个泛型函数,你需要将泛型参数指定为BSC,以及to_address,如下所示:
$ aptos move run — function-id 0x1::coin::transfer — type-args BSC-module-address::bsc::BSC — args address:To_address u64:1
此命令将调用coin模块的transfer函数,将1个BSC Coin转账给**To_address。这里0x1::coin::transfer是transfer函数的函数ID。请记住,BlockSec::bsc::BSC是你Coin的标识符,必须为其指定泛型参数。此外,BSC-module-address应替换为模块发布者账户地址,该地址在Move.toml中指定为BlockSec**。
0x5. 下一步
在了解了如何创建、管理和交互自己的Coin之后,我们将演示如何构建第一个DeFi基石项目:自动做市商(AMM)。还将涵盖更多关于Move开发和安全实践的有趣话题。敬请关注!
参考
[1] https://aptos.dev/concepts/coin-and-token/aptos-coin/ [2] https://aptos.dev/concepts/coin-and-token/index [3] https://aptos.dev/concepts/coin-and-token/aptos-token



