An idea about flexible payment of transaction fees by zkApps

Payment of transaction fees by zkApps means that the network fees (equivalent to the gas charged in ethereum) for transactions initiated by ordinary users are paid by smart contracts under the mina protocol. Users can make zkApps pay for their transactions by generating proofs.

Two main types of transaction fees paid by zkApps:

  1. zkApps pay network fees for any transaction initiated by the user.

  2. zkApps pay network fees for transactions associated with themselves.

Benefits and use cases of transaction fees paid by zkApps

  1. Developers of zkApps can build and run programs like “fee paying relayers” to pay for users, but this requires hosting the private key of their wallet used to pay on the server, and there is a risk of leakage of the private key. If zkApps can pay the fee through proof verification, it will be more secure.

  2. For some zkApps that focus on privacy transactions, supporting zkApps to pay for users can better protect users’ privacy.

  3. Create a non-custodial smart contract wallet with better user experience and no reliance on third-party centralized services.

  4. Developers or operators of zkApps can pay transaction fees for their users, lowering the threshold for users to use and experience.

  5. NFT issuers can make their issued NFTs transferable among users for free, and users can enter the NFT world without purchasing mina coins.

  6. Users can obtain the right to send transactions for free through the project’s tokens or NFT credentials, and zkApps can also consume the user’s project tokens or NFTs to pay for users, increasing the utility of the project’s issued tokens or NFTs.

  7. Developers can set up zkApps to accept donations from anyone to pay for user transactions, which ensures that donations can only be used to pay for network fees.

And more…

Ideally, users can generate proof by executing the payment method in the smart contract, and let the smart contract pay for the user’s transaction through the authorization of the proof. This method allows developers to freely define payment conditions and assertions.

A succinct example of snarkyjs pseudocode that can describe the above idea of paying fees is as follows:

class SimpleZkapp extends SmartContract {
  @state(Field) x = State<Field>();
  @State(Field) whitelistRoot = State<Field>();

  deploy() {
    this.x.set(Field(initialState));
    this.whitelistRoot.set(initialWhitelistRoot);
  }

  @method 
  update(y: Field) {
    let x = this.x.get();
    this.x.set(x.add(y));
  }

  // add a new decorator(payfee) for payment of fees.
  // This method determines whether to pay for any of the user's transactions by verifying 
  // that the sender of the transaction is in the whitelist.
  @payfee 
  payFeesForWhitelist(currentTx: Transaction, proof: MerkleTreeProof): UInt64 {
    const fromAddress = currentTx.from;
    currentTx.from.assertEqual(fromAddress);

    const whitelistRoot = this.whitelistRoot.get();
    // Verify that the sender's address is in the whitelist
    verifyProof(whitelistRoot, fromAddress, proof).assertEqual(true);

    // Set and return the allowable fee for each transaction
    return UInt64.fromNumber(100_000_000);
  }

  // This method allows network fees to be paid for any transaction that interacts with the current zkApp.
  @payfee 
  payFeesForUser(currentTx: Transaction): UInt64 {
    const toAddress = this.address;
    currentTx.to.assertEqual(toAddress);

    // Set and return the allowable fee for each transaction
    return UInt64.fromNumber(100_000_000);
  }
}

let simpleZkapp = new SimpleZkapp(xxxx);
// zkApps pay network fees for any transaction initiated by the user.
await Mina.transaction(() => {
        simpleZkapp.update(Field(10));
    }
  ).payFees(simpleZkapp, (currentTx: Transaction): UInt64 => {
    return simpleZkapp.payFeesForWhitelist(currentTx, proof);
  }).prove().send();

// zkApps pay network fees for transactions associated with themselves.
await Mina.transaction(() => {
    simpleZkapp.update(Field(10));
}
).payFees(simpleZkapp, (currentTx: Transaction): UInt64 => {
return simpleZkapp.payFeesForUser(currentTx);
}).prove().send();

4 Likes

I like this a lot. The main issue is whether there’s any chance to support this on the protocol level. Tagging @mrmr1993 as the expert on this topic.

AFAIU, this is out of scope to support within the next hard fork. There seems to be a fundamental conflict between two objectives:

  • Having anonymous transactions, not linked to a particular user account
  • Prevent replays of transactions

In normal transactions, we prevent replays by having a fee payer which signs the transaction, increments its account nonce, and adds a precondition on that nonce. Those measures ensure that the transaction can be only applied once to the ledger.

If a transaction should be anonymous, e.g. if a zkapp is paying fees for the user, then that implies there must be nothing on that transaction that refers to a user account. If there is nothing referring to a user account, we also can’t increase a user account nonce to prevent replays.

More generally: if the only thing accessible as part of that transaction is a zkapp account, then in a sense there is only “global” state (i.e., zkapp account state) that is available to us to prevent replays. And if we modify “global” state, then we introduce a race between different users sending transactions to the same zkapp. Say, if we increment the zkapp account nonce, then this would race with any other tx in the same block that does the same.

Any solutions / workarounds to this fundamental problem are very appreciated, of course!

For this kind of transaction where the user is completely anonymous, I haven’t thought of a solution for the time being (it seems that nullifier is a solution, but it affects the performance, the lightweight and scalability of the node).

But if the user’s address as the originator of the transaction is fully exposed in the transaction(Actually this address is not necessarily the user’s own address), this replay problem can be easily solved, right?
In addition, I can think of a scenario where zkApp needs to increase its nonce: zkApp acts as a smart contract wallet for a certain user, requiring no additional wallet interaction.