Back to Blog

Prácticas de Seguridad en el Desarrollo de Move (2): Aptos Coin

Code Auditing
November 21, 2022
19 min read

En el artículo anterior, presentamos brevemente cómo desarrollar un programa Hello World en la red Aptos. A partir de ahora, profundizaremos un poco más en el desarrollo de aplicaciones DeFi y sus consideraciones de seguridad. Como siempre, nos gustaría comenzar con algunos conceptos básicos pero importantes. En este artículo, nos centraremos en Aptos coin (es decir, el token fungible en Aptos [1]), incluyendo su desarrollo y gestión, e interacción.

TL;DR

Este artículo te explicará:

  • ¿qué es Aptos Coin?
  • ¿cómo crear y gestionar tu coin?
  • ¿cómo interactuar con tu coin?

0x1. Acerca de Aptos Coin

Como los átomos de DeFi, los tokens (o coins) se han utilizado ampliamente en los ecosistemas blockchain. Pueden usarse para representar muchos tipos de cosas, incluyendo moneda electrónica, participaciones en staking y poder de voto para la gestión organizacional. En cierta medida, la actividad diaria de DeFi puede simplemente considerarse como un enorme volumen de flujos de tokens a través de los sistemas blockchain.

Ethereum ha desarrollado un conjunto de estándares para tokens. El más conocido se llama ERC20, que especifica las interfaces con las que un token estándar ERC20 debe cumplir. ERC20 es un estándar de token fungible, mientras que también existen estándares de tokens no fungibles, como ERC721.

Similar a otros sistemas blockchain, Aptos también tiene su estándar de tokens [2] que define cómo se crean y utilizan los activos digitales en sus respectivas blockchains. Específicamente, en Aptos, el token fungible se llama coin, mientras que el token no fungible (es decir, NFT) se denomina token. A continuación, discutiremos la forma de crear, gestionar e interactuar con un Aptos coin.

0x2. Crea y Gestiona Tu Primer Coin

Aptos proporciona un módulo estándar oficial (similar a ERC20): coin.move. Al llamar a la API de este módulo, cualquier usuario puede crear fácilmente su propio coin. Además, coin.move también proporciona el mecanismo de permisos para gestionar coins, lo cual es importante y útil para construir aplicaciones DeFi complejas. A continuación, demostraremos cómo crear un coin basado en este módulo.

Como se mencionó en el artículo anterior, puedes crear un proyecto escribiendo el siguiente comando:

aptos move init --name my_coin

Luego necesitas crear un nuevo archivo Move dentro de la carpeta ``sources''. Ahora llenémoslo con el siguiente código de ejemplo, que define un módulo llamado bsc para crear y gestionar un coin estándar llamado 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 Diseño Básico

Primero, veamos la parte de la estructura. En total, se definen tres estructuras.

  • La estructura BSC, que se utiliza como identificador único del coin. De esta manera, este coin puede determinarse de forma única a través de la ruta BlockSec::bsc::BSC.
  • La estructura CapStore, que se utiliza para almacenar algunas capacidades obtenidas del módulo aptos_framework::coin. Estas capacidades corresponden a los permisos de algunas operaciones especiales y se explicarán más adelante.
  • La estructura BSCEventStore, que se utiliza para registrar eventos de usuario.

Después de esas estructuras, existe una función init_module, que se utiliza para inicializar el módulo y solo se llamará una vez cuando el módulo se publique en la cadena. En esta función, el módulo llama a coin::initialize<BSC> para registrar BlockSec::bsc::BSC como identificador único de un nuevo 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 {}

Después del registro, todas las funciones genéricas que toman el tipo BlockSec::bsc::BSC en aptos_framework::coin operarán sobre este coin. Este proceso de registro devolverá tres capacidades, es decir, MintCapability, FreezeCapability y BurnCapability. Estas capacidades son necesarias para acuñar coins, congelar cuentas de usuarios y quemar coins, respectivamente. En cierta medida, la funcionalidad de dicha estructura de capacidad es similar a una llave, que se usa para abrir un candado a un permiso específico. Si alguien tiene la llave, puede obtener el permiso correspondiente. Aquí almacenamos estas capacidades en la estructura CapStore (propiedad del administrador/publicador de este módulo) para uso posterior.

Mientras tanto, durante el proceso de registro, se almacenará una estructura CoinInfo bajo la dirección del administrador para registrar información relevante:

/// 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>,
}

Después de la invocación de la función init_module, tu coin ha sido registrado en la cadena. Sin embargo, nadie puede usar este coin ya que no existe ninguna circulación por el momento. Para hacer que el coin sea utilizable, se deben admitir algunas operaciones, incluyendo emisión, asignación y destrucción. Estas operaciones requieren las capacidades que obtuvimos al registrar el coin.

0x2.2 Gestión del Coin

Este coin está diseñado para obedecer las siguientes reglas:

  • Solo el administrador (admin) puede acuñar coins.
  • Los usuarios pueden quemar sus propios coins en cualquier momento.
  • Los usuarios pueden congelar/descongelar sus cuentas en cualquier momento.

