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

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

在上一篇文章中,我们简要介绍了如何在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。此注册过程将返回三种能力,即MintCapabilityFreezeCapabilityBurnCapability。这些能力分别用于铸造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_coinburn_coinfreeze_selfemergency_freezeunfreeze。前两个函数分别负责铸造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模块提供的一个入口函数。其逻辑包括调用两个公共函数,即withdrawdepositwithdraw函数需要&signer权限,用于从你的账户提取一定数量的资产到Coin中。deposit函数可以向任何已注册的账户存入Coin。此函数不需要额外的权限,并将指定的Coin存入账户地址。最后,转账的Coin将自动与存储在目标地址的CoinStore结构中的Coin合并。

技巧#5:提取后,Coin中的资产由当前的 _transfer_ 函数控制。此函数可以在不获取额外权限的情况下将这些资产传递给 _deposit_ 函数。

0x3.3 分割和合并

与以太坊代币不同,Coin的流通不能通过修改用户余额来更新。相反,它可以通过在coin模块中提取Coin结构来实现。通过这样做,用户通过将此结构传递给其他模块来实现资产流通。由于一个结构只能由定义它的模块操作,coin模块提供了一些接口来操作Coin结构,包括将Coin分割成更小的单位以及合并多个Coin以满足不同场景的需求。

  1. 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 }
}
  1. 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))
}
  1. merge函数用于合并Coin。你可以将两个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;
}
  1. 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::transfertransfer函数的函数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

Sign up for the latest updates
Weekly Web3 Security Incident Roundup | Feb 9 – Feb 15, 2026

Weekly Web3 Security Incident Roundup | Feb 9 – Feb 15, 2026

During the week of February 9 to February 15, 2026, three blockchain security incidents were reported with total losses of ~$657K. All incidents occurred on the BNB Smart Chain and involved flawed business logic in DeFi token contracts. The primary causes included an unchecked balance withdrawal from an intermediary contract that allowed donation-based inflation of a liquidity addition targeted by a sandwich attack, a post-swap deflationary clawback that returned sold tokens to the caller while draining pool reserves to create a repeatable price-manipulation primitive, and a token transfer override that burned tokens directly from a Uniswap V2 pair's balance and force-synced reserves within the same transaction to artificially inflate the token price.

Top 10 "Awesome" Security Incidents in 2025

Top 10 "Awesome" Security Incidents in 2025

To help the community learn from what happened, BlockSec selected ten incidents that stood out most this year. These cases were chosen not only for the scale of loss, but also for the distinct techniques involved, the unexpected twists in execution, and the new or underexplored attack surfaces they revealed.

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

On August 29, 2025, Panoptic disclosed a Cantina bounty finding and confirmed that, with support from Cantina and Seal911, it executed a rescue operation on August 25 to secure roughly $400K in funds. The issue stemmed from a flaw in Panoptic’s position fingerprint calculation algorithm, which could have enabled incorrect position identification and downstream fund risk.