Back to Blog

移动开发中的安全实践(二):Aptos Coin

Code Auditing
November 21, 2022
18 min read

在上一篇文章中,我们简要介绍了如何在 Aptos 网络上开发 Hello World 程序。从现在开始,我们将更深入地探讨 DeFi 应用程序的开发及其安全问题。一如既往,我们想从一些基本但重要的概念开始。在本文中,我们将重点关注 Aptos 币(即 Aptos 中的同质化代币 [1]),包括其开发、管理和交互。

摘要

本文将告诉您:

  • 什么是 Aptos 币?
  • 如何创建和管理您的币?
  • 如何与您的币进行交互?

0x1. 关于 Aptos 币

作为 DeFi 的基石,代币(或币)已被广泛应用于区块链生态系统中。它们可以用来代表各种事物,包括电子货币、质押份额以及组织管理的投票权。在某种程度上,DeFi 的日常活动可以简单地看作是区块链系统之间大量的代币流动。

以太坊为代币制定了一套标准。其中最著名的是 ERC20,它规定了标准 ERC20 代币需要遵守的接口。ERC20 是一个同质化代币标准,同时也有非同质化代币标准,如 ERC721

与其他区块链系统类似,Aptos 也有其代币标准 [2],它定义了数字资产在其各自区块链上的创建和使用方式。具体来说,在 Aptos 中,同质化代币称为 币 (coin),而非同质化代币(即 NFT)则称为 代币 (token)。下面,我们将讨论创建、管理和交互 Aptos 币的方法。

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)
}

/// 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 类型的泛型函数都将操作此币。此注册过程将返回三种能力,即 MintCapabilityFreezeCapabilityBurnCapability。这些能力分别用于铸造币、冻结用户账户和销毁币。在某种程度上,这种能力结构的函数功能类似于密钥,用于打开特定权限的锁。如果有人拥有密钥,他就可以获得相应的权限。在这里,我们将这些能力存储在 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 函数后,您的币已在链上注册。但是,由于目前还没有流通,没有人可以使用此币。要使币可用,需要支持一些操作,包括发行、分配和销毁。这些操作需要我们在注册币时获得的能力。

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 函数的交易发起者是用户而不是管理员。因此,无法像 mint_coin 函数那样通过 signer 获取管理员地址。幸运的是,可以通过 aptos_std::type_infoBSC 来获取模块的地址,在该地址定义了这个结构。由于该模块是在管理员地址下发布的,我们可以进一步获取管理员地址,并最终获得 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 币不能在用户未显式注册的情况下持有和操作。

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 模块提供的入口函数。其逻辑包括调用两个公共函数,即 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)。可以通过调用 destroy_zero 函数销毁 zero_coin 结构。

    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

此命令将调用 coin 模块的 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