En consecuencia, definimos cinco funciones de gestión, es decir, mint_coin, burn_coin, freeze_self, emergency_freeze y unfreeze. Las dos primeras funciones se encargan de acuñar coins y quemar coins, respectivamente; mientras que las tres últimas se utilizan para congelar y descongelar cuentas.

Acuñar Coins

En nuestro módulo, la función mint_coin se utiliza para acuñar coins. Debido a que solo los administradores pueden acuñar coins, tenemos que verificar la capacidad correspondiente en esta función.

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 función requiere tres parámetros:

  • cap_owner es de tipo &signer, es decir, el iniciador de la transacción.
  • to_address indica la dirección a la que se depositarán los coins acuñados.
  • amount indica el número de coins que se acuñarán.

Consta de tres pasos: adquirir la capacidad para acuñar coins, acuñar coins y depositar coins.

Primero, al comienzo de la función mint_coin, la dirección de cuenta del iniciador de la transacción se puede obtener mediante signer::address_of(cap_owner). Después de eso, se usa borrow_global<CapStore> para confirmar si la cuenta posee CapStore y verificar que es el administrador del módulo. Al hacerlo, podemos garantizar que solo el administrador puede acuñar coins, mientras que otros usuarios fallarán en este paso.

Segundo, la función mint_coin invocará entonces la función mint del módulo aptos_framework::coin para acuñar coins.

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

Aquí se requiere MintCapability. Específicamente, se requiere que se pase un parámetro llamado _cap como referencia de MintCapability. Luego el permiso asociado con la capacidad ```MintCapability``` también se transfiere en consecuencia. Aunque no hay control de acceso explícito, la verificación es aplicada por el lenguaje Move.

Tercero, la función mint_coin invocará la función deposit para depositar los coins acuñados en la to_address especificada.

Consejo#1: Los controles de acceso para cuentas privilegiadas pueden verificarse utilizando un método similar.

Quemar Coins

El procedimiento de quemar coins es diferente al de acuñar coins. Específicamente, solo el administrador puede llamar a la función mint_coin, mientras que cualquier usuario puede invocar la función burn_coin. Para ello, la función burn_coin tiene que escalar temporalmente el privilegio, es decir, obtener la capacidad BurnCapability, para estos usuarios.

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 función requiere dos parámetros:

  • account es de tipo &signer, es decir, el iniciador de la transacción.
  • amount indica el número de coins que se quemarán.

También consta de tres pasos: adquirir la capacidad para quemar coins, retirar coins y quemar coins.

Obviamente, la función burn del módulo aptos_framework::coin requiere que el llamador pase una referencia a BurnCapability, pero esta capacidad está almacenada en el CapStore del administrador. Por lo tanto, tenemos que permitir que los usuarios ordinarios obtengan esta capacidad para quemar los coins que poseen.

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 lograr este objetivo, podemos usar el operador borrow_global proporcionado por el lenguaje Move. Se utiliza para leer un tipo de dato particular del almacenamiento global inmutable de una cuenta. Al usar este operador, un módulo puede prestar la capacidad que posee el administrador a otros usuarios. Es decir, se requiere la dirección del administrador para obtener la capacidad que queremos.

Sin embargo, el iniciador de la transacción de la función burn_coin es el usuario y no el administrador. Por lo tanto, la dirección del administrador no se puede obtener a través de signer (como la función mint_coin). Afortunadamente, se puede obtener a través de aptos_std::type_info con BSC para obtener la dirección del módulo donde se define esta estructura. Como el módulo fue publicado bajo la dirección del administrador, podemos obtener la dirección del administrador en consecuencia y, finalmente, obtener la capacidad BurnCapability.

Consejo#2: El operador _borrow_global_ puede usarse para obtener temporalmente las capacidades de un módulo.

Después de obtener la BurnCapability, el módulo puede retirar el coin de la cantidad especificada del usuario y quemar el coin con esa capacidad.

Congelar y Descongelar Cuentas de Coin

Basándonos en la discusión anterior, ahora podemos recorrer fácilmente la gestión de las cuentas de coin. Específicamente, proporcionamos la función freeze_self para que los usuarios congelen sus cuentas de coin. Aquí también proporcionamos la función emergency_freeze para congelación de emergencia, que solo puede ser utilizada por el administrador. Además, debido a la existencia del mecanismo de congelación de emergencia, los usuarios no deberían poder descongelarse a sí mismos. Por lo tanto, la función unfreeze también requiere que el administrador descongele las cuentas de usuario.

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

Consejo#3: ¡Ten cuidado al devolver capacidades a través de funciones! ¡Los usuarios maliciosos que obtengan estas capacidades pueden abusar de ellas para causar daños a los titulares de coins!

0x3. Interactuar Con el Coin

Aquí nos centramos en la forma de interactuar con el coin.

0x3.1 Registrar

