Fee payment discussion

Q: do we want to handle fee payments in a token-agnostic fashion or not?

Possible Token-agnostic fee model

Sequencers apply heuristic that 1st function call in a transaction is the “fee payment” transaction. They check it to see if/how much the tx will pay them.

Gas metering in a token-agnostic model

In the kernel circuit, the tx sender defines gasPrice and gasLimit values. The sequencer must validate these are consistent with the fee payment in the 1st transaction

Q: How do we handle refunds in this model if not all gas is used?

Depositing value into Aztec

On L1, a user calls the deposit function of a token portal contract. The portal will write a message into the L2 message box that allows an L2 deposit function to be executed.

The L2 deposit function will take a portion of the deposit value (Q: how much and how is this computed?) and send it to the sequencer; the remainder being added to the depositor’s balance.

The user makes an L2 tx that calls the deposit function on the token contract. As this is also the 1st (and only) function call in the tx, the sequencer will identify they are being paid.

i.e. depositing funds can work by following the existing above heuristics and using the message passing spec

Paying the sequencer

(idea from Mike)

We create an L2 contract that anybody can send funds to. The contract has a withdraw function that, when called, will send its funds to a designated address. The withdraw function asserts that msg.sender == coinbase.

This is useful because it enables the sequencer identity to remain private.

Users send their funds to the above contract. At the end of the block, the sequencer calls withdraw, which is a private function that does not leak their identity.

24 Likes

Q: How do we handle refunds?

If a user does not spend all their gas, we want to refund the user the difference, while preserving token-agnostic fees.

One way would be to have a ‘payGasRebate’ method in the fee-receiving contract in the above post.

The ‘payGasRebate’ method will use ‘msg.gas, msg.gasPrice’ to transfer tokens back to the tx sender.

Issues that need to be resolved:

1: this rebate tx needs to be at the end of the tx queue, which implies we need some kind of transaction context sequencing. I.e. a tx now consists of a minimum of 3 function calls: initial fee payment function, actual tx function, gas rebate function

It makes sense to treat these as independent txns. I.e. if tx 3 fails, tx 1 and 2 succeed and can still make state changes (i.e. sequencer still gets their fees if main fn call or rebate call fails. If rebate call fails user still has their main tx executed successfully)

2: need to ensure sequencer has a minimal number of checks to run before accepting a tx. This means it must be easy to validate the ‘gasRebate’ function is not called twice. They must be able to validate this by examining the initial call stack of the transaction.

By extension it would be useful to have a global variable that indicates whether a function is being called as a ‘top level’ function. I.e. the user added the function call onto the function stack directly and it was not created by another function call.

The gasRebate function would throw if not sent as a ‘top level’ function call so that the user cannot grief sequencers by burying multiple gasRebate calls deep into a sequence of other function calls, as the sequencer would not be able to detect this in O(1) time.

13 Likes

I am not entirely convinced that it is a large value-add to allow arbitrary tokens for fee-payments. If it substantially increases complexity, I’d say maybe it’s not worth building into the protocol itself. It feels like it just adds a lot of risk (oracles, shallow-liquidity tokens, tokens with spender-whitelists, …)

22 Likes

I wrote lots of related ideas in this ‘discussion’ hackmd which we chatted about last month.

https://hackmd.io/gs1Q_fOSRpyJWG3Sa_cwDQ#Escrow-the-max-fee-then-refund-unused-gas

Here’s the detail pasted (some of the thinking around paying L1 gas separately is slightly outdated).

It includes a discussion on refunds, and a discussion on private refunds, by ‘completing’ token commitments.

# Gas limits and gas prices.

A proposal

Here’s a simple proposal for spcifying gas info, which copies the Ethereum model:

Each tx that a user submits will contain the following fields:

