在上一篇文章中,我們簡要介紹了如何在 Aptos 網路上開發 Hello World 程式。從現在開始,我們將更深入地探討 DeFi 應用程式的開發及其安全性考量。一如既往,我們想先從一些基礎但重要的概念開始。在本文中,我們將專注於 Aptos coin(即 Aptos 中的同質化代幣 [1]),包括其開發、管理以及交互方式。
TL;DR
本文將告訴您:
- 什麼是 Aptos Coin?
- 如何創建和管理您的代幣?
- 如何與您的代幣進行交互?
0x1. 關於 Aptos Coin
作為 DeFi 的原子,代幣(或幣)已在區塊鏈生態系統中被廣泛使用。它們可用於代表許多事物,包括電子貨幣、抵押股份以及用於組織管理的投票權。在某種程度上,DeFi 的日常活動可以簡單地視為區塊鏈系統中海量的代幣流動。
以太坊已經為代幣開發了一套標準。最著名的一個稱為 ERC20,它指定了標準 ERC20 代幣需要遵守的介面。ERC20 是一種同質化代幣標準,同時也存在非同質化代幣標準,例如 ERC721。
與其他區塊鏈系統類似,Aptos 也有其代幣標準 [2],該標準定義了數位資產如何在各自的區塊鏈上創建和使用。具體來說,在 Aptos 中,同質化代幣稱為 coin,而非同質化代幣(即 NFT)稱為 token。接下來,我們將討論創建、管理 Aptos coin 並與其交互的方法。
0x2. 創建並管理您的第一個代幣
Aptos 提供了一個官方標準模組(類似於 ERC20):coin.move。透過呼叫該模組的 API,任何使用者都可以輕鬆創建自己的代幣。此外,coin.move 還提供了管理代幣的權限機制,這對於構建複雜的 DeFi 應用程式來說既重要又有用。在下面,我們將示範如何基於此模組創建代幣。
如上一篇文章所述,您可以輸入以下指令來創建一個專案:
aptos move init --name my_coin
然後,您需要在 ``sources’’ 資料夾下創建一個新的 Move 檔案。現在,讓我們用以下範例程式碼填充它,該程式碼定義了一個名為 bsc 的模組,用於創建和管理名為 BSC 的標準代幣。
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結構,用作代幣的唯一識別碼。因此,該代幣可以透過BlockSec::bsc::BSC路徑進行唯一確定。CapStore結構,用於儲存從aptos_framework::coin模組中獲得的一些能力。這些能力對應於某些特殊操作的權限,稍後會進行解釋。BSCEventStore結構,用於記錄使用者事件。
在這些結構之後,有一個 init_module 函數,用於初始化模組,並且僅在模組發佈到鏈上時被呼叫一次。在此函數中,模組呼叫 coin::initialize<BSC> 來註冊 BlockSec::bsc::BSC 作為新代幣的唯一識別碼。
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)
}
/// 鑄造代幣所需的能力。
struct MintCapability<phantom CoinType> has copy, store {}
/// 凍結代幣儲存所需的能力。
struct FreezeCapability<phantom CoinType> has copy, store {}
/// 銷毀代幣所需的能力。
struct BurnCapability<phantom CoinType> has copy, store {}
註冊後,aptos_framework::coin 中所有採用 BlockSec::bsc::BSC 類型的泛型函數都將作用於此代幣。此註冊過程將返回三種能力,即 MintCapability(鑄造能力)、FreezeCapability(凍結能力)和 BurnCapability(銷毀能力)。這些能力分別是鑄造代幣、凍結使用者帳戶和銷毀代幣所必需的。在某種程度上,這種能力結構的功能類似於鑰匙,用於打開特定權限的鎖。如果有人擁有這把鑰匙,他就能獲得相應的權限。在這裡,我們將這些能力儲存在 CapStore 結構中(由該模組的管理員/發佈者擁有),以便稍後使用。
同時,在註冊過程中,一個 CoinInfo 結構將被儲存在管理員帳戶下,用於記錄相關資訊:
/// 關於特定代幣類型的資訊。儲存在代幣創建者的帳戶中。
struct CoinInfo<phantom CoinType> has key {
name: string::String,
/// 代幣的符號,通常是名稱的簡稱。
/// 例如,Singapore Dollar 是 SGD。
symbol: string::String,
/// 用於獲得使用者表示的小數位數。
/// 例如,如果 `decimals` 等於 `2`,505 個代幣的餘額應該
/// 顯示為 `5.05` (`505 / 10 ** 2`) 給使用者。
decimals: u8,
/// 此種代幣類型的現有總量。
supply: Option<OptionalAggregator>,
}
在呼叫 init_module 函數後,您的代幣已經在鏈上註冊。然而,目前還沒有人可以使用該代幣,因為它暫時還沒有流通。為了使代幣可用,需要支援包括發行、分配和銷毀在內的一些操作。這些操作需要我們在註冊代幣時獲得的能力。
0x2.2 代幣管理
該代幣設計遵循以下規則:
- 只有管理員 (admin) 可以鑄造代幣。
- 使用者可以隨時銷毀自己的代幣。
- 使用者可以隨時凍結/解凍自己的帳戶。
因此,我們定義了五個管理函數,即 mint_coin、burn_coin、freeze_self、emergency_freeze 和 unfreeze。前兩個函數分別負責鑄造代幣和銷毀代幣;後三個函數用於凍結和解凍帳戶。
鑄造代幣
在我們的模組中,mint_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指示將存入鑄造代幣的地址。amount指示正在鑄造的代幣數量。
它包含三個步驟:獲取鑄造代幣的能力、鑄造代幣,以及存入代幣。
首先,在 mint_coin 函數開始時,可以透過 signer::address_of(cap_owner) 獲取交易發起者的帳戶地址。之後,使用 borrow_global<CapStore> 來確該帳戶是否擁有 CapStore,以驗證其是否為模組的管理員。透過這種方式,我們可以保證只有管理員可以鑄造代幣,而其他使用者將在此步驟失敗。
其次,mint_coin 函數隨後將呼叫 aptos_framework::coin 模組的 mint 函數來鑄造代幣。
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 的引用傳遞。隨後,與 MintCapability 能力相關聯的權限也相應地被轉移。雖然沒有顯式的存取控制,但驗證是由 Move 語言強制執行的。
第三,mint_coin 函數將呼叫 deposit 函數將鑄造的代幣存入指定的 to_address。
提示#1:特權帳戶的存取控制可以使用類似的方法進行驗證。
銷毀代幣
銷毀代幣的過程與鑄造代幣不同。具體來說,只有管理員可以呼叫 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指示要銷毀的代幣數量。
它也包含三個步驟:獲取銷毀代幣的能力、提現代幣,以及銷毀代幣。
顯然,aptos_framework::coin 模組的 burn 函數要求呼叫者傳入 BurnCapability 的引用,但此能力儲存在管理員的 CapStore 中。因此,我們必須允許普通使用者獲取此能力來銷毀他們持有的代幣。
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 函數的交易發起者是使用者,而不是管理員。因此,無法透過 signer(像 mint_coin 函數那樣)獲取管理員地址。幸運的是,它可以透過 aptos_std::type_info 搭配 BSC 獲取定義此結構的模組地址來取得。由於模組是在管理員地址下發佈的,我們可以據此進一步獲取管理員地址,並最終得到 BurnCapability 能力。
提示#2:
borrow_global運算子可用於臨時獲取模組的能力。
在獲取 BurnCapability 後,模組可以從使用者那裡提取指定數量的代幣,並利用該能力銷毀代幣。
凍結和解凍代幣帳戶
基於上述討論,現在我們可以輕鬆處理代幣帳戶的管理。具體來說,我們提供了 freeze_self 函數供使用者凍結自己的代幣帳戶。在這裡,我們還提供了 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:透過函數返回能力時要小心!惡意使用者獲取這些能力後,可能會濫用它們對代幣持有者造成損害!
0x3. 與代幣進行交互
這裡我們專注於與代幣進行交互的方式。
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)});
};
}
此函數用於幫助使用者註冊代幣使用權和事件記錄器。為了使用特定的代幣,aptos_framework::coin 模組規定使用者必須先透過 aptos_framework::coin::register 函數明確註冊使用該代幣的權利。
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);
}
使用者只有在透過此函數註冊該類型的代幣後,才能正常持有該代幣。換句話說,如果您不想持有某種特定代幣,未經您的同意,其他人無法將該代幣存入您的帳戶。註冊實際上將一個 CoinStore 結構(目標代幣類型)放入您的帳戶中。此 CoinStore 結構包含一個 Coin 結構,用於記錄您的餘額。
提示#4:與以太坊代幣不同,若沒有使用者的明確註冊,無法持有並操作 Aptos coin。
0x3.2 轉帳
假設您現在有一些 BSC 代幣,那麼您可以透過呼叫 aptos_framework::coin 模組的 transfer 函數來轉移這些代幣。
public entry fun transfer<CoinType>(
from: &signer,
to: address,
amount: u64,
) acquires CoinStore {
let coin = withdraw<CoinType>(from, amount);
deposit(to, coin);
}
請注意,這是 coin 模組提供的入口(entry)函數。其邏輯包含呼叫兩個公開函數,即 withdraw 和 deposit。withdraw 函數需要 &signer 權限,用於從您的帳戶中提取一定數量的資產並轉化為代幣。deposit 函數可以將代幣存入該代幣的任何已註冊帳戶中。此函數不需要額外的權限,並且會將指定的代幣存入目標帳戶地址。最終,轉移的代幣將自動與存儲在目標地址 CoinStore 結構中的代幣合併。
提示#5:提取後,代幣中的資產由當前的
transfer函數控制。該函數可以在不獲取額外權限的情況下將這些資產交付給deposit函數。
0x3.3 分割與合併
與以太坊代幣不同,代幣的流通不能透過修改使用者的餘額來更新。相反,它透過在 coin 模組中提取 Coin 結構來實現。透過這樣做,使用者透過將此結構傳遞給其他模組來實現資產流通。由於結構只能由定義它的模組操作,coin 模組提供了一些介面來操作 Coin 結構,包括將代幣劃分為更小的單位,以及合併多個代幣以滿足不同場景的需求。
1. extract 函數用於分割代幣。它接收一個 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 }
}
2. 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))
}
3. merge 函數用於合併代幣。您可以將兩個 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;
}
4. zero 函數用於生成 zero_coin 結構。
public fun zero<CoinType>(): Coin<CoinType> {
Coin<CoinType> {
value: 0
}
}
0x4. 測試代幣
要快速測試該代幣,您可以先使用以下指令進行部署(別忘了在 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",
...
這樣,代幣就成功在鏈上發佈了,但它還沒有任何流通。您需要註冊您的帳戶以接收鑄造的代幣。
$ 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 代幣。


如果您想將代幣轉移到另一個帳戶,別忘了讓該帳戶註冊使用該代幣。由於 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
此指令將呼叫代幣模組的 transfer 函數,將 1 BSC 代幣轉移到 To_address。這裡 0x1::coin::transfer 是 transfer 函數的函數 ID。記住 BlockSec::bsc::BSC 是您的代幣識別碼,必須將泛型參數指定為該識別碼。此外,BSC-module-address 應替換為模組發佈者帳戶地址,該地址在 Move.toml 中分配給 BlockSec。
0x5. 下一步
在了解如何創建、管理並與您自己的代幣交互之後,我們將示範如何構建第一個 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



