Skip to content
🎉 Welcome to the new Aptos Docs! Click here to submit feedback!
Build
Your First Fungible Asset

Your First Fungible Asset

This tutorial introduces how you can compile, deploy, and mint your own fungible asset (FA), named FACoin. Make sure you have understood FA before moving on to the tutorial. If not, it is highly recommended to read it first.

Step 1: Pick an SDK

Install your preferred SDK from the below list:


Step 2: Install the CLI

Install the precompiled binary for the Aptos CLI.


Step 3: Run the example

Clone the aptos-ts-sdk repo and build it:

Terminal
git clone https://github.com/aptos-labs/aptos-ts-sdk.git
cd aptos-ts-sdk
pnpm install
pnpm build

Navigate to the TypeScript examples directory:

Terminal
cd examples/typescript

Install the necessary dependencies:

Terminal
pnpm install

Run the TypeScript your_fungible_asset example:

Terminal
pnpm run your_fungible_asset

The application will complete, printing:

Terminal
===Publishing FAcoin package===
 
Transaction hash: 0x90fbe811171dde1ffad9157314ce0c4f6070fd5c1095a0e18a0b5634d10d7f48
metadata address: 0x77503715cb75fcc276b4e7236210fb0bcac7b510f6428233b65468b1cd3d708b
All the balances in this example refer to balance in primary fungible stores of each account.
Alice's initial FACoin balance: 0.
Bob's initial FACoin balance: 0.
Charlie's initial balance: 0.
Alice mints Charlie 100 coins.
Charlie's updated FACoin primary fungible store balance: 100.
Alice freezes Bob's account.
Alice as the admin forcefully transfers the newly minted coins of Charlie to Bob ignoring that Bob's account is frozen.
Bob's updated FACoin balance: 100.
Alice unfreezes Bob's account.
Alice burns 50 coins from Bob.
Bob's updated FACoin balance: 50.
Bob transfers 10 coins to Alice as the owner.
Alice's updated FACoin balance: 10.
Bob's updated FACoin balance: 40.

Step 4: FACoin in depth

Step 4.1: Building and publishing the FACoin package

Move contracts are effectively a set of Move modules known as a package. When deploying or upgrading a new package, the compiler must be invoked with --save-metadata to publish the package. In the case of FACoin, the following output files are critical:

  • build/Examples/package-metadata.bcs: Contains the metadata associated with the package.
  • build/Examples/bytecode_modules/fa_coin.mv: Contains the bytecode for the fa_coin.move module.

These are read by the example and published to the Aptos blockchain:

In the TypeScript example, we use aptos move build-publish-payload command to compile and build the module. That command builds the build folder that contains the package-metadata.bcs and the bytecode for the moon_coin.mv module. The command also builds a publication transaction payload and stores it in a JSON output file that we can later read from to get the metadataBytes and byteCode to publish the contract to chain with.

Compile the package:

compile.ts
export function compilePackage(
  packageDir: string,
  outputFile: string,
  namedAddresses: Array<{ name: string; address: AccountAddress }>,
) {
  const addressArg = namedAddresses
    .map(({ name, address }) => `${name}=${address}`)
    .join(" ");
  // Assume-yes automatically overwrites the previous compiled version, only do this if you are sure you want to overwrite the previous version.
  const compileCommand = `aptos move build-publish-payload --json-output-file ${outputFile} --package-dir ${packageDir} --named-addresses ${addressArg} --assume-yes`;
  execSync(compileCommand);
}
 
compilePackage("move/facoin", "move/facoin/facoin.json", [
  { name: "FACoin", address: alice.accountAddress },
]);

Publish the package to chain:

publish.ts
import path from "path";
import fs from "fs";
 
export function getPackageBytesToPublish(filePath: string) {
  // current working directory - the root folder of this repo
  const cwd = process.cwd();
  // target directory - current working directory + filePath (filePath json file is generated with the previous, compilePackage, CLI command)
  const modulePath = path.join(cwd, filePath);
 
  const jsonData = JSON.parse(fs.readFileSync(modulePath, "utf8"));
 
  const metadataBytes = jsonData.args[0].value;
  const byteCode = jsonData.args[1].value;
 
  return { metadataBytes, byteCode };
}
 
