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 theapproveBase()
method to be defined by the subclassTransferable
a wrapper aroundApprovable
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