# Tutorial 8: Custom Tokens

> Smart contract code to create and manage new tokens. Mina has native support for custom tokens.

Canonical URL: https://docs.minaprotocol.com/zkapps/tutorials/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/](https://github.com/MinaProtocol/docs2/tree/main/examples/zkapps/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](https://github.com/o1-labs/o1js/blob/main/src/lib/token.test.ts).

## Prerequisites

- Make sure you have the latest version of the zkApp CLI installed:

    ```sh
    $ npm install -g zkapp-cli
    ```

- Ensure your environment meets the [Prerequisites](/zkapps/tutorials#prerequisites) for zkApp Developer Tutorials.

The example project for this tutorial is tested with [o1js](https://www.npmjs.com/package/o1js) version `3.0.0` and Node.js 22.

## Create the project

1. Create or change to a directory where you have write privileges.
1. Create a project by using the `zk project` command:

  ```sh
  $ zk project 08-custom-tokens
  ```

  The `zk project` command has the ability to scaffold the UI for your project. For this tutorial, select `none`:

  ```
  ? Create an accompanying UI project too? …
    next
    svelte
    nuxt
    empty
  > none
  ```

## Prepare the project

1. Change to the project directory, delete the existing files, and create a new `src/BasicTokenContract` smart contract, and a `index.ts` file:

  ```sh
  $ cd 08-custom-tokens
  $ rm src/Add.ts
  $ rm src/Add.test.ts
  $ rm src/interact.ts
  $ zk file src/BasicTokenContract
  $ touch src/index.ts
  ```

1. Edit `index.ts` to export your new smart contract:

  ```ts title="src/index.ts"
  export { BasicTokenContract, tokenSymbol } from './BasicTokenContract.js';
  ```

  The example project also exports `WhitelistedTokenContract` from `index.ts`. See [Examples](#examples).

## Concepts

Before you read the contract, learn the three objects that every custom token on Mina uses: the token manager, the token id, and the token account. Then learn the approval mechanism, which is how the token manager controls every change to a token balance.

The [tokenConcepts.test.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/08-custom-tokens/src/tokenConcepts.test.ts) test file in the example project checks each statement in this section on a local blockchain.

### Token manager

The token manager is the zkApp account that owns a token. In o1js, you write a token manager as a class that extends `TokenContract`. The token manager can:

- Set a token symbol for its token, such as `MYTKN`. The token symbol is not unique. Two token managers can set the same symbol and still manage two different tokens.
- Mint tokens. Minting adds tokens to the balance of an account. In o1js, `this.internal.mint()` does this.
- Burn tokens. Burning removes tokens from the balance of an account. In o1js, `this.internal.burn()` does this, and it requires a signature from the account that loses the tokens. A balance cannot go below zero, so an account cannot burn or send more tokens than it holds.
- Approve transfers. Any account that holds the token can start a transfer, but the token manager must approve it. See [Approval mechanism](#approval-mechanism).

`TokenContract.deploy()` sets the `access` permission of the token manager account to `proofOrSignature()`, and requires that the account is new. Without this permission, anyone can mint the token without the token manager.

### Token id

A token id is a field element that identifies a token. The token id is derived from the public key of the token manager, so each token manager has exactly one token id. That is why the token symbol does not need to be unique.

To get the token id of a token manager, call `deriveTokenId()` on the contract:

```ts
const tokenId = token.deriveTokenId();
```

This is the same value as `TokenId.derive(token.address)`. The MINA token has the token id `TokenId.default`, which is `1`.

`TokenId.derive()` also takes the token id of the token manager account as a second argument, which is `TokenId.default` when you do not supply it. For a token manager that is a normal MINA account, like the one in this tutorial, the two calls give the same value.

### Token account

An account on Mina is identified by a public key _and_ a token id. A token account is an account whose token id is the id of a custom token. Its balance is in that token, not in MINA.

So one public key can have many accounts: one MINA account and one token account for each token that it holds. They are separate accounts. When you mint tokens to a public key, its MINA balance does not change.

To read a token balance, give the token id:

```ts
Mina.getBalance(address, tokenId); // the token balance
Mina.getBalance(address); // the MINA balance
```

A token account is created the first time that its public key receives the token. The public key does not need a MINA account. Creating a token account costs the account creation fee, the same as creating a MINA account. Some account in the transaction must pay it, usually with `AccountUpdate.fundNewAccount()`. If no account pays it, the transaction fails with `Invalid fee excess`. Later transactions to the same token account do not pay the fee again.

### Approval mechanism

Every change to a token account is an account update with that token id. The Mina protocol accepts such an account update only when it is a child of the token manager's account update in the same transaction, or a deeper descendant that inherits the token permission from its parent. A top-level account update that uses a custom token id fails with the error `Top-level account update can not use or pass on token permissions`. So a token holder cannot move tokens without the token manager, even with a valid signature.

The token manager approves account updates in two ways:

- Its own methods create the account updates, as `this.internal.mint()`, `this.internal.burn()` and `this.internal.send()` do. The rules in the method are the rules of the token.
- It approves account updates that other code created. `TokenContract` leaves the `approveBase()` method for you to define. It receives the account updates as an `AccountUpdateForest` and must fail if it does not approve them. The helpers `transfer()`, `approveAccountUpdate()` and `approveAccountUpdates()` all put the account updates into a forest and call `approveBase()`.

`checkZeroBalanceChange()` is the most common implementation of `approveBase()`. It proves that the token balance changes of the approved account updates sum to zero, so an approved transaction cannot create tokens. `approveBase()` can check at most `TokenContract.MAX_ACCOUNT_UPDATES` account updates, which is `9` by default.

Make `approveBase()` a `@method`, so that the token manager can prove its approval. If `approveBase()` is not a `@method`, `transfer()` does not add the token account updates to the transaction, and the transaction moves no tokens. When `approveBase()` is a `@method`, any token holder can call `transfer()`, and signs only for the account that sends the tokens:

```ts title="src/tokenConcepts.test.ts"
const tx = await Mina.transaction(alice, async () => {
  AccountUpdate.fundNewAccount(alice); // only if bob has no token account yet
  await token.transfer(alice, bob, UInt64.from(400));
});
await tx.prove();
await tx.sign([alice.key]).send();
```

## Basic Token Example

To create a token manager smart contract, create a normal smart contract whose methods call special functions that manipulate tokens.

A full copy of the [BasicTokenContract.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/08-custom-tokens/src/BasicTokenContract.ts) is provided.

### Imports and smart contract structure

First, bring in imports and set up the structure for the smart contract.

The single state variable `totalAmountInCirculation` tracks how many tokens exist.

```ts title="src/BasicTokenContract.ts"
import {
  AccountUpdateForest,
  DeployArgs,
  method,
  Permissions,
  PublicKey,
  Signature,
  State,
  state,
  TokenContract,
  UInt64,
} from 'o1js';

const tokenSymbol = 'MYTKN';

/**
 * A token manager: a smart contract whose methods create the account updates
 * that mint and move a custom token.
 *
 * It extends `TokenContract` rather than `SmartContract`, which is what
 * supplies `this.internal.mint` / `.send` and the `Approvable` API.
 */
export class BasicTokenContract extends TokenContract {
  /** How many tokens exist. */
  @state(UInt64) totalAmountInCirculation = State<UInt64>();

  async deploy(args?: DeployArgs) {
    await super.deploy(args);

    const permissionToEdit = Permissions.proof();

    this.account.permissions.set({
      ...Permissions.default(),
      editState: permissionToEdit,
      setTokenSymbol: permissionToEdit,
      send: permissionToEdit,
      receive: permissionToEdit,
    });
  }
```

The contract extends `TokenContract`, not `SmartContract`. `TokenContract` is the
o1js base class for a token manager: it supplies the `this.internal` helpers used
below, sets the `access` permission so that nobody can bypass this contract and
mint the token themselves, and requires one method that says which account
updates the contract approves.

### init() and mint() methods

Next, add an init method and a method that mints tokens.

- Set the token symbol (MYTKN) in the `init()` method.
- To start tracking the amount in circulation, set it to zero.
- Write a function to mint new tokens and send them to a recipient.

  This function checks that a signature has been provided by the zkApp account, so that only the zkApp account can call the `mint()` method.

- In the `mint()` method, track how many tokens are in existence.

  ```ts title="src/BasicTokenContract.ts"
  @method async init() {
    await super.init();
    this.account.tokenSymbol.set(tokenSymbol);
    this.totalAmountInCirculation.set(UInt64.zero);
  }

  /**
   * Approve the account updates of a transaction that moves this token.
   *
   * `checkZeroBalanceChange` proves that the balance changes of the approved
   * updates sum to zero, so a transaction cannot create tokens out of nothing.
   * Minting goes through `mint()` below, which is the only method that raises
   * the supply.
   *
   * It is a `@method`, so the contract can prove its approval. That is what
   * lets a token holder call the inherited `transfer()` and
   * `approveAccountUpdate()` helpers, which both end in `approveBase()`.
   */
  @method async approveBase(forest: AccountUpdateForest) {
    this.checkZeroBalanceChange(forest);
  }

  /**
   * Mint `amount` tokens to `receiverAddress`.
   *
   * The admin signature is over the amount and the receiver, checked against
   * this contract's own address, so only the holder of the zkApp key can mint.
   */
  @method async mint(
    receiverAddress: PublicKey,
    amount: UInt64,
    adminSignature: Signature
  ) {
    const totalAmountInCirculation = this.totalAmountInCirculation.getAndRequireEquals();
    const newTotalAmountInCirculation = totalAmountInCirculation.add(amount);

    adminSignature
      .verify(
        this.address,
        amount.toFields().concat(receiverAddress.toFields())
      )
      .assertTrue();

    this.internal.mint({ address: receiverAddress, amount });

    this.totalAmountInCirculation.set(newTotalAmountInCirculation);
  }
  ```

`approveBase()` is the method `TokenContract` leaves for you to define: it decides
which account updates this contract approves. `checkZeroBalanceChange()` proves
that the balance changes of the approved updates sum to zero, so a transaction
cannot create tokens out of nothing. It is a `@method`, so token holders can call
the inherited `transfer()` helper (see [Approval mechanism](#approval-mechanism)). `mint()` is the only method that raises the
supply, and it is gated by the admin signature.

### Send function

Finally, write a send function.

- Holders of the MYTKN token call the `sendTokens()` method to send tokens to other Mina accounts.

  ```ts title="src/BasicTokenContract.ts"
    /**
     * Send `amount` tokens from one account to another.
     *
     * Holders of MYTKN call this to move the token. The supply does not change,
     * so `totalAmountInCirculation` is untouched.
     */
    @method async sendTokens(
      senderAddress: PublicKey,
      receiverAddress: PublicKey,
      amount: UInt64
    ) {
      this.internal.send({
        from: senderAddress,
        to: receiverAddress,
        amount,
      });
    }
  }
  ```

A transfer moves tokens between accounts, so it does not change
`totalAmountInCirculation`.

That completes a review of a basic token.

## Examples

To see an example of interacting with this contract, see [main.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/08-custom-tokens/src/main.ts).

To see an example of putting rules around a token, see [WhitelistedTokenContract.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/08-custom-tokens/src/WhitelistedTokenContract.ts). Only the public keys on a whitelist can hold or move this token. The contract keeps only the hash of the whitelist in its on-chain state, and each method gets the whitelist from the caller and checks it against that hash.

### Rules in approveBase()

Any account can call a `@method`. So when `approveBase()` is a `@method`, it must enforce every rule of the token. If it only calls `checkZeroBalanceChange()`, a token holder can use `transfer()` to send tokens to an address that `sendTokens()` refuses.

`WhitelistedTokenContract.approveBase()` uses `forEachUpdate()`, which `checkZeroBalanceChange()` also uses, to check each account update that uses the token. It also requires that the public key of each of these account updates is on the whitelist:

```ts title="src/WhitelistedTokenContract.ts"
@method async approveBase(forest: AccountUpdateForest) {
  // The whitelist is a private input that approveBase() cannot take as an
  // argument. memoizeWitness() keeps the value from the transaction for the
  // proof.
  const whitelist = new Whitelist(
    Experimental.memoizeWitness(Whitelist, () => {
      if (this.whitelist === undefined) {
        throw Error('set `whitelist` on the contract before you approve updates');
      }
      return this.whitelist;
    })
  );
  whitelist
    .hash()
    .assertEquals(
      this.whitelistCommitment.getAndRequireEquals(),
      'the whitelist given is not the one this contract committed to'
    );

  let totalBalanceChange = Int64.zero;
  this.forEachUpdate(forest, (update, usesToken) => {
    usesToken
      .implies(whitelist.contains(update.publicKey))
      .assertTrue('the address is not on the whitelist');
    totalBalanceChange = totalBalanceChange.add(
      Provable.if(usesToken, update.balanceChange, Int64.zero)
    );
  });
  totalBalanceChange.assertEquals(0);
}
```

`approveBase()` cannot take the whitelist as an argument, because `TokenContract` sets its arguments. Thus the caller sets the `whitelist` property of the contract object before it calls `transfer()`. `Experimental.memoizeWitness()` keeps the value that the method used when it made the transaction, so that `tx.prove()` uses the same value:

```ts title="src/WhitelistedTokenContract.proofs.test.ts"
token.whitelist = Whitelist.from([alice, bob]);
const tx = await Mina.transaction(alice, async () => {
  await token.transfer(alice, bob, UInt64.from(100));
});
await tx.prove();
await tx.sign([alice.key]).send();
```

A transfer to an address that is not on the whitelist fails with `the address is not on the whitelist`.

## Building zkApps that interact with Tokens

With zkApps, you can also build smart contracts that interact with tokens. For example, swapping one token for another or taking deposits of Mina tokens.

For now, see this [example](https://github.com/o1-labs/o1js/blob/main/src/examples/zkapps/dex/dex.ts) of a zkApp implementing an AMM-based DEX.

## 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 [Custom Token API](/zkapps/writing-a-zkapp/feature-overview/custom-tokens).

Check out [Tutorial 9: Recursion](/zkapps/tutorials/recursion) to learn how to use recursive ZKPs with o1js, to implement zkRollups, large computations, and more.
