Back to Blog

Move 개발의 보안 실천 (2): Aptos Coin

Code Auditing
November 21, 2022
16 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 모듈에서 얻은 일부 권한(capability)을 저장하는 데 사용됩니다. 이 권한들은 특정 특수 작업에 대한 허가에 해당하며, 이후에 설명하겠습니다.
  • 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,
    /// 코인의 심볼, 일반적으로 이름의 축약형.
    /// 예를 들어, 싱가포르 달러는 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 함수처럼). 다행히도, BSC와 함께 aptos_std::type_info를 통해 이 구조체가 정의된 모듈의 주소를 얻을 수 있습니다. 모듈이 관리자 주소 아래에 배포되었으므로, 이를 통해 관리자 주소를 얻고 최종적으로 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 모듈에서 제공하는 진입 함수입니다. 로직은 두 가지 공개 함수, 즉 withdrawdeposit의 호출로 구성됩니다. 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 구조체의 값은 0이 됩니다(일명 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.yamlaccount 참조). 브라우저에 주소를 입력하고 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::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

Sign up for the latest updates
~$410만 손실: Taiko, SecondFi 익스플로잇 | BlockSec 위클리
Security Insights

~$410만 손실: Taiko, SecondFi 익스플로잇 | BlockSec 위클리

이 주간 블록체인 보안 리포트는 2026년 6월 22~28일 발생한 주요 사건 2건을 다루며, 이더리움과 카르다노에서 약 410만 달러의 피해가 확인됐습니다. Taiko 브릿지 공격은 노출된 SGX 서명 키와 디버그 엔클레이브를 거부하지 못한 증명 정책 결함을 이용해 악성 증명자를 등록하고 L2 상태 증명을 위조했습니다. SecondFi 지갑은 Ed25519 논스 도출 시 비밀 입력이 제거되는 결함으로 공개 트랜잭션 데이터만으로 개인 키 복구가 가능했습니다.

~$18M 손실: jaredFromSubway, Aztec 등 | BlockSec 위클리
Security Insights

~$18M 손실: jaredFromSubway, Aztec 등 | BlockSec 위클리

이 주간 블록체인 보안 보고서는 2026년 6월 15일~21일을 다루며, 이더리움과 BNB 체인에서 3건의 주요 사고가 발생해 약 $18.3M의 손실이 발생했습니다. jaredFromSubway 사건은 MEV 봇이 차익거래를 위해 신뢰할 수 없는 제3자 컨트랙트에 자산을 승인한 역방향 승인 공격으로, 가짜 래퍼 토큰과 스왑 풀을 이용해 약 $15M 손실이 발생했습니다. Aztec은 이스케이프 해치 ZK 회로의 제약 누락으로 공격자가 가짜 머클 트리로 온체인 검증을 통과했습니다.

Web3 컴패니언: 오픈소스 보안 에이전틱 지갑

Web3 컴패니언: 오픈소스 보안 에이전틱 지갑

BlockSec가 Web3 Companion을 오픈소스로 공개했습니다. 이 보안 중심의 에이전트 지갑은 자체 AI 에이전트를 신뢰하지 않는 방식으로 설계되었으며, 키 격리, 강력한 정책, Passkey를 활용해 온체인 자산을 보호합니다.

Best Security Auditor for Web3

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

BlockSec Audit