Skip to main content
info

Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.

Tutorial 11: Advanced Account Updates

In the last tutorial, you learned the structure of account updates and how zkApp transactions are composed of account updates.

In this tutorial, you learn about more advanced features of account updates: tokens, zkApp manager accounts, and account update assertions.

To review the different types of accounts:

  • A zkApp account is a smart contract account.
  • A zkApp manager account is a specific type of smart contract that manages something; for example, a token.

zkApp Manager Accounts and Tokens

Each token is associated with a zkApp manager account. The manager account determines rules for token minting, burning, and transfer.

Internally, it's more general. The zkApp manager account for a token actually controls all properties of token accounts.

For an account update to be able to modify state (including sending tokens) on a token account, two things must be true:

  1. The account update must meet the permissions criteria on the zkApp account itself.

    If the zkApp Account is set up with the proof permission, the account update proof must pass the verification key in the account.

  2. The account update must set mayUseToken to a truthy value (a value that evaluates to true in a Boolean context).

    However, for this to happen, a transaction must get its account update approved by the corresponding zkApp manager account. The zkApp manager account can approve accounts based on its access permission.

zkApp Token Management

For example, you can write a token that can approve a zkApp account to send tokens from itself to another zkApp.

To implement, you need three contracts:

  1. MyToken: The token contract. On instantiation, it serves as the zkAppManagerContract for all instances of the token.
  2. TokenUser: A zkApp contract that manages tokens. This contract is instantiated on the Mina (default) tokenId.
  3. TokenHolder: A zkApp contract that stores tokens on behalf of the TokenUser contract. This contract is instantiated on the MyToken instance of tokenId.

The full code is provided in the examples/zkapps/11-advanced-account-updates/ folder.

As shown in the main.ts example file, a token standard library abstracts many of these details away for standard use. This tutorial helps you understand what the library is doing and how to think about a zkApp manager account.

Set up the contracts as follows:

const myTokenInstance = new MyToken(myTokenAddr);
const tokenUserInstance = new TokenUser(tokenUserAddr);
const tokenHolderInstance = new TokenHolder(
tokenUserAddr,
myTokenInstance.token.id
);

And deploy:

const deploy_txn = await Mina.transaction(deployerAddr, async () => {
let feePayerUpdate = AccountUpdate.fundNewAccount(deployerAddr, 4);
await feePayerUpdate.send({ to: myTokenAddr, amount: accountFee });

await myTokenInstance.deploy();
await tokenUserInstance.deploy();
await tokenHolderInstance.deploy();

await myTokenInstance.approveDeploy(tokenHolderInstance.self);
});

Note the myTokenInstance.approveDeploy() method. Because tokenHolderInstance is going to exist as a token account, it needs approval to be deployed.

The approveDeploy() code is as follows:

class MyToken extends SmartContract {
// ...

@method async approveDeploy(deployUpdate: AccountUpdate) {
this.approve(deployUpdate, AccountUpdate.Layout.NoChildren);

// check that balance change is zero
let balanceChange = Int64.fromObject(deployUpdate.body.balanceChange);
balanceChange.assertEquals(Int64.from(0));
}
}

This checks that the deployUpdate is a single account update, with no children, and that its balance change is zero.

The balance change check is essential: It means the account update isn't creating any additional tokens. Without the check, a user could pass in an account update with a positive balance change, which would mint tokens to its account out of thin air.

Conversely, note that a user can call this method with any account update they like, if it only has a balance change of zero. For example, a user could make their 8-field on-chain state whatever they want. In other words, the balance change is the only property of token accounts that the token manager contract chooses to "manage".

note

The approval mechanism gives you (the developer) complete freedom in encoding constraints on account updates in your manager contract logic.

The fungible token use case is just one example of possibilities. For example, a different zkApp manager contract could store NFTs (or a commitment to NFTs) in the on-chain state of user accounts. In that case, the on-chain state would be the meaningful part of token accounts. The manager contract wouldn't care about constraining the balance change. Instead, the approval logic contains checks that the on-chain state is updated in a certain way:

this.approve(deployUpdate, AccountUpdate.Layout.NoChildren);

// check that the deploy update doesn't modify on-chain state
deployUpdate.body.update.appState.every((x) => x.isSome.assertFalse());

Next, here is an example of sending tokens from the tokenUser/tokenHolder to another account:

const txn2 = await Mina.transaction(deployerAddr, async () => {
let feePayerUpdate = AccountUpdate.fundNewAccount(deployerAddr, 1);
await tokenUserInstance.sendMyTokens(UInt64.from(100), deployerAddr);
});

The fundNewAccount call is included as the deployer account. In this case, it doesn't have an existing token account.

tokenUserInstance.sendMyTokens(...) is implemented as follows:

  @method async sendMyTokens(
amount: UInt64,
destination: PublicKey
) {
const tokenHolder = this.tokenHolder;
tokenHolder.transferAway(amount);
this.tokenContract.approveTransfer(tokenHolder.self, destination);
}

Which calls the transferAway() method on tokenHolder:

  @method async transferAway(amount: UInt64) {
// TODO: in a real zkApp, here would be application-specific checks for whether we want to allow sending tokens

this.balance.subInPlace(amount);
this.self.body.mayUseToken = AccountUpdate.MayUseToken.ParentsOwnToken;
}

And is approved by the token contract's approveTransfer:

  @method async approveTransfer(transferUpdate: AccountUpdate, receiver: PublicKey) {
this.approve(transferUpdate, AccountUpdate.Layout.NoChildren);

let balanceChange = Int64.fromObject(transferUpdate.body.balanceChange);

// assert that the balance change is negative
balanceChange.isPositive().not().assertTrue();

// move the same amount to the receiver
this.token.mint({ address: receiver, amount: balanceChange.magnitude });
}

The account update is for the fee payment and the main tree that does the token operations. The call to tokenUser is followed by a call to myToken that does the approving.

This transaction approves two account updates:

  • Takes tokens away from the tokenUser
  • Sends tokens to the deployer account

Both updates have the mayUseToken field set to parentsOwnToken.

Because of the logic in the myToken approval account, the balances must match between these two account updates. Which they do, so a valid proof can be constructed.

A token standard library abstracts many of these details away for standard use.

Conclusion

Congratulations! You have explored some of the advanced features of account updates and are equipped to build with token zkApp manager accounts.

To see how to apply token managers in a more complex example, see the wrappedMinaToken implementation.