o1-labs / o1js

TypeScript framework for zk-SNARKs and zkApps
https://docs.minaprotocol.com/en/zkapps/how-to-write-a-zkapp
Apache License 2.0
495 stars 108 forks source link

Invalid fee excess error on minimal Custom Token contract #1297

Open emlautarom1 opened 9 months ago

emlautarom1 commented 9 months ago

We're currently experimenting with Custom Tokens (https://docs.minaprotocol.com/zkapps/o1js/custom-tokens) as a possible solution for on-chain state. Our goal is to have a single SmartContract which mints a special token per user, which is unique and untransferable.

Our current code looks like this:

import { DeployArgs, Permissions, SmartContract, UInt64, method } from "o1js";

export const MINTEXAMPLE_TOKEN_NAME = "ZKTST"

export class MintExample extends SmartContract {
  deploy(args: DeployArgs) {
    super.deploy(args);

    const permissionToEdit = Permissions.none();

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

  @method init() {
    super.init();
    this.account.tokenSymbol.set(MINTEXAMPLE_TOKEN_NAME);
  }

  @method submitSecret(secret: UInt64) {
    secret.assertEquals(UInt64.from(420));

    this.token.mint({
      address: this.sender,
      amount: 1,
    });
  }
}

Here, if the user provides the correct secret, then we should mint a single unit of a Custom Token "ZKTST".

For testing, we're using the following code which is similar to the default Add.test.ts test:

import { AccountUpdate, Mina, PrivateKey, PublicKey, UInt64 } from 'o1js';
import { MintExample } from './MintExample';

let proofsEnabled = false;

describe('MintExample', () => {
  let deployerAccount: PublicKey,
    deployerKey: PrivateKey,
    senderAccount: PublicKey,
    senderKey: PrivateKey,
    zkAppAddress: PublicKey,
    zkAppPrivateKey: PrivateKey,
    zkApp: MintExample;

  beforeAll(async () => {
    if (proofsEnabled) await MintExample.compile();
  });

  beforeEach(() => {
    const Local = Mina.LocalBlockchain({ proofsEnabled });
    Mina.setActiveInstance(Local);
    ({ privateKey: deployerKey, publicKey: deployerAccount } = Local.testAccounts[0]);
    ({ privateKey: senderKey, publicKey: senderAccount } = Local.testAccounts[1]);
    zkAppPrivateKey = PrivateKey.random();
    zkAppAddress = zkAppPrivateKey.toPublicKey();
    zkApp = new MintExample(zkAppAddress);
  });

  async function localDeploy() {
    const txn = await Mina.transaction(deployerAccount, () => {
      AccountUpdate.fundNewAccount(deployerAccount);
      zkApp.deploy({});
    });
    await txn.prove();
    await txn.sign([deployerKey, zkAppPrivateKey]).send();
  }

  it('correctly mints a new token when the secret is correct', async () => {
    await localDeploy();

    const txn = await Mina.transaction(senderAccount, () => {
      zkApp.submitSecret(UInt64.from(420));
      // zkApp.sign(zkAppPrivateKey); Listed in https://docs.minaprotocol.com/zkapps/o1js/custom-tokens#signature-authorization and deprecated
      // zkApp.requireSignature(); Suggested as a replacement for `zkApp.sign(PrivateKey)`
    });
    await txn.prove();
    await txn.sign([senderKey]).send();
    // await txn.sign([senderKey, zkAppPrivateKey]).send();

    let newBalance = Mina.getBalance(senderAccount, zkApp.token.id).value.toBigInt();
    expect(newBalance).toEqual(1);
  });
});

Unfortunately, this test fails with the following error

    Invalid fee excess.
    This means that balance changes in your transaction do not sum up to the amount of fees needed.
    Here's the list of balance changes:

    Account update #1) 0.00 MINA
    Account update #2) not a MINA account

    Total change: 0.00 MINA

    If there are no new accounts created in your transaction, then this sum should be equal to 0.00 MINA.
    If you are creating new accounts -- by updating accounts that didn't exist yet --
    then keep in mind the 1.00 MINA account creation fee, and make sure that the sum equals
    -1.00 times the number of newly created accounts.

    Raw list of errors: [[],[["Cancelled"]],[["Invalid_fee_excess"]]]

      at raise_error (ocaml/js_of_ocaml-compiler/runtime/jsoo_runtime.ml:110:3)
      at apply_json_transaction (src/lib/snarkyjs/src/bindings/ocaml/lib/local_ledger.ml:252:9)
      at Object.sendTransaction (o1js/src/lib/mina.ts:491:16)
      at sendTransaction (o1js/src/lib/mina.ts:1202:10)
      at Object.send (o1js/src/lib/mina.ts:325:16)
      at Object.<anonymous> (src/MintExample.test.ts:45:5)

Note that we're not creating any new accounts (at least not intentionally), so the sum matches the expected amount (0.00 MINA). We've commented out things we've tried with no success. We've verified that the senderAccount has a balance of 1000000000000n MINA through Mina.getBalance(senderAccount).value.toBigInt(), so being unable to pay fees does not seem to be the reason.

emlautarom1 commented 9 months ago

It's important to note what we actually are looking for: a unique SmartContract that mints a single, untransferable token per user given some input which has to satisfy a predicate.

emlautarom1 commented 9 months ago

Another thing we've tried is removing all permission configuration from the SmartContract:

import { SmartContract, UInt64, method } from "o1js";

export const MINTEXAMPLE_TOKEN_NAME = "ZKEML"

export class MintExample extends SmartContract {
  @method submitSecret(secret: UInt64) {
    secret.assertEquals(UInt64.from(420));

    this.token.mint({
      address: this.sender,
      amount: 1,
    });
  }
}

This also results in the same Invalid fee excess error

mitschabaude commented 9 months ago

@emlautarom1 you are creating a new account because one account can hold only one kind of token. So your transaction is creating the account with public key = this.sender, tokenId = zkApp.token.id.

check out our docs on token accounts: https://docs.minaprotocol.com/zkapps/o1js/custom-tokens#token-accounts

image

emlautarom1 commented 9 months ago

According to the docs:

Suppose a token account is being created for the first time. In that case, an account creation fee must be paid similarly to creating a new standard account.

In this case then a token account is created from the existing senderAccount, and I would assume that senderAccount should pay the account creation fees using MINA. If so, then why I cannot pay the creation fees? Or is it that I need to explicitly list the fee as part of the transaction (something like the following)?

const txn = await Mina.transaction(senderAccount, () => {
      zkApp.submitSecret(UInt64.from(420));
      // Explicitly pay fees for account creation of `senderAccount` for a specific Custom Token
      senderAccount.payFee(Mina.accountCreationFee()); // not valid code
    });

I also found this example which pays fees for the account creation, but it's part of the SmartContract code:

https://github.com/o1-labs/docs2/blob/2b34d37f533a5a67505d4aa27bc116e7cb12971c/examples/zkapps/11-advanced-account-updates/src/WrappedMina.ts#L29-L41

This does not apply to our use case because then the SmartContract would be paying the fees (as far as I understand).

mitschabaude commented 9 months ago

@emlautarom1 yes fee paying is an explicit action. the example snippet in your link works also when replacing this with any AccountUpdate.

There's also a single command to create an account update which pays an account creation fee:

AccountUpdate.fundNewAccount(senderAccount);