{ 
    aztec_gas_limit: 50000,
    aztec_gas_price: 10, // Currency dictated by price_currency_id
    eth_gas_limit: 100000,
    eth_gas_price: 10,   // NOT NECESSARILY MEASURED IN GWEI!!!
                         // Currency dictated by price_currency_id
    // These allow kernel circuits to handle different choices:
    pay_fee_from_private_l2: true,
    pay_fee_from_public_l2: false,
    pay_fee_from_l1: false,
    signed_fee_tx: {
        contract_address,
        function_signature,
        arguments,
        // other stuff
        signature,
    }
}
  • The user specifies a separate aztec_gas_limit and eth_gas_limit, since eth_gas and aztec_gas measure different kinds of computation.
  • The user specifies a corresponding aztec_gas_price and eth_gas_price, as a way of specifying the amount they’re willing to pay for each unit of gas.
  • NOTICE: the eth_gas_price is not measured in traditional gwei:
    • Since we want users to be able to pay fees in any currency, we allow the user to specify the price in any currency.
    • The signed_fee_tx contains all the information the Sequencer needs to infer the currency in which they’ll be paid per unit of gas.
    • The Sequencer can check current exchange rates to determine whether these parameters will cover their cost of compute - both on Ethereum and inside the rollup.
  • The Kernel & Rollup circuits will be able read the specified aztec_gas_limit values to determine whether the user hasn’t specified a high enough gas limit.
  • The Kernel & Rollup circuits will be able to measure the actual aztec-gas used, by the tx, which can be used to deduct an exact amount (of whichever currency the user is paying in) from the user’s balance. I.e. we’ll need to deduct aztec_gas_used * aztec_gas_price from the user’s balance (by calling the relevant fee-paying contract).

But giving some Kernel (or Rollup) circuit permission to deduct value from someone’s balance within a custom token contract is a hard problem to solve…

Subtracting gas_used from someone’s balance

In Ethereum, the EVM has special permission to decrement a user’s ETH balance based on the eth_gas * eth_gas_price (measured in gwei) that their tx consumes. ETH is special in this regard, since such decrementation permissions (restricted to the party producing the block) are baked into the protocol. The protocol does the following:

  • Check the user’s ETH balances is >= eth_gas_price * eth_gas_limit.
  • Execute the tx.
  • Subtract eth_gas_price * eth_gas_used from the user’s ETH balance.
  • Transfer some of that ETH to the validator after burning some ETH in line with EIP-1559.

For us to support something similar for any asset type is more complicated. Different L2 token contracts might have different interfaces, depending on whether they’re for public or private tokens, and especially in the early days of the network when standard interfaces for tokens won’t yet have been agreed. In particular, a standard which requires token contracts to somehow give special permissions to the current sequencer’s address, so that they may decrement others’ balances under certain conditions sounds pretty sketchy.

Escrow the max fee, then refund unused gas

Instead of permitting a sequencer to decrement balances, we could require users to lock up a max fee in escrow (up to their chosen gas limit) before sending their txs. Sequencers could then refund any unspent gas after the amount of actual gas used has been measured.

We could do this with some standardised “Special Sequencer Escrow Contracts” - one escrow contract for each fungible token standard that exists on the network.

  • The user would send aztec_max_fee = aztec_gas_limit * aztec_gas_price to the appropriate escrow contract, which would be stored against an identifier for that tx, and an identifier for the currency being paid (i.e. the token contract’s address).
  • The sequencer would measure the aztec_gas_used of each tx in their rollup and collate this info. The info would be linked to a public input of the root rollup proof. - The sequencer would then be forced to append a special set of txs to the end of the rollup, which would call each escrow contract (there might be many such contracts if there are multiple fungible token standards on the network) to refund users.
    • The escrow contract would refund the users for any unused gas; an amount (aztec_gas_limit - aztec_gas_used) * aztec_gas_price. (See below for how to do this with private tokens).
    • The escrow contract would then allow the sequencer to withdraw all remaining tokens in the escrow contract.

Refunding private tokens, without learning user IDs

  • If fees are paid in Private L2 tokens, we don’t want the Sequencer to know who the user is, and so can’t as easily refund a particular address. Here’s what we could do:
    • The user executes a fee-paying tx which creates a token commitment of value aztec_gas_limit * aztec_gas_price - i.e. the maximum they’re willing to pay for their tx.
    • The user also submits a “partial commitment” (commitments would have to be additively homomorphic, like Pedersen), commiting to their address and a salt.
    • Once the Sequencer has executed the user’s tx and the aztec_gas_used is known, the protocol could force the Sequencer to pay a gas refund through a special escrow contract, by ‘completing’ the user’s partial commitment, by ‘adding’ a value equal to: (aztec_gas_limit - aztec_gas_used) * aztec_gas_price.
      I.e.:
    uint32_t max_fee = gas_limit * gas_price;
    
    // Might need more info in practice, but this is just illustrative.
    // A, B, C, D are elliptic curve points.
    Point max_fee_commitment = (contract_address * A) + (max_fee * B) +
                               (owner * C) + (salt_1 * D);
    
    Point refund_partial_commitment = (contract_address * A) + 
                                      (owner * C) + (salt_2 * D);
    
    uint32_t refund_amount = (gas_limit - gas_used) * gas_price;
    Point refund_commitment = (refund_amount * B) + refund_partial_commitment;
    

A single fee-paying token

