Back to Blog

Move 開發安全實踐(二):Aptos Coin

Code Auditing
November 21, 2022
41 min read

在上一篇文章中,我們簡要介紹了如何在 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_coinburn_coinfreeze_selfemergency_freezeunfreeze。前兩個函數分別負責鑄造代幣和銷毀代幣;後三個函數用於凍結和解凍帳戶。

鑄造代幣

在我們的模組中,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)函數。其邏輯包含呼叫兩個公開函數,即 withdrawdepositwithdraw 函數需要 &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_coindst_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 函數是一個泛型函數,您需要將泛型參數指定為 BSCto_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::transfertransfer 函數的函數 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

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit