Skip to main content

Custom Tokens

Blockchain applications have various use cases for custom tokens, including a real-world financial asset, stake in an on-chain protocol, or even skill points in a game.

Most blockchains, like Ethereum, do not natively support custom tokens. You implement custom tokens as smart contracts on top of the execution layer of the underlying protocol. Token standards ensure the interoperability of applications on Etherum, these standardisations are agree upon in ERCs, Ethereum Request for customElements, such as the fungible token standard ERC-20. The Ethereum community has created and agreed upon other reference implementations and standardisation that are audited and easy to configure, such as ERC-721 for NFTs.

Mina supports custom token functionality at a low level in the tech stack. Mina treats custom tokens almost the same way as the native MINA token. This approach offers the following benefits:

  • As a developer, you do not have to manage as many boilerplate contracts.
  • Developers don't need to keep track of accounts and balances themselves.
  • It is more secure because fewer vulnerabilities can result from incorrect configuration and deployment.

Each account on Mina can have tokens associated with it. With zkApps, you build smart contracts that interact with tokens, such as swapping one token for another or depositing MINA tokens. A token manager smart contract is a standard smart contract with the TokenContract class that manipulates tokens.

Token manager account

The token manager account can set a token symbol (also called token name) for its token. For example, MYTKN. Uniqueness is not enforced for token names because the public key of the manager account is used to derive a unique identifier for each token.

A token manager smart contract sets the rules around minting, burning, and sending the custom token:

  • Minting generates new tokens. The zkApp updates an account's balance by adding the newly created tokens to it. You can send minted tokens to any existing account in the network.
  • Burning tokens is the opposite of minting. Burning tokens deducts the balance of a certain address by the specified amount. A zkApp cannot burn more tokens than the specified account has.
  • Sending tokens between two accounts must be approved by a zkApp.

TokenContract class

Use the TokenContract class to perform common token operations, such as minting, burning, and sending tokens. In o1js, the TokenContract class is your blueprint for custom token implementations.

As shown in this example code, you inherit from the TokenContract class:

class ExampleTokenContract extends TokenContract { 
// your custom token implementation
}

TokenContract API

The TokenContract comes with a set of prebuilt methods and helpers to get you started in your token journey. The base token smart contract implements the following two APIs:

  • Approvable leaves the approveBase() method to be defined by the subclass
  • Transferable a wrapper around Approvable that deals with transfers of token

Additionally, the token smart contract also comes with an internal namespace which contains helper methods that can be used from within a token contract only.

TokenContract.internal: {
/**
* Mints token balance to `address`. Returns the mint account update.
*/
mint(
address: PublicKey | AccountUpdate | SmartContract;
amount: number | bigint | UInt64;
): AccountUpdate;
/**
* Burn token balance on `address`. Returns the burn account update.
*/
burn(
address: PublicKey | AccountUpdate | SmartContract;
amount: number | bigint | UInt64;
): AccountUpdate;
/**
* Move token balance from `from` to `to`. Returns the `to` account update.
*/
send(
from: PublicKey | AccountUpdate | SmartContract;
to: PublicKey | AccountUpdate | SmartContract;
amount: number | bigint | UInt64;
): AccountUpdate;
}

The Approvable API

Each subclass token contract that inherits the default TokenContract must implement the core approveBase() method. It has the following signature:

approveBase(forest: AccountUpdateForest): void;

The TokenContract also containts helper methods that make it easy to iterate through and approve a forest of child account updates. The usual implementation is as easy as this:

@method async approveBase(forest: AccountUpdateForest) {
this.checkZeroBalanceChange(forest);
}

However, if you want to do a custom implementation for every child account update, you can utilize the forEachUpdate() method.

@method async 
approveBase(updates: AccountUpdateForest) {
let totalBalanceChange = Int64.zero;

this.forEachUpdate(updates, (accountUpdate, usesToken) => {
totalBalanceChange = totalBalanceChange.add(
Provable.if(usesToken, accountUpdate.balanceChange, Int64.zero)
);
// additional logic
});

// prove that the total balance change is zero
totalBalanceChange.assertEquals(0);
}

The Approvable API also provides easy to use wrappers around approveBase(), such as the following:

abstract class TokenContract extends SmartContract {
/**
* Approve a single account update (with arbitrarily many children).
*/
approveAccountUpdate(accountUpdate: AccountUpdate): Promise<void>;;
/**
* Approve a list of account updates (with arbitrarily many children).
*/
approveAccountUpdates(accountUpdates: AccountUpdate[]): Promise<void>;;
/**
* Transfer `amount` of tokens from `from` to `to`.
*/
transfer(from: PublicKey | AccountUpdate, to: PublicKey | AccountUpdate, amount: UInt64): Promise<void>;;
}

The Transferable API

The Transferable API is a simple wrapper around the Approvable API. It implements the following method:

abstract class TokenContract extends SmartContract {
/**
* Transfer `amount` of tokens from `from` to `to`.
*/
transfer(
from: PublicKey | AccountUpdate,
to: PublicKey | AccountUpdate,
amount: UInt64 | number | bigint
): Promise<void>;
}

Which utlizses the Approvable API to send token from an account to another one.

Custom Token Terminology

If your zkApp interacts with custom tokens, here are the essential terms.

Token id

Token ids are unique identifiers that distinguish between different types of custom tokens. Custom token identifiers are globally unique across the entire network.

Token ids are derived from a zkApp. To check the token id of a zkApp, use the this.token.id property.

Token Accounts

Token accounts are like regular accounts, but they hold a balance of a specific custom token instead of MINA. A token account is created from an existing account and is specified by a public key and a token id.

Token accounts are specific for each type of custom token, so a single public key can have many different token accounts.

A token account is automatically created for a public key whenever an existing account receives a transaction denoted with a custom token.

When a token account is created for the first time, an account creation fee must be paid the same as creating a new standard account.

In addition to sending custom tokens, a token owner account can mint and burn custom tokens. A token owner account is the governing zkApp account for a specific custom token.

Token Owner

A token owner is an zkApp account that creates, facilitates, and governs how a custom token can be used. The token owner is the account that created the custom token and is the only account that can:

  • Mint tokens
  • Burn tokens
  • Approve sending tokens between two accounts