zkApp programmability is not yet available on the Mina Mainnet. You can get started now by deploying zkApps to the Berkeley Testnet.
Tutorial 8: Custom Tokens
In this tutorial, you learn to create custom tokens.
Mina comes with native support for custom tokens. Each account on Mina can also have tokens associated with it.
To create a new token, one creates a smart contract, which becomes the manager for the token, and uses that contract to set the rules around how the token can be mint, burned, and sent.
The manager account may also set a token symbol for its token, such as in this example, MYTKN
. Uniqueness is not enforced for token names. Instead the public key of the manager account is used to identify tokens.
In this tutorial, you review smart contract code that creates and manages new tokens.
The full example code is provided in the 08-custom-tokens/src/ example files.
For reference, a more extensive example, including all the ways to interact with token smart contracts, is provided in token.test.ts.
Prerequisites
- Ensure your environment meets the Prerequisites for zkApp Developer Tutorials.
This tutorial has been tested with:
- Mina zkApp CLI version 0.17.2
- o1js version 0.17.0
Create the project
Create or change to a directory where you have write privileges.
Create a project by using the
zk project
command:$ zk project 08-custom-tokens
The
zk project
command has the ability to scaffold the UI for your project. For this tutorial, selectnone
:? Create an accompanying UI project too? …
next
svelte
nuxt
empty
❯ none
Prepare the project
Change to the project directory, delete the existing files, and create a new
src/BasicTokenContract
smart contract, and aindex.ts
file:$ cd 08-custom-tokens
$ rm src/Add.ts
$ rm src/Add.test.ts
$ rm src/interact.ts
$ zk file src/BasicTokenContract
$ touch src/index.tsEdit
index.ts
to import and export your new smart contract:import { BasicTokenContract } from './BasicTokenContract.js';
export { BasicTokenContract };Add the official token standard implementation to your project dependencies
$ npm i mina-fungible-token
Concepts
As mentioned above, Mina comes with custom token mechanism built-in.
Let's pause to explore terms and ideas, that are essential for understanding how this mechanism is implemented in Mina.
Token Manager
The token manager account is a zkApp that can:
- Set a token symbol (also called token name) for its token. 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.
- Mint 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.
- Burn tokens (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.
- Send tokens between two accounts. Any account can initiate a transfer, and the transfer must be approved by a Token Manager zkApp (see Approval mechanism).
Token Account
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.
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.deriveTokenId()
function.
Approval mechanism
Sending tokens between two accounts must be approved by a Token Manager zkApp. This can be done with approveBase()
method of the custom token standard reference implementation.
If you customize the transfer()
function, don't forget to call approveBase()
.
Upgradeability
As opposed to most blockchains, where you deploy a bytecode, on Mina, when you deploy a smart contract you generate a verification key from the contract source code. The verification key is stored on-chain and used to verify proofs that belong to that smart contract.
That means, the upgradeability is achieved by changing the verification key. In the Customn Token Standard implementation this can be done with setVerificationKey()
method.
FungibleTokenBase implementation overview
The token standard implementation is a Token Manager zkApp that is splitted in 2 parts: low-level and high-level one.
The low-level implementation is included in o1js
library TokenContract
abstract class. See the overview in the o1js Custom Tokens tutorial
The high-level part inherts from the TokenContract
class and has following user-facing features:
On-chain State, decimals
and deploy arguments
The on-chain state is defined as follows:
@state(PublicKey) public adminAccount = State<PublicKey>();
@state(UInt64) public totalSupply = State<UInt64>();
@state(UInt64) public circulatingSupply = State<UInt64>();
The
adminAccount
is set on deployment, and some of token functionality requires an admin signature.If you want to implement admin-only method, just call
this.requireAdminSignature()
helper in the method you want to protect.The
totalSupply
defines a maximum amount of tokens to exist. It is set on deployment and can be modified withsetTotalSupply()
function (can be called by admin only)The
circulatingSupply
tracks the total amount in circulation. When new tokens are minted, thecirculatingSupply
changes by an amount minted.The
decimals
is a constant, that defines where to place the decimal comma in the token amounts. It is exposed ingetDecimals()
method.The
.deploy()
function requiresadminAccount
andtotalSupply
to be passed as parameters.
Methods
Transfer and burn functionality is available by following methods:
transfer(from: PublicKey | AccountUpdate, to: PublicKey | AccountUpdate, amount: UInt64 | number | bigint)
burn(from: PublicKey, amount: UInt64)
Methods that can be called only by admin are:
mint(address: PublicKey, amount: UInt64)
setVerificationKey(verificationKey: VerificationKey)
setTotalSupply(amount: UInt64)
Helper methods for reading state and fetching account balance
getBalanceOf(address: PublicKey)
getTotalSupply()
getCirculatingSupply()
getDecimals()
That completes a review of a basic token.
Create and deploy a custom token
To create a token manager smart contract, inherit your smart contract from base custom token implementation
import {
FungibleToken
} from 'mina-fungible-token';
class MyToken extends FungibleToken {}
To deploy a token manager contract, create and compile the token contract instance, then create, prove and sign the deploy transaction:
const {
privateKey: tokenKey,
publicKey: tokenAccount
} = PrivateKey.randomKeypair();
const token = new MyToken(tokenAccount);
// paste the private key of the admin account here
const tokenAdminKey = PrivateKey.fromBase58('...');
const tokenAdminAccount = PublicKey.fromPrivateKey(tokenAdminKey);
const totalSupply = UInt64.from(21000000);
const tokenSymbol = 'MYTKN';
const tx = await Mina.transaction(deployerAccount, () => {
token.deploy(tokenAdminAccount, totalSupply, tokenSymbol);
});
tx.sign([deployerKey, tokenKey]);
await tx.prove();
await tx.send();
For this and following samples to work, make sure you have enough funds on deployer and admin accounts.
A full copy of the MyToken.ts is provided.
Token Operations
In this section, we will explore the various token operations represented by the standard, which include:
- Minting
- Burning
- Transferring between users
- Building zkApps that interact with tokens
Mint tokens
To mint tokens to some address:
// paste the address where you want to mint tokens to
const mintTo = PublicKey.fromBase58('');
const mintAmount = UInt64.from(1000);
const tx = await Mina.transaction(tokenAdminAccount, () => {
token.mint(mintTo, mintAmount);
});
tx.sign([tokenAdminKey]);
await tx.prove();
await tx.send();
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.
Burn tokens
To burn tokens owned by some address:
// paste the address where you want to burn tokens from
const burnFrom = PublicKey.fromBase58('');
const burnAmount = UInt64.from(1000);
const tx = await Mina.transaction(burnFrom, () => {
token.burn(burnFrom, burnAmount);
});
tx.sign([burnFromKey]);
await tx.prove();
await tx.send();
Transfer tokens between user accounts
To transfer tokens between two user accounts:
/// paste the private key of the sender and the address of the receiver
const sendFrom = PublicKey.fromBase58('...');
const sendFromKey = Private.fromPublicKey(sendFrom);
const sendTo = PublicKey.fromBase58('...');
const sendAmount = UInt64.from(1);
const tx = await Mina.transaction(sendFrom, () => {
token.transfer(sendFrom, sendTo, sendAmount);
});
tx.sign([sendFromKey]);
await tx.prove();
await tx.send();
Build zkApp that interact with tokens
Implement a smart contract that use tokens
With zkApps, you can also build smart contracts that interact with tokens. For example, a simple escrow contract, where tokens can be deposited to and withdrawn from.
Transfer from user to smart contract
Transfer from contract to user
Transfer from contract to contract
Implement custom mechanics
Conclusion
You have finished reviewing the steps to build a smart contract to manage a token. You learned how to build a smart contract that places custom rules over tokens.
To learn more, see Fungible token standard.
Check out Tutorial 9: Recursion to learn how to use recursive ZKPs with o1js, to implement zkRollups, large computations, and more.