A lot of the above headaches around standardising token interfaces - so that a Sequencer may be constrained to subtract-from or refund value back to users in a consistent way - disappear if we have a single token through which aztec-gas fees must be paid.

Imagine all aztec-gas must be paid through a single token; let’s call it the “Aztec Token”. Then this Aztec Token’s contract could be designed in such a way that the Sequencer could indeed charge exact aztec-gas fees to users. The Kernel and Rollup circuits could be designed based on that one interface.

But would such an Aztec Token be public or private? Indeed, we know such an Aztec Token might be needed as part of the sequencer-selection protocol, regardless of whether there’s a canonical fee-paying token…

(I’ll address some answers to topics raised by messages above, in a separate message in this thread)

12 Likes

One way I’ve convinced myself of the need for arbitrary tokens for fee payments is the following:

  • We know there will be use cases where users will want fees to be paid with private tokens, otherwise there’s no point building a private smart contract platform at all. Hiding the function logic without hiding who’s paying the fee is a bit inconsistent.
  • So we know we’ll need at least one private token through which fees can be paid.
  • But what would that ‘one’ private token look like (how ‘private’ would it be? what would commitment preimages contain? would it have compliance features? etc.?)
  • I’d suggest we’re too early to know what exact characteristics a private token should have.
    • Indeed, the requirements of a private token might differ, depending on different users’ ideologies, and by jurisdictions. Even the technology of how private tokens can be designed might change over the years.
    • Ethereum has shown that many different token standards come into being.
  • And then there might be use cases where someone wants to pay with a public token, which I haven’t considered above.

So based on the above, I’ve concluded (but open to discussion) that a single fee-paying token isn’t feasible on our L2; and we should support arbitrary functions through which value can be transferred, so standards can naturally evolve on the network.

28 Likes

Re the “Depositing Value into Aztec” section in the original post.

There’s a related question I wrote in the discourse thread about L1->L2 comms, which asks how a Sequencer would be paid for an L1->L2 call which adds a message to the message tree. This topic seems related to how a Sequencer would be paid for a deposit (also an L1->L2 call).

In my old specs, I suggest something similar to the following (updated to allow for the new L1->L2 comms):

  • At the time the L1->L2 message is added to the message queue (on L1, via the Rollup Contract), allow value to be escrowed in the Rollup Contract. It’d be up to the logic of the Portal Contract to ensure value can be transferred (either ETH or an ERC20) at the time of making the call.
  • The Rollup Contract would expose an addMessageAndPayFee function for this purpose.
  • The following could be stored in the Rollup Contract’s storage against a particular message:
    • inboxFee: uint256 ← the fee that’s been escrowed to pay to the Sequencer when they add this message to the inbox.
    • l2TxFee: uint256 ← the fee to pay to the Sequencer if they include a particular L2 tx in a rollup.
    • l2TxId: uint256 ← an identifier for the L2 tx for which the l2TxFee will be paid.
      This l2TxId could be a simple hash of the signedTxObject of the L2 function call. Note: the signedTxObject can always be computed in advance.
  • The Kernel Circuit spec already contains logic to optionally reveal certain data depending on certain boolean flags. One such boolean is payFeeFromL1, which forces the kernel circuit to reveal this l2TxId to L1 as calldata, so that the fee can be collected on L1.

Pros:

  • The L2 function being called doesn’t need to handle fee payment logic (it doesn’t even need handle money at all).
  • This allows non-financial apps to make L1->L2 calls, even if the user doesn’t-yet have any L2 funds.
  • Any message to the L1->L2 message box can make a payment to the Sequencer (see linked post which elaborates further on this possibly outstanding problem).
  • Any L2 function could be paid-for via this L1 payment mechanism, if the user doesn’t have funds on L2, giving further flexibility.
13 Likes

Mike what do yo think of the model I outlined at the top of this thread?

It has the benefit of not requiring any additional logic or mechanics in either the rollup contract logic or in the messaging specification

The only additional requirements to make it work are:

  1. Addition of a ‘transactionContext’ in the kernel circuit to enable a user to start a tx with >1 function call
  2. An extra global variable passed into user circuits that denotes whether the function has been directly called by the tx sender

I think the above changes can be used to handle arbitrary fee patients. Imo this architecture is even simpler than specifying a protocol token like Ethereum as we remove the following components:

  1. Public and private db of protocol token balances external to the smart contract do
  2. Custom functions and code paths to deposit value into and out of the L2
  3. Custom circuit logic to process a msg.value parameter and the Aztec-connect style database updates this would require
12 Likes

