Introducing the Fungible Token Contract

Intro

We’re excited to introduce a new Fungible Token Contract, currently released in beta. This new configurable contract simplifies token deployments, reduces the number of account updates needed for common actions, and lays the groundwork for powerful customizations while preserving properties like a static verification key, which is required for cross-application compatibility.

Motivation

The original token standard relied on both a FungibleToken and a FungibleTokenAdmin contract. This added complexity, complicated deployments, increased account updates, and introduced trust assumptions around the admin.

The new design addresses these issues by:

  • Using a single contract with built-in configurability

  • Preserving a static verification key for auditability and composability

  • Supporting flexible logic via sideloaded proofs

This makes it easier to reason about token behavior while enabling more advanced use cases like gated minting and ZK-powered airdrops.

Key Features

Single Contract Design

Combines token and admin functionality, eliminating the need for an external admin contract. This reduces one account update per method (e.g. mint) and simplifies deployment.

Sideloaded Proof Support

Advanced conditions (e.g. “only mint if the user owns 1,000 MINA”) can be enforced using proofs that live outside the token contract. These do not alter the token’s verification key and support privacy-preserving logic.

Static Verification Key

All logic lives in a single, auditable contract.

Design of the current Fungible Token Standard (mint)

Account Updates for mint (7 total):

  1. feePayor
  2. fundNewAccount - only if a tokenID account does not exist
  3. FungibleToken.mint() zkApp method
  4. FungibleTokenAdmin.canMint() zkApp method
  5. Admin Signature
  6. Token ID balance change
  7. Token ID balance change

Design of new Fungible Token Contract (mint)

Account Updates for mint (6 total):

  1. feePayer
  2. fundNewAccount - only if a tokenID account does not exist
  3. FungibleToken.mint() zkApp method
  4. Admin Signature
  5. Token ID balance change (mint)
  6. Token ID balance change (mint)

Conclusion

The new Mina Fungible Token Contract is a cleaner, more composable foundation for token-based apps. It preserves auditability with a static verification key, reduces deployment overhead, and enables advanced features like privacy-preserving mint conditions with a single contract.

We invite developers to contribute and collaborate with us as we build on this foundation and evolve the contract together in the future. If you’re building zkApps that involve tokens, we encourage you to try it and let us know what you need next.

6 Likes

Here we go again.

Good to see development on token side.

1 Like

Let me start by saying how much I appreciate the work on the new token contract standard! I believe it’s one of the most important improvements to the current devX on Mina.
The ability to provide conditional logic in each of the core methods unlocks a lot of functionality.

I wanted to ask about the permission model for side‑loaded verification keys. I believe functionality similar to regular smart‑contract verification‑key upgradability would be very useful. Let me give an example:

Let’s assume there is conditional logic requiring a side‑loaded proof to trigger mintWithProof. As part of contract initialization, updateSideLoadedVKeyHash was called with vkMap and OperationKeys.Mint, so to mint, a proof with vk.A is required.

Going forward, if the required side‑loaded VK is upgraded (vk.A → vk.B), the admin currently has full unrestricted control to change that key.

  • How could one guard an upgrade of the SideLoadedVKey—for example, via multisig, or another config, or logic like Permissions.VerificationKey.proofDuringCurrentVersion()—to give a more “permissionless” flavor?
  • Would it be possible to make the SideLoadedVKey immutable?

On a separate note, I’m not sure I follow the API decision to throw on transfer and use transferCustom. transferWithProof would fit nicely with mintWithProof and burnWithProof—why not transfer and transferWithProof?

2 Likes

I like the new design choices, however I think there are few technical details that should be figured out.

  1. Why use multiple ZkPrograms with one method, instead of one ZkProgram with multiple methods? The PublicOutputs struct could hold the enum for each operation, and the token contract methods would constrain the correct operation. I find this API design simpler and easier to use, but it’s a personal preference.
  2. Are these account updates actually constrained? I don’t know about the o1js magic behind the scenes, but from a quick glance it looks like the minaAccountUpdate.publicKey == publicInput.address is not actually enforced. Not only that, but I don’t even see them being added as children anywhere. I guess audit should find that, but before counting account updates, it should be known that there might be some missing.
  3. I don’t like the PublicOutputs struct design.
    • Why are there balances and nonces, when there are already the whole account updates which contain preconditions?
    • AFAIK the public inputs/outputs are fairly expensive and the account update has ~160 fields. This doesn’t show up now, because they are probably not even constrained, however if all the fields are part of the public output (I might be wrong on this, maybe o1js hashes the account update already), it should be changed. Ideally it should include a field of forest hash, which allows you to do any account updates, not just the mina and token account update. The auxiliary output is there for this exact usecase, where you output only the hash and to be able to use it outside, you also return the whole structure as aux.
    • so my expectation would be something like
    publicOutput: { forestHash: Field, method: OperationEnum }
    auxiliaryOutput: CallForest
    