Es posible que hayas notado que existe una función register en el 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 función se utiliza para ayudar a los usuarios a registrar los derechos de uso del coin y los registradores de eventos. Para usar un determinado coin, el módulo aptos_framework::coin estipula que el usuario primero debe registrar explícitamente el derecho a usar el coin a través de la función 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);
}

Los usuarios pueden tener este coin normalmente solo si registran este tipo de coin a través de esta función. Es decir, si no deseas tener un coin en particular, otros no pueden poner ese coin en tu cuenta sin tu consentimiento. El registro en realidad coloca una estructura CoinStore (del tipo de coin objetivo) en tu cuenta. Esta estructura CoinStore contiene una estructura Coin para registrar tu saldo.

Consejo#4: A diferencia de los tokens de Ethereum, un coin de Aptos no puede poseerse ni operarse sin el registro explícito de un usuario.

0x3.2 Transferir

Suponiendo que ahora tienes algunos coins BSC, puedes transferirlos invocando la función transfer del 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);
}

Ten en cuenta que esta es una función de entrada proporcionada por el módulo coin. La lógica consiste en invocaciones de dos funciones públicas, es decir, withdraw y deposit. La función withdraw requiere el permiso &signer, que se utiliza para retirar una cierta cantidad de activos de tu cuenta a un coin. La función deposit puede depositar un coin en cualquier cuenta registrada del coin. Esta función no necesita permisos adicionales y depositará los coins especificados en la dirección de la cuenta. Finalmente, los coins transferidos se fusionarán automáticamente con los coins almacenados en la estructura CoinStore de la dirección de destino.

Consejo#5: Después de retirar, los activos en el coin están bajo el control de la función _transfer_ actual. Esta función puede entregar estos activos a la función _deposit_ sin adquirir permisos adicionales.

0x3.3 Dividir y Fusionar

A diferencia de los tokens de Ethereum, la circulación de coins no puede actualizarse modificando los saldos de los usuarios. En cambio, puede lograrse retirando la estructura Coin en el módulo coin. Al hacerlo, los usuarios realizan la circulación de activos pasando esta estructura a otros módulos. Dado que una estructura solo puede ser manipulada por el módulo que la define, el módulo coin proporciona algunas interfaces para operar la estructura Coin, incluyendo dividir coins en unidades más pequeñas y fusionar múltiples coins para satisfacer las necesidades de diferentes escenarios.

1. La función extract se utiliza para dividir coins. Recibe una estructura Coin, extrae una parte del activo en ella para generar una nueva estructura Coin y devuelve la nueva estructura.

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. La función extract_all se utiliza para extraer el valor completo de la estructura Coin original y depositarlo en una nueva estructura Coin. Como resultado, el valor de la estructura Coin original se volverá cero (también conocido como zero_coin). La estructura zero_coin puede destruirse invocando la función 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. La función merge se utiliza para fusionar coins. Puedes fusionar el valor de dos estructuras Coin, es decir, source_coin y dst_coin, en la estructura dst_coin y destruir la estructura 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. La función zero se utiliza para generar una estructura zero_coin.

public fun zero<CoinType>(): Coin<CoinType> {
    Coin<CoinType> {
        value: 0
    }
}

0x4. Probar el Coin

Para probar rápidamente este coin, primero puedes desplegarlo con el siguiente comando (¡no olvides establecer la dirección de tu cuenta de publicador en 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",
    ...

De esta manera, el coin se publicó exitosamente en la cadena, pero no tiene ninguna circulación. Necesitas registrar tu cuenta para recibir los coins acuñados.

$ 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",
    ...

Ten en cuenta que Tu-dirección en el segundo comando debe reemplazarse con tu propia dirección (ver account en .aptos/config.yaml). Ingresa tu dirección en el navegador y haz clic en la pestaña Resources, puedes ver que esta cuenta ahora tiene 100 coins BSC.

Si deseas transferir coins a otra cuenta, no olvides hacer que esa cuenta se registre para usar el coin. Debido a que la función transfer es una función genérica, debes especificar el parámetro genérico como BSC y to_address, de la siguiente manera:

$ aptos move run — function-id 0x1::coin::transfer — type-args BSC-module-address::bsc::BSC — args address:To_address u64:1

Este comando invocará la función transfer del módulo coin para transferir 1 coin BSC a To_address. Aquí 0x1::coin::transfer es el id de función de la función transfer. Recuerda que BlockSec::bsc::BSC es el identificador de tu coin, el parámetro genérico debe especificarse como tal. Además, BSC-module-address debe reemplazarse con la dirección de cuenta del publicador del módulo, que se asigna a BlockSec en Move.toml.

0x5. ¿Qué Sigue?

Después de entender cómo crear, gestionar e interactuar con tu propio coin, demostraremos cómo construir el primer proyecto fundamental de DeFi: el Creador de Mercado Automatizado (AMM). Se cubrirán más temas interesantes relacionados con el desarrollo en Move y las prácticas de seguridad. ¡Mantente atento!

Referencia

[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