const { metadataBytes, byteCode } = getPackageBytesToPublish(
  "move/facoin/facoin.json",
);
 
// Publish FACoin package to chain
const transaction = await aptos.publishPackageTransaction({
  account: alice.accountAddress,
  metadataBytes,
  moduleBytecode: byteCode,
});
 
const pendingTransaction = await aptos.signAndSubmitTransaction({
  signer: alice,
  transaction,
});
 
await aptos.waitForTransaction({ transactionHash: pendingTransaction.hash });

Step 4.2: Understanding the FACoin module

The FACoin module contains a function called init_module in which it creates a named metadata object that defines a type of FA called “FACoin” with a bunch of properties. The init_module function is called when the module is published. In this case, FACoin initializes the FACoin metadata object, owned by the owner of the account. According to the module code, the owner will be the admin of “FACoin” so that they are entitled to manage “FACoin” under the fungible asset framework.

Managed Fungible Asset framework Managed Fungible Asset is a full-fledged FA management framework for FAs directly managed by users. It provides convenience wrappers around different refs and both primary and secondary fungible stores. This example is a simplified version that only deals with primary stores.


Step 4.3: Understanding the management primitives of FACoin

The creator of FACoin have several managing primitives:

  • Minting: Creating new coins.
  • Burning: Deleting coins.
  • Freezing/Unfreezing: Disabling/Enabling the owner of an account to withdraw from or deposit to their primary fungible store of FACoin.
  • Withdraw: Withdrawing FACoin from the primary store of any account regardless of being frozen or not.
  • Deposit: Deposit FACoin from the primary store of any account regardless of being frozen or not.
  • Transfer: Withdraw from one account and deposit to another regardless of either being frozen or not.

The entity that creates FACoin gains the capabilities for minting, burning, freezing/unfreezing, and forceful transferring between any fungible stores no matter they are frozen or not. So Withdraw, Deposit, and Transfer in the management module have different semantics than those described in fungible asset framework that limited by frozen status.


Step 4.3.1: Initializing “FACoin” metadata object

After publishing the module to the Aptos blockchain, the entity that published that coin type should initialize a metadata object describing the information about this FA:

fa_coin.move
module deploy_addr::fa_coin {
    fun init_module(admin: &signer) {
        let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL);
        primary_fungible_store::create_primary_store_enabled_fungible_asset(
            constructor_ref,
            option::none(),
            utf8(b"FA Coin"), /* name */
            utf8(ASSET_SYMBOL), /* symbol */
            8, /* decimals */
            utf8(b"https://example.com/favicon.ico"), /* icon */
            utf8(b"https://example.com"), /* project */
        );
 
        // Create mint/burn/transfer refs to allow creator to manage the fungible asset.
        let mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
        let burn_ref = fungible_asset::generate_burn_ref(constructor_ref);
        let transfer_ref = fungible_asset::generate_transfer_ref(constructor_ref);
        let metadata_object_signer = object::generate_signer(constructor_ref);
        move_to(
            &metadata_object_signer,
            ManagedFungibleAsset { mint_ref, transfer_ref, burn_ref }
        )
  }
}

This ensures that this FA type has never been initialized before using a named object. Notice the first line create a named object with a static seed, if the metadata object has been created it will abort. Then we call primary_fungible_store::create_primary_store_enabled_fungible_asset to create a FA metadata resource inside the newly created object, most of the time you call this function to initialize the metadata object. After this call, we generate all the Refs that are necessary for the management api and store them in a customized wrapper resource.

FACoin does all the initialization automatically upon package publishing via init_module(&signer).

Different from coin module, FA doesn’t require users to register to use it because primary store will be automatically created if necessary.


Step 4.3.3: Managing a coin

Minting coins requires MintRef that was produced during initialization. the function mint (see below) takes in the creator and an amount, and returns a FungibleAsset struct containing that amount of FA. If the FA tracks supply, it will be updated.

fa_coin.move
module deploy_addr::fa_coin {
    /// Mint as the owner of metadata object.
    public entry fun mint(admin: &signer, to: address, amount: u64) acquires ManagedFungibleAsset {
        let asset = get_metadata();
        let managed_fungible_asset = authorized_borrow_refs(admin, asset);
        let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, asset);
        let fa = fungible_asset::mint(&managed_fungible_asset.mint_ref, amount);
        fungible_asset::deposit_with_ref(&managed_fungible_asset.transfer_ref, to_wallet, fa);
  }
}

FACoin makes this easier by providing an entry function fa_coin::mint that accesses the required MintRef for the creator.

Similarly, the module provides burn, set_frozen_flag, transfer, Withdraw and Deposit functions to manage FACoin following the same pattern with different refs.


Step 4.3.4: API of Transferring FAs

Aptos provides several APIs to support FA flows with same names in different modules:

  • fungible_asset::{transfer/withdraw/deposit}: Move FA between different unfrozen fungible stores objects.
  • fungible_asset::{transfer/withdraw/deposit}_with_ref: Move FA between different fungible stores objects with the corresponding TransferRef regardless of their frozen status.
  • primary_fungible_store::{transfer/withdraw/deposit}: Move FA between unfrozen primary stores of different accounts.

There are two separate withdraw and deposit events instead of a single transfer event.

Step 4.4: Understanding how to use Dispatchable Hooks

Aptos provides APIs to register withdraw and deposit hooks, allowing for custom logic being injected at each step during a transfer. In the hooks, we will add a global pause to all withdraws and deposits upon a transfer.

fa_coin.move
module deploy_addr::fa_coin {
    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
    /// Global state to pause the FA coin.
    struct State has key {
        paused: bool,
    }
}

This is how you would register the hooks upon asset creation (init_module).

fa_coin.move
module deploy_addr::fa_coin {
    init_module(admin: &signer) {
      // ...previous code
 
      // Override the deposit and withdraw functions which mean overriding transfer.
        // This ensures all transfer will call withdraw and deposit functions in this module 
        // and perform the necessary checks.
        let deposit = function_info::new_function_info(
            admin,
            string::utf8(b"fa_coin"),
            string::utf8(b"deposit"),
        );
        let withdraw = function_info::new_function_info(
            admin,
            string::utf8(b"fa_coin"),
            string::utf8(b"withdraw"),
        );
        dispatchable_fungible_asset::register_dispatch_functions(
            constructor_ref,
            option::some(withdraw),
            option::some(deposit),
            option::none(),
        );
    }
}

After registration of the hooks, we can call the newly defined assert_not_paused function in our hooks to verify that a transfer is allowed.

fa_coin.move
module deploy_addr::fa_coin {
    // ...previous code
 
    /// Deposit function override to ensure that the account is not denylisted and the FA coin is not paused.
    public fun deposit<T: key>(
        store: Object<T>,
        fa: FungibleAsset,
        transfer_ref: &TransferRef,
    ) acquires State {
        assert_not_paused();
        fungible_asset::deposit_with_ref(transfer_ref, store, fa);
    }
 
    /// Withdraw function override to ensure that the account is not denylisted and the FA coin is not paused.
    public fun withdraw<T: key>(
        store: Object<T>,
        amount: u64,
        transfer_ref: &TransferRef,
    ): FungibleAsset acquires State {
        assert_not_paused();
        fungible_asset::withdraw_with_ref(transfer_ref, store, amount)
    }
 
    /// Assert that the FA coin is not paused.
    fun assert_not_paused() acquires State {
        let state = borrow_global<State>(object::create_object_address(&@FACoin, ASSET_SYMBOL));
        assert!(!state.paused, EPAUSED);
    }
}

Note that this will not prevent transfers of stores or impose the pause during minting and burning. When using this feature, developers should be careful to follow the guidelines to prevent circular dependencies between modules. Please read the developer guidelines in the Fungible Asset Standard section.

Supporting documentation