2 Likes

Great proposal!

I really like the direction of this new Fungible Token Contract.
:white_check_mark: The single-contract design feels cleaner and reduces overhead.
:white_check_mark: Keeping a static verification key while adding sideloaded proof support strikes a strong balance between auditability and flexibility.
:white_check_mark: This clearly lays the groundwork for more advanced zkApp use cases (like gated minting or private airdrops) without adding trust assumptions.

That said, I do have a few questions and points of curiosity:

  1. Migration path:

    • For teams that already deployed tokens under the previous standard, will there be a clear migration tool or recommended approach?
  2. Developer accessibility:

    • While sideloaded proofs unlock powerful use cases, how do you envision making them approachable for less experienced developers? Will there be templates or simplified SDK support?
  3. User experience:

    • Reducing account updates is great — but are there any measurable impacts expected on fees or speed for token actions (mint, transfer) with this new design?
  4. Adoption roadmap:

    • Are there plans to release example integrations or “starter kits” to help builders transition quickly to this new standard?

Overall, this feels like an important step toward making token interactions on Mina more composable and privacy-ready.
Excited to see how this evolves and would love to hear thoughts on the questions above!

2 Likes

Thanks for the feedback Karol! I agree that the ability to upgrade side-loaded verification keys in a more controlled way would be very useful. With the current standard, updating the side-loaded VK hash requires a transaction authorized by a permissioned signer (typically the owner/admin). Because of this, it’s not currently possible to use solutions that require authorization with proofs.
If we had native support for threshold signature schemes like FROST or similar primitives, we could move toward a more permissionless approach without requiring any change but in the meantime, we could consider introducing a permission model for updateSideLoadedVKeyHash, similar to what’s already done for side-loaded proof configs.

Here’s an idea: we can have a state variable like vKeyUpdatePermission, with three possible modes:

  1. Admin (0) – requires an admin signature (default)
  2. Proof (1) – requires a valid zkApp proof (e.g., for multisig logic)
  3. Impossible (2) – disallows updates, making the VK immutable

Below is an example of such logic:

/**
 * Determines if the side-loaded VKey can be updated based on permission configuration.
 * 0: Admin signature required
 * 1: Proof required (for multisig or other zk-based authorization)
 * 2: Impossible (immutable)
 */
private async canChangeSideLoadedVKey(
  _vKey: VerificationKey,
  _operationKey: Field
): Promise<Bool> {
  const permission = this.vKeyUpdatePermission.getAndRequireEquals();

  const isAdminPermission = permission.equals(UInt8.from(0));
  const adminAuthorized = await this.ensureAdminSignature(isAdminPermission);

  const isProofPermission = permission.equals(UInt8.from(1));
  const currentAccountUpdate = AccountUpdate.create(this.address);
  const hasProofAuthorization = currentAccountUpdate.authorization.isProved;

  const isImpossible = permission.equals(UInt8.from(2));

  return Provable.switch(
    [isAdminPermission, isProofPermission, isImpossible],
    Bool,
    [Bool(true), hasProofAuthorization, Bool(false)]
  );
}

Regarding API consistency, the fungible token contract extends the native token contract, which defines core methods (e.g., approve, transfer) that cannot be modified to accept additional parameters like those required for side-loaded proofs. To work around this limitation, we use the custom keyword to define alternative methods (e.g., transferCustom) that support the required functionality. The original transfer and approve methods are intentionally overridden to throw errors, explicitly guiding developers to use their corresponding custom versions instead.

  1. The ZkProgram you mention is in the examples folder and serves as a template for developers to follow. It doesn’t matter whether it includes one or multiple methods as long as the structure of the public outputs remains the same. That’s the important part because it’s what allows a side-loaded ZkProgram to be correctly bound to the fungible token contract.
    That said, in my personal opinion using a separate ZkProgram for each operation provides a few practical advantages for token contracts. It helps isolate custom logic per operation which makes maintenance and upgrades easier. It can also help avoid potential issues related to WASM memory usage when combining complex custom logic under a single circuit. I also think including multiple methods means a proof could correspond to any one of those methods, which may make it more difficult for the admin to review and approve. It introduces ambiguity about what exactly is being proven.

  2. Could you please point out where you saw that public key constraint? I don’t think we have any code like that in the codebase.The account updates in the ZkProgram follow a two-stage verification approach. Let me explain with an example:
    "If account has balance X, then allow mint"

    • Stage 1 (Inside the ZkProgram):

      At this stage, the program doesn’t yet verify whether the inputs match actual on-chain state. You can use arbitrary or even incorrect values to generate a proof that still passes this stage.

    • Stage 2 (Inside the token contract / main zkApp method):

      Validates that the proof corresponds to the current chain state via preconditions on actual account updates.

      So if the balance has changed between proof generation and verification, the contract will reject the proof with an error like: "MINA balance changed between proof generation and verification".

    You can test this by modifying the token balance in the proof input and calling mintWithProof. It will fail if the on-chain state doesn’t match. Feel free to try it out in src/examples/side-loaded/. Here’s a useful Gist for reference: Mina Fungible Token Standard Malicious Example · GitHub

    You can also check out some test cases such as: fungible-token-contract/src/test/burn.test.ts at 9d33cc35821ffa279baad3b693f6ec80f974c964 · o1-labs-XT/fungible-token-contract · GitHub

    It’s also important to keep in mind that preconditions only apply to attached account updates. That means for token standards, if we want optional preconditions (e.g., on balance), we need to add additional account updates. Technically, setting a precondition in a zkApp method requires an account update to be present for that account. If there’s a requireEquals on a detached account update, the check will not run unless it’s attached to the transaction.

  3. We include balances and nonces in PublicOutputs to support broader use cases where this data might be needed in downstream zkApps. if it’s not needed, these fields can hold dummy values. this doesn’t affect dynamic proof compatibility or the FTS verification key, so custom assertions in the ZkProgram won’t break verification as long as the public output format stays consistent. The PublicOutputs struct should be inclusive by design in order to bind the ZkProgram to the FTC. If it were to be minimized, it would be a considerable compromise on what can be proved with side-loaded proofs.

Thanks for the feedback Martin!

  1. We’re already in close contact with several teams and actively discussing these topics to ensure smooth adoption and integration.

  2. We have examples of side-loaded operations in the repo here:
    fungible-token-contract/src/examples/side-loaded at main · o1-labs-XT/fungible-token-contract · GitHub
    There’s also an example available on the new docs page:
    Sideloaded Verification Keys | o1Labs Documentation

  3. Measurable impacts like those you mentioned are expected with the protocol updates planned for the Fall 2025 hard fork.

  4. For a broader set of examples, you can check out the repo here:
    fungible-token-contract/src/examples at main · o1-labs-XT/fungible-token-contract · GitHub

Thanks for the feedback Jays!

  1. I think this would need the input from more people since it’s more about the personal preference and what people find easier. That said, isolation is I think pretty much the same, maintenance and upgrades I think are harder, hence the whole vkey map merkle tree here fungible-token-contract/src/FungibleTokenContract.ts at 9d33cc35821ffa279baad3b693f6ec80f974c964 · o1-labs-XT/fungible-token-contract · GitHub

    I also think including multiple methods means a proof could correspond to any one of those methods

    That would be solved by using the enum for each operation. No need for merkle trees would mean simpler maintenance and upgrades.

  2. Could you please point out where you saw that public key constraint?

    I didn’t and I think it’s an issue. The proof’s statement is public input with some address, and public output are account updates that should correspond to the same address. I don’t think that’s enforced anywhere, but I might be wrong.
    EDIT: I see now what you are doing there, those account updates are there just to fetch the account data using o1js. This isn’t the ideal design, you don’t have to add the whole account updates structure, just to witness two fields. My suggestion in third point would solve that.

    It’s also important to keep in mind that preconditions only apply to attached account updates.

    Yes, they are not attached. Your test is just not sufficient to actually exploit the contract, you have to engineer the o1js a little bit. Try applying following diff and run the test again, it doesn’t fail where it should gist:de32ba9787d3d38e36d2c91f7e9c6dd6 · GitHub . Notice that those changes didn’t change the vkey of the contract.

  3. We include balances and nonces in PublicOutputs to support broader use cases where this data might be needed in downstream zkApps.

    Having just the balance and nonce limits the use cases heavily. Let’s say you want to prove something about the state of some third party contract. You can’t. Having a forest hash is as general as it can be. For this use case, you could create a forest with the account update of a third party contract, and later attach it in to the transaction in the FTC method.

1 Like