No artigo anterior, apresentamos brevemente como desenvolver um programa Hello World na rede Aptos. A partir de agora, vamos nos aprofundar um pouco mais no desenvolvimento de aplicações DeFi e suas preocupações de segurança. Como sempre, gostaríamos de começar com alguns conceitos básicos, mas importantes. Neste artigo, vamos nos concentrar na moeda Aptos (ou seja, o token fungível no Aptos [1]), incluindo seu desenvolvimento e gerenciamento, e interação.
TL;DR
Este artigo irá apresentar:
- o que é a Moeda Aptos?
- como criar e gerenciar sua moeda?
- como interagir com sua moeda?
0x1. Sobre a Moeda Aptos
Como os átomos do DeFi, os tokens (ou moedas) têm sido amplamente utilizados nos ecossistemas blockchain. Eles podem ser usados para representar muitos tipos de coisas, incluindo moeda eletrônica, cotas de staking e poder de voto para gestão organizacional. Em certa medida, a atividade diária do DeFi pode ser simplesmente considerada como um enorme volume de fluxos de tokens pelos sistemas blockchain.
O Ethereum desenvolveu um conjunto de padrões para tokens. O mais conhecido é chamado ERC20, que especifica as interfaces com as quais um token ERC20 padrão precisa estar em conformidade. ERC20 é um padrão de token fungível, enquanto também existem padrões de tokens não fungíveis, como o ERC721.
Semelhante a outros sistemas blockchain, o Aptos também possui seu padrão de token [2] que define como os ativos digitais são criados e usados em seus respectivos blockchains. Especificamente, no Aptos, o token fungível é chamado de coin (moeda), enquanto o token não fungível (ou seja, NFT) é denominado token. A seguir, discutiremos a forma de criar, gerenciar e interagir com uma moeda Aptos.
0x2. Crie e Gerencie Sua Primeira Moeda
O Aptos fornece um módulo padrão oficial (semelhante ao ERC20): coin.move. Ao chamar a API deste módulo, qualquer usuário pode criar facilmente sua própria moeda. Além disso, o coin.move também fornece o mecanismo de permissão para gerenciar moedas, o que é importante e útil para construir aplicações DeFi mais complexas. A seguir, demonstraremos como criar uma moeda baseada neste módulo.
Conforme mencionado no artigo anterior, você pode criar um projeto digitando o seguinte comando:
aptos move init --name my_coin
Em seguida, você precisa criar um novo arquivo Move na pasta sources. Agora vamos preenchê-lo com o seguinte código de exemplo, que define um módulo chamado bsc para criar e gerenciar uma moeda padrão chamada 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 Design Básico
Primeiro, observe a parte de estrutura. No total, três estruturas são definidas.
- A struct
BSC, que é usada como identificador único da moeda. Dessa forma, esta moeda pode ser determinada de forma exclusiva pelo caminhoBlockSec::bsc::BSC. - A struct
CapStore, que é usada para armazenar algumas capacidades obtidas do móduloaptos_framework::coin. Essas capacidades correspondem às permissões de algumas operações especiais e serão explicadas mais adiante. - A struct
BSCEventStore, que é usada para registrar eventos do usuário.
Após essas estruturas, há uma função init_module, que é usada para inicializar o módulo e será chamada apenas uma vez quando o módulo for publicado na blockchain. Nesta função, o módulo chama coin::initialize<BSC> para registrar o BlockSec::bsc::BSC como um identificador único para uma nova moeda.
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 {}
Após o registro, todas as funções genéricas que recebem o tipo BlockSec::bsc::BSC em aptos_framework::coin operarão sobre esta moeda. Esse processo de registro retornará três capacidades, ou seja, MintCapability, FreezeCapability e BurnCapability. Essas capacidades são necessárias para cunhar moedas, congelar contas de usuários e queimar moedas, respectivamente. Em certa medida, a funcionalidade de tal struct de capacidade é semelhante a uma chave, que é usada para abrir um cadeado de uma permissão específica. Se alguém tem a chave, ele pode obter a permissão correspondente. Aqui armazenamos essas capacidades na struct CapStore (de propriedade do administrador/publicador deste módulo) para uso posterior.
Enquanto isso, durante o processo de registro, uma struct CoinInfo será armazenada sob o endereço do administrador para registrar informações relevantes:
/// 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>,
}
Após a invocação da função init_module, sua moeda foi registrada na blockchain. No entanto, ninguém pode usar esta moeda, pois não existe nenhuma circulação por enquanto. Para tornar a moeda utilizável, algumas operações, incluindo emissão, alocação e destruição, precisam ser suportadas. Essas operações requerem as capacidades que obtivemos ao registrar a moeda.
0x2.2 Gerenciamento de Moedas
Esta moeda foi projetada para obedecer às seguintes regras:
- Somente o administrador (admin) pode cunhar moedas.
- Os usuários podem queimar suas próprias moedas a qualquer momento.
- Os usuários podem congelar/descongelar suas contas a qualquer momento.
Assim, definimos cinco funções de gerenciamento, ou seja, mint_coin, burn_coin, freeze_self, emergency_freeze e unfreeze. As duas primeiras funções são responsáveis por cunhar moedas e queimar moedas, respectivamente; enquanto as três últimas são usadas para congelar e descongelar contas.
Cunhagem de Moedas
Em nosso módulo, a função mint_coin é usada para cunhar moedas. Como somente admins podem cunhar moedas, temos que verificar a capacidade correspondente nesta função.
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"));
}
Esta função requer três parâmetros:
cap_owneré do tipo&signer, ou seja, o iniciador da transação.to_addressindica o endereço para o qual as moedas cunhadas serão depositadas.amountindica a quantidade de moedas sendo cunhadas.
Ela consiste em três etapas: adquirir a capacidade de cunhar moedas, cunhar moedas e depositar moedas.
Primeiro, no início da função mint_coin, o endereço da conta do iniciador da transação pode ser obtido através de signer::address_of(cap_owner). Depois disso, borrow_global<CapStore> é usado para confirmar se a conta possui CapStore para verificar que ela é o admin do módulo. Ao fazer isso, podemos garantir que apenas o admin pode cunhar moedas, enquanto outros usuários falharão nesta etapa.
Em segundo lugar, a função mint_coin invocará então a função mint do módulo aptos_framework::coin para cunhar moedas.
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 }
}
Aqui MintCapability é necessário. Especificamente, um parâmetro chamado _cap precisa ser passado como referência de MintCapability. Em seguida, a permissão associada à capacidade MintCapability também é transferida. Embora não haja controle de acesso explícito, a verificação é imposta pela linguagem Move.
Terceiro, a função mint_coin invocará a função deposit para depositar as moedas cunhadas no to_address especificado.
Dica#1: Os controles de acesso para contas privilegiadas podem ser verificados usando um método semelhante.
Queima de Moedas
O procedimento de queima de moedas é diferente do de cunhagem de moedas. Especificamente, apenas o admin tem permissão para chamar a função mint_coin, enquanto qualquer usuário pode invocar a função burn_coin. Para este fim, a função burn_coin tem que elevar temporariamente o privilégio, ou seja, obtendo a capacidade BurnCapability, para esses usuários.
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"));
}
Esta função requer dois parâmetros:
accounté do tipo&signer, ou seja, o iniciador da transação.amountindica a quantidade de moedas sendo queimadas.
Ela também consiste em três etapas: adquirir a capacidade de queimar moedas, sacar moedas e queimar moedas.
Obviamente, a função burn do módulo aptos_framework::coin exige que o chamador passe uma referência para BurnCapability, mas essa capacidade está armazenada no CapStore do admin. Dessa forma, temos que permitir que usuários comuns obtenham essa capacidade para queimar as moedas que possuem.
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));
}
}
Para atingir esse objetivo, podemos usar o operador borrow_global fornecido pela linguagem Move. Ele é usado para ler um tipo de dado específico do armazenamento global imutável de uma conta. Ao usar este operador, um módulo pode emprestar a capacidade pertencente ao admin para outros usuários. Ou seja, o endereço do admin é necessário para obter a capacidade desejada.
No entanto, o iniciador da transação da função burn_coin é o usuário e não o admin. Portanto, o endereço do admin não pode ser obtido através de signer (como na função mint_coin). Felizmente, ele pode ser obtido através de aptos_std::type_info com BSC para obter o endereço do módulo onde esta estrutura é definida. Como o módulo foi publicado sob o endereço do admin, podemos então obter o endereço do admin e, finalmente, obter a capacidade BurnCapability.
Dica#2: O operador
_borrow_global_pode ser usado para obter temporariamente as capacidades de um módulo.
Após obter o BurnCapability, o módulo pode sacar a moeda no valor especificado do usuário e queimar a moeda com essa capacidade.
Congelamento e Descongelamento de Contas de Moedas
Com base na discussão acima, agora podemos passar facilmente pelo gerenciamento das contas de moedas. Especificamente, fornecemos a função freeze_self para os usuários congelarem suas contas de moedas. Aqui também fornecemos a função emergency_freeze para congelamento de emergência, que só pode ser usada pelo admin. Além disso, devido à existência do mecanismo de congelamento de emergência, os usuários não devem ter permissão para descongelar a si mesmos. Portanto, a função unfreeze também exige que o admin descongele as contas dos usuários.
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"));
}
Dica#3: Tenha cuidado ao retornar capacidades por meio de funções! Usuários maliciosos que obtêm essas capacidades podem utilizá-las indevidamente para causar danos aos detentores de moedas!
0x3. Interagir Com a Moeda
Aqui nos concentramos na forma de interagir com a moeda.
0x3.1 Registro
Você pode ter notado que existe uma função register no módulo:
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)});
};
}
Esta função é usada para ajudar os usuários a registrar direitos de uso de moedas e gravadores de eventos. Para usar uma determinada moeda, o módulo aptos_framework::coin estipula que o usuário deve primeiro registrar explicitamente o direito de uso da moeda através da função 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);
}
Os usuários podem manter esta moeda normalmente somente se registrarem este tipo de moeda através desta função. Ou seja, se você não deseja manter uma moeda específica, outros não podem colocar esta moeda em sua conta sem o seu consentimento. O registro na verdade coloca uma struct CoinStore (do tipo de moeda alvo) em sua conta. Esta struct CoinStore contém uma struct Coin para registrar seu saldo.
Dica#4: Ao contrário dos tokens Ethereum, uma moeda Aptos não pode ser mantida e operada sem o registro explícito de um usuário.
0x3.2 Transferência
Supondo que você agora tenha algumas moedas BSC, você pode transferir essas moedas invocando a função transfer do módulo aptos_framework::coin.
public entry fun transfer<CoinType>(
from: &signer,
to: address,
amount: u64,
) acquires CoinStore {
let coin = withdraw<CoinType>(from, amount);
deposit(to, coin);
}
Note que esta é uma função de entrada fornecida pelo módulo coin. A lógica consiste em invocações de duas funções públicas, ou seja, withdraw e deposit. A função withdraw requer a permissão &signer, que é usada para sacar uma certa quantidade de ativos de sua conta para uma moeda. A função deposit pode depositar uma moeda em qualquer conta registrada da moeda. Esta função não precisa de permissões extras e depositará as moedas especificadas no endereço da conta. Finalmente, as moedas transferidas serão automaticamente mescladas com as moedas armazenadas na struct CoinStore do endereço de destino.
Dica#5: Após o saque, os ativos na moeda estão sob o controle da função
_transfer_atual. Esta função pode entregar esses ativos à função_deposit_sem adquirir permissões extras.
0x3.3 Divisão e Mesclagem
Ao contrário dos tokens Ethereum, a circulação de moedas não pode ser atualizada modificando os saldos dos usuários. Em vez disso, isso pode ser alcançado sacando a struct Coin no módulo coin. Ao fazer isso, os usuários realizam a circulação de ativos passando esta struct para outros módulos. Como uma struct só pode ser manipulada pelo módulo que a define, o módulo coin fornece algumas interfaces para operar a struct Coin, incluindo dividir moedas em unidades menores e mesclar múltiplas moedas para atender às necessidades de diferentes cenários.
1. A função extract é usada para dividir moedas. Ela recebe uma struct Coin, extrai uma parte do ativo nela para gerar uma nova struct Coin, e retorna a nova struct.
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. A função extract_all é usada para extrair o valor total da struct Coin original e depositá-lo em uma nova struct Coin. Como resultado, o valor da struct Coin original se tornará zero (também conhecido como zero_coin). A struct zero_coin pode ser destruída invocando a função 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. A função merge é usada para mesclar moedas. Você pode mesclar o valor de duas structs Coin, ou seja, source_coin e dst_coin, na struct dst_coin e destruir a struct 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. A função zero é usada para gerar uma struct zero_coin.
public fun zero<CoinType>(): Coin<CoinType> {
Coin<CoinType> {
value: 0
}
}
0x4. Testando a Moeda
Para testar rapidamente esta moeda, você pode primeiro implantá-la com o seguinte comando (não se esqueça de definir o endereço da sua conta de publicador em 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",
...
Dessa forma, a moeda foi publicada com sucesso na blockchain, mas não possui nenhuma circulação. Você precisa registrar sua conta para receber as moedas cunhadas.
$ 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",
...
Note que Seu-endereço no segundo comando precisa ser substituído pelo seu próprio endereço (veja account em .aptos/config.yaml). Digite seu endereço no navegador e clique na aba Resources, você pode ver que esta conta agora possui 100 moedas BSC.


Se você quiser transferir moedas para outra conta, não se esqueça de fazer com que essa conta se registre para usar a moeda. Como a função transfer é uma função genérica, você precisa especificar o parâmetro genérico como BSC e to_address, da seguinte forma:
$ aptos move run — function-id 0x1::coin::transfer — type-args BSC-module-address::bsc::BSC — args address:To_address u64:1
Este comando invocará a função transfer do módulo coin para transferir 1 moeda BSC para To_address. Aqui 0x1::coin::transfer é o id da função transfer. Lembre-se que BlockSec::bsc::BSC é o identificador de sua moeda, o parâmetro genérico deve ser especificado para ele. Além disso, BSC-module-address deve ser substituído pelo endereço da conta do publicador do módulo, que é atribuído a BlockSec em Move.toml.
0x5. Próximos Passos
Após entender como criar, gerenciar e interagir com sua própria moeda, demonstraremos como construir o primeiro projeto fundamental de DeFi: o Automated Market Maker (AMM). Mais tópicos interessantes relacionados ao desenvolvimento em Move e práticas de segurança serão abordados. Fique ligado!
Referência
[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