I think we can mitigate risk by writing v1.0 of the sequencer software to handle only Eth.

Adding the optionality of using other tokens adds a lot of value imo

For example we want A3 to be used as a private payments platform, one where a web browser could embed a simple wallet that uses USD tokens on the Aztec network for micropayments.

It removes significant complexity from app developers if their users can transact in the native currency of their app.

But the big question I’d like to validate is this:

At the protocol level, is the token agnostic model more complex than defining a protocol token that fees are paid in?

I don’t think it is. Consider the extra mechanics in the Ethereum protocol that we would need to duplicate:

  1. A database to manage user balances (both public and private in our case)
  2. Protocol-level logic to handle value transfers for every contract call (I.e. this is not delegated to a token contract, this requires a join-split transaction to be embedded into the private kernel circuit and an account-based transfer to be embedded into the public kernel circuit
  3. Additional code paths and logic to deposit tokens into Aztec from L1 (cannot just use plain messages as protocol token deposit messages must be processed by the kernel circuit and not a user circuit)
  4. Additional code paths and logic to process withdrawals from Aztec for the same reasons
12 Likes

To expand on my earlier message, the idea would be that sequencers can decide what fees they are willing to take.

Ideally social consensus develops around a few ‘canonical’ tokens that are widely accepted. I’m assuming this won’t include tokens that have whitelists or shallow liquidity.

IMO being token agnostic creates the following tradeoffs:

  • less complexity for users
  • less complexity for app developers
  • (probably) less protocol level complexity
  • more complexity for sequencers

Who do we want to optimise for here? Imo it’s not sequencers; they work for the protocol. app devs are our primary users; if we can make their lives easier without adding protocol complexity, isn’t that a net positive?

15 Likes

Okay I think you and @Mike have sufficiently convinced me. This is making a lot more sense now. Thanks guy!

12 Likes

Mike what do yo think of the model I outlined at the top of this thread?

I think the ‘Depositing Value into Aztec’ section works as a neat special case of paying for the L2 component of an L1->L2 call - specifically in the case where it’s a ‘common’ token that the Sequencer is happy to accept.

  • It’s just whether we want the protocol to support deposits of ‘uncommon’ tokens, or payments for other (non-financial) L1->L2 calls, or to pay for any L2 tx from L1?
    (More generality would be fairly simple to achieve (see above post)).

Yep, I think 3 ‘txs’ per tx works well, and having a global variable to indicate that it’s a top level function is a nice way of preventing double-rebate calls.

Edit: one could attack this by having tx 2 (the ‘main’ tx) be a call to the gas rebate function as well as having tx 3 be a call to the gas rebate function. They’re both technically ‘top level’ functions. So you could steal money from a ‘Gas Payment’ contract this way, by getting 2x the gas rebate.

As a similar alternative, a global variable could be the ‘txHash’* of tx 2 (the ‘main’ tx). This way, a ‘Gas Payment’ contract could:

  • store details of the max fee paid in tx 1, against the txHash; and
  • only allow one call to the gasRebate function, because it could identify whether the rebate for this txHash had already been sent.

*the txHash could be the hash of the TxObject, which the kernel circuit will be aware of when it validates the tx signature: https://github.com/AztecProtocol/aztec2-internal/blob/3.0/markdown/specs/aztec3/src/architecture/contracts/transactions.md#txobject


I was briefly wondering if there’s a problem if tx 3 has to be a public function.

But that’s ok, because a public function can ‘complete’ a commitment to return a rebate to an anonymous user.


13 Likes

Re the ‘Depositing Value into Aztec’ section.

I don’t think we need to consider anything other than these special cases.

For tokens that aren’t commonly taken as fee payments, the deposit flow is identical with the fee section removed, and the user has to create a fee payment tx that pays in a canonical currency.

i.e. If a user wants to add value into Aztec and does not possess any L2 currency, we only need to enable a mechanism for doing this for tokens that are accepted as fee-paying tokens by sequencers. Once a user has deposited a fee-paying asset into Aztec they can pay their own fees from L2 to handle other token deposits.

13 Likes

Re: the attack where tx 2 calls the gas rebate:

That’s absolutely fine. The only desired outcome is that a sequencer can observe the number of rebate calls a tx makes by examining the private kernel proof’s public inputs. They can just count the number of rebate calls. If it’s >1 they don’t accept the tx.

13 Likes

Ah, yes I think this works. I was going to say the Sequencer could be the person attacking in this way. But they’d only be stealing money from their own pot, since only they are entitled to withdraw what’s left in the Gas Payment contract at the end of a block.

15 Likes