/// This module provides the foundation for typesafe Coins. module aptos_framework::coin { use std::string; use std::error; use std::option::{Self, Option}; use std::signer; use aptos_std::event::{Self, EventHandle}; use aptos_std::type_info; friend aptos_framework::account; friend aptos_framework::aptos_coin; friend aptos_framework::coins; // // Errors. // /// When address of account which is used to initilize a coin `CoinType` /// doesn't match the deployer of module containining `CoinType`. const ECOIN_INFO_ADDRESS_MISMATCH: u64 = 0; /// When `CoinType` is already initilized as a coin. const ECOIN_INFO_ALREADY_PUBLISHED: u64 = 1; /// When `CoinType` hasn't been initialized as a coin. const ECOIN_INFO_NOT_PUBLISHED: u64 = 2; /// When an account already has `CoinStore` registered for `CoinType`. const ECOIN_STORE_ALREADY_PUBLISHED: u64 = 3; /// When an account hasn't registered `CoinStore` for `CoinType`. const ECOIN_STORE_NOT_PUBLISHED: u64 = 4; /// When there's not enough funds to withdraw from an account or from `Coin` resource. const EINSUFFICIENT_BALANCE: u64 = 5; /// When destruction of `Coin` resource contains non-zero value attempted. const EDESTRUCTION_OF_NONZERO_TOKEN: u64 = 6; /// Total supply of the coin overflows. No additional coins can be minted. const ETOTAL_SUPPLY_OVERFLOW: u64 = 7; const EINVALID_COIN_AMOUNT: u64 = 8; const MAX_U128: u128 = 340282366920938463463374607431768211455; /// Core data structures /// Main structure representing a coin/token in an account's custody. struct Coin has store { /// Amount of coin this address has. value: u64, } /// A holder of a specific coin types and associated event handles. /// These are kept in a single resource to ensure locality of data. struct CoinStore has key { coin: Coin, deposit_events: EventHandle, withdraw_events: EventHandle, } /// Information about a specific coin type. Stored on the creator of the coin's account. struct CoinInfo 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: u64, /// Amount of this coin type in existence. supply: Option, } /// Event emitted when some amount of a coin is deposited into an account. struct DepositEvent has drop, store { amount: u64, } /// Event emitted when some amount of a coin is withdrawn from an account. struct WithdrawEvent has drop, store { amount: u64, } /// Capability required to mint coins. struct MintCapability has copy, key, store { } /// Capability required to burn coins. struct BurnCapability has copy, key, store { } // // Getter functions // /// Returns the balance of `owner` for provided `CoinType`. public fun balance(owner: address): u64 acquires CoinStore { assert!( is_account_registered(owner), error::not_found(ECOIN_STORE_NOT_PUBLISHED), ); borrow_global>(owner).coin.value } /// Returns `true` if the type `CoinType` is an initialized coin. public fun is_coin_initialized(): bool { let type_info = type_info::type_of(); let coin_address = type_info::account_address(&type_info); exists>(coin_address) } /// Returns `true` if `account_addr` is registered to receive `CoinType`. public fun is_account_registered(account_addr: address): bool { exists>(account_addr) } /// Returns the name of the coin. public fun name(): string::String acquires CoinInfo { let type_info = type_info::type_of(); let coin_address = type_info::account_address(&type_info); borrow_global>(coin_address).name } /// Returns the symbol of the coin, usually a shorter version of the name. public fun symbol(): string::String acquires CoinInfo { let type_info = type_info::type_of(); let coin_address = type_info::account_address(&type_info); borrow_global>(coin_address).symbol } /// Returns the 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`). public fun decimals(): u64 acquires CoinInfo { let type_info = type_info::type_of(); let coin_address = type_info::account_address(&type_info); borrow_global>(coin_address).decimals } /// Returns the amount of coin in existence. public fun supply(): Option acquires CoinInfo { let type_info = type_info::type_of(); let coin_address = type_info::account_address(&type_info); borrow_global>(coin_address).supply } // Public functions /// Burn `coin` with capability. /// The capability `_cap` should be passed as a reference to `BurnCapability`. public fun burn( coin: Coin, _cap: &BurnCapability, ) acquires CoinInfo { let Coin { value: amount } = coin; assert!(amount > 0, error::invalid_argument(EINVALID_COIN_AMOUNT)); let coin_addr = type_info::account_address(&type_info::type_of()); let supply = &mut borrow_global_mut>(coin_addr).supply; if (option::is_some(supply)) { let supply = option::borrow_mut(supply); *supply = *supply - (amount as u128); } } /// Burn `coin` from the specified `account` with capability. /// The capability `burn_cap` should be passed as a reference to `BurnCapability`. /// This function shouldn't fail as it's called as part of transaction fee burning. public fun burn_from( account_addr: address, amount: u64, burn_cap: &BurnCapability, ) acquires CoinInfo, CoinStore { // Skip burning if amount is zero. This shouldn't error out as it's called as part of transaction fee burning. if (amount == 0) { return }; let coin_store = borrow_global_mut>(account_addr); let coin_to_burn = extract(&mut coin_store.coin, amount); burn(coin_to_burn, burn_cap); } /// Deposit the coin balance into the recipient's account and emit an event. public fun deposit(account_addr: address, coin: Coin) acquires CoinStore { assert!( is_account_registered(account_addr), error::not_found(ECOIN_STORE_NOT_PUBLISHED), ); let coin_store = borrow_global_mut>(account_addr); event::emit_event( &mut coin_store.deposit_events, DepositEvent { amount: coin.value }, ); merge(&mut coin_store.coin, coin); } /// Destroys a zero-value coin. Calls will fail if the `value` in the passed-in `token` is non-zero /// so it is impossible to "burn" any non-zero amount of `Coin` without having /// a `BurnCapability` for the specific `CoinType`. public fun destroy_zero(zero_coin: Coin) { let Coin { value } = zero_coin; assert!(value == 0, error::invalid_argument(EDESTRUCTION_OF_NONZERO_TOKEN)) } /// Extracts `amount` from the passed-in `coin`, where the original token is modified in place. public fun extract(coin: &mut Coin, amount: u64): Coin { assert!(coin.value >= amount, error::invalid_argument(EINSUFFICIENT_BALANCE)); coin.value = coin.value - amount; Coin { value: amount } } /// Extracts the entire amount from the passed-in `coin`, where the original token is modified in place. public fun extract_all(coin: &mut Coin): Coin { let total_value = coin.value; coin.value = 0; Coin { value: total_value } } /// Creates a new Coin with given `CoinType` and returns minting/burning capabilities. /// The given signer also becomes the account hosting the information /// about the coin (name, supply, etc.). public fun initialize( account: &signer, name: string::String, symbol: string::String, decimals: u64, monitor_supply: bool, ): (MintCapability, BurnCapability) { let account_addr = signer::address_of(account); let type_info = type_info::type_of(); assert!( type_info::account_address(&type_info) == account_addr, error::invalid_argument(ECOIN_INFO_ADDRESS_MISMATCH), ); assert!( !exists>(account_addr), error::already_exists(ECOIN_INFO_ALREADY_PUBLISHED), ); let coin_info = CoinInfo { name, symbol, decimals, supply: if (monitor_supply) { option::some(0) } else { option::none() }, }; move_to(account, coin_info); (MintCapability { }, BurnCapability { }) } /// "Merges" the two given coins. The coin passed in as `dst_coin` will have a value equal /// to the sum of the two tokens (`dst_coin` and `source_coin`). public fun merge(dst_coin: &mut Coin, source_coin: Coin) { dst_coin.value = dst_coin.value + source_coin.value; let Coin { value: _ } = source_coin; } /// Mint new `Coin` with capability. /// The capability `_cap` should be passed as reference to `MintCapability`. /// Returns minted `Coin`. public fun mint( amount: u64, _cap: &MintCapability, ): Coin acquires CoinInfo { if (amount == 0) { return zero() }; let coin_addr = type_info::account_address(&type_info::type_of()); let supply = &mut borrow_global_mut>(coin_addr).supply; if (option::is_some(supply)) { let supply = option::borrow_mut(supply); let amount_u128 = (amount as u128); assert!(*supply <= MAX_U128 - amount_u128, error::invalid_argument(ETOTAL_SUPPLY_OVERFLOW)); *supply = *supply + amount_u128; }; Coin { value: amount } } #[test_only] public fun register_for_test(account: &signer) { register(account) } public(friend) fun register(account: &signer) { let account_addr = signer::address_of(account); assert!( !is_account_registered(account_addr), error::already_exists(ECOIN_STORE_ALREADY_PUBLISHED), ); let coin_store = CoinStore { coin: Coin { value: 0 }, deposit_events: event::new_event_handle(account), withdraw_events: event::new_event_handle(account), }; move_to(account, coin_store); } /// Transfers `amount` of coins `CoinType` from `from` to `to`. public entry fun transfer( from: &signer, to: address, amount: u64, ) acquires CoinStore { let coin = withdraw(from, amount); deposit(to, coin); } /// Returns the `value` passed in `coin`. public fun value(coin: &Coin): u64 { coin.value } /// Withdraw specifed `amount` of coin `CoinType` from the signing account. public fun withdraw( account: &signer, amount: u64, ): Coin acquires CoinStore { let account_addr = signer::address_of(account); assert!( is_account_registered(account_addr), error::not_found(ECOIN_STORE_NOT_PUBLISHED), ); let coin_store = borrow_global_mut>(account_addr); event::emit_event( &mut coin_store.withdraw_events, WithdrawEvent { amount }, ); extract(&mut coin_store.coin, amount) } /// Create a new `Coin` with a value of `0`. public fun zero(): Coin { Coin { value: 0 } } // // Tests // #[test_only] struct FakeMoney { } #[test_only] struct FakeMoneyCapabilities has key { mint_cap: MintCapability, burn_cap: BurnCapability, } #[test_only] public entry fun create_fake_money( source: &signer, destination: &signer, amount: u64 ) acquires CoinInfo, CoinStore { let name = string::utf8(b"Fake money"); let symbol = string::utf8(b"FMD"); let (mint_cap, burn_cap) = initialize( source, name, symbol, 18, true ); register(source); register(destination); let coins_minted = mint(amount, &mint_cap); deposit(signer::address_of(source), coins_minted); move_to(source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x1, destination = @0x2)] public entry fun end_to_end( source: signer, destination: signer, ) acquires CoinInfo, CoinStore { let source_addr = signer::address_of(&source); let destination_addr = signer::address_of(&destination); let name = string::utf8(b"Fake money"); let symbol = string::utf8(b"FMD"); let (mint_cap, burn_cap) = initialize( &source, name, symbol, 18, true ); register(&source); register(&destination); assert!(*option::borrow(&supply()) == 0, 0); assert!(name() == name, 1); assert!(symbol() == symbol, 2); assert!(decimals() == 18, 3); let coins_minted = mint(100, &mint_cap); deposit(source_addr, coins_minted); transfer(&source, destination_addr, 50); assert!(balance(source_addr) == 50, 4); assert!(balance(destination_addr) == 50, 5); assert!(*option::borrow(&supply()) == 100, 6); let coin = withdraw(&source, 10); assert!(value(&coin) == 10, 7); burn(coin, &burn_cap); assert!(*option::borrow(&supply()) == 90, 8); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x1, destination = @0x2)] public entry fun end_to_end_no_supply( source: signer, destination: signer, ) acquires CoinInfo, CoinStore { let source_addr = signer::address_of(&source); let destination_addr = signer::address_of(&destination); let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, false, ); register(&source); register(&destination); assert!(option::is_none(&supply()), 0); let coins_minted = mint(100, &mint_cap); deposit(source_addr, coins_minted); transfer(&source, destination_addr, 50); assert!(balance(source_addr) == 50, 1); assert!(balance(destination_addr) == 50, 2); assert!(option::is_none(&supply()), 3); let coin = withdraw(&source, 10); burn(coin, &burn_cap); assert!(option::is_none(&supply()), 4); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x2)] #[expected_failure(abort_code = 0x10000)] public fun fail_initialize(source: signer) { let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, true, ); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x1, destination = @0x2)] #[expected_failure(abort_code = 0x60004)] public entry fun fail_transfer( source: signer, destination: signer, ) acquires CoinInfo, CoinStore { let source_addr = signer::address_of(&source); let destination_addr = signer::address_of(&destination); let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, true, ); register(&source); assert!(*option::borrow(&supply()) == 0, 0); let coins_minted = mint(100, &mint_cap); deposit(source_addr, coins_minted); transfer(&source, destination_addr, 50); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x1, destination = @0x2)] public entry fun test_burn_from_with_capability( source: signer, ) acquires CoinInfo, CoinStore { let source_addr = signer::address_of(&source); let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, true ); register(&source); let coins_minted = mint(100, &mint_cap); deposit(source_addr, coins_minted); assert!(balance(source_addr) == 100, 0); assert!(*option::borrow(&supply()) == 100, 1); burn_from(source_addr, 10, &burn_cap); assert!(balance(source_addr) == 90, 2); assert!(*option::borrow(&supply()) == 90, 3); move_to(&source, FakeMoneyCapabilities{ mint_cap, burn_cap, }); } #[test(source = @0x1)] #[expected_failure(abort_code = 0x10006)] public fun test_destroy_non_zero( source: signer, ) acquires CoinInfo { let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, true, ); let coins_minted = mint( 100, &mint_cap); destroy_zero(coins_minted); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x1)] public entry fun test_extract( source: signer, ) acquires CoinInfo, CoinStore { let source_addr = signer::address_of(&source); let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, true ); register(&source); let coins_minted = mint(100, &mint_cap); let extracted = extract(&mut coins_minted, 25); assert!(value(&coins_minted) == 75, 0); assert!(value(&extracted) == 25, 1); deposit(source_addr, coins_minted); deposit(source_addr, extracted); assert!(balance(source_addr) == 100, 2); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test(source = @0x1)] public fun test_is_coin_initialized(source: signer) { assert!(!is_coin_initialized(), 0); let (mint_cap, burn_cap) = initialize( &source, string::utf8(b"Fake money"), string::utf8(b"FMD"), 1, true ); assert!(is_coin_initialized(), 1); move_to(&source, FakeMoneyCapabilities { mint_cap, burn_cap }); } #[test] fun test_zero() { let zero = zero(); assert!(value(&zero) == 0, 1); destroy_zero(zero); } #[test_only] public fun destroy_mint_cap(mint_cap: MintCapability) { let MintCapability { } = mint_cap; } #[test_only] public fun destroy_burn_cap(burn_cap: BurnCapability) { let BurnCapability { } = burn_cap; } }