SNIP-20: Enshrined Paymasters for ERC-20 tokens

snip: 20
title: Enshrined Paymasters for ERC-20 tokens
description: This SNIP proposes a design for enshrined paymasters for ERC-20 tokens
author: Ilia Volokh iliav@starkware.co, Leonardo Lerer leo@starkware.co
status: Review
type: Standards Track
category: Core
created: 2024/09/15
Github link

Simple Summary

We propose a design for enshrined paymasters that leverages the paymaster field in INVOKE transactions version 3. The design allows anyone to deploy paymaster contracts that abstract token exchanges away from users. Its scope is limited to ERC-20 contracts and does not offer full fee abstraction (e.g paying transaction fees with NFTs or other assets).

In a nutshell, users send v3 transactions and specify three pieces of data in the paymaster field:

  1. Paymaster contract address
  2. ERC-20 token contract address
  3. Maximal allowed exchange rate between the above token and STRK. Specifically, the max rate r means the user is willing to pay at most r tokens for a single STRK.
    The target paymaster contract will receive funds from the user (denominated in their ERC-20 token of choice) and pay the transaction fee (in STRK) to the sequencer, at a rate ≤ r. Economic calculation and choice of rate are left to each paymaster contract.

The proposal involves no reputation systems, enshrined oracles/AMMs, or other capital costs to deploy paymaster contracts.

Motivation

The Starknet transaction fee market will act on v3 transaction, whose fees are denominated in the native fee token STRK (see also SNIP-16). However, users may wish to pay transaction fees in other tokens. Paymasters are an umbrella term for mechanisms, products, and/or services that provide users with such functionality.

In broad terms, paymasters can be applicative or enshrined/protocol-level. The latter refers to a paymaster mechanism that is recognized at the protocol level, e.g via the paymaster field in transaction version 3. The former is the complement of the latter, e.g architecture relying solely on outside execution (see SNIP-9).

We think the ability of users to transact on Starknet without owning STRK is an important feature. Moreover, we are in favor of enshrining such a mechanism to avoid strict dependency of such users on additional services external to the standard transaction flow. Thus we present this design proposal for enshrined paymasters.

Proposal

We draw inspiration from ERC-4337, although we take a slightly different route.

Definitions

We shall say a contract is a paymaster if it has a __validate_paymaster__ entrypoint. No computational restrictions are necessary for __validate_paymaster__ as users will pay for its failure - this is explained in detail later.

An example __validate_paymaster__ would read an exchange rate from some chosen oracle(s) or AMM(s) and check the user’s specified exchange rate is liberal enough, e.g 1.05 × oracle_rate ≤ r.

A paymaster transaction is one with a non-empty paymaster field. A valid paymaster transaction specifies three concatenated pieces of data in the paymaster field:

  1. Paymaster contract address
  2. ERC-20 token contract address
  3. Maximal allowed exchange rate between the above token and STRK. Specifically, max rate r means the user is willing to pay at most r tokens for a single STRK.

Flow

We now outline the steps for processing of a paymaster transaction by the sequencer. For simplicity, the initial flow will involve charging at the user’s max rate r; the optimization of charging at discounted rates will be deferred to the next section.

Mempool

Users can submit unrealistically low rates; these should be interpreted as overvaluations of their designated payment tokens. Exaggerated claims should be rejected. However, we wish to avoid defining “reasonable rates” at the protocol level. To this end, each sequencer client locally defines reasonable rates via dictionary: TOKEN → min_STRK/TOKEN_rate. (The ratio follows exchange conventions: it is the price of one STRK in units of the TOKEN.) This dictionary can be static or dynamically updated e.g by some oracle feed. Sequencers can also choose to serve only some chosen whitelist of paymaster contracts.

Full nodes can either maintain such configs or indiscriminantly propagate paymaster transactions without looking into the paymaster field. In the former case, each full node filters some paymaster transactions from the P2P network according to its local configurations. If most nodes have similar configs to sequencers, this does a service by reducing the traffic of invalid transactions on the network, essentially defending sequencer mempools. On the other hand, if full nodes configure themselves too defensively, they may filter out paymaster transactions that sequencers are willing to process. In the latter case where full nodes do not maintain any configs, all invalid transactions will need to be rejected by some sequencer from entering its mempool.

Now the flow:

  1. Syntactic checks (transaction is properly formatted).
  2. User submitted rate should be at least client’s locally configured rate. min_STRK/TOKEN_rate ≤ r
  3. Balance checks:
    • User token balance should cover their bid given their specified max exchange rate balance(user) ≥ max_amount Ă— r(base_price(current_block)+tip).
    • Paymaster STRK balance should cover user bid balance(paymaster) ≥ max_amount Ă— (base_price(current_block)+tip).

Block builder

A paymaster contract should be used only by those who trust its code. Such users know the paymaster’s exchange policies, and therefore know which rates they should submit to pass, and which rates are too frugal and may cause __validate_paymaster__ to fail. Hence, we consider it justified to “blame” users for failed __validate_paymaster__ and to consequently charge them. Now arises a problematic scenario: the user may hold zero STRK and the paymaster rejected the request to cover its transaction fee. In this particular case we allow the sequencer to charge the transaction fee directly in the user’s designated token.

In the naive example above, __validate_paymaster__ will fail during block building if and only if min_STRK/TOKEN_rate ≤ r ≤ 1.05 × oracle_rate, meaning the user’s rate was high enough to enter the client’s mempool but too low for the paymaster.

Now the flow:

  1. Balance checks:
    • User token balance should cover their bid given their specified max rate balance(user) ≥ max_amount Ă— r(base_price(current_block)+tip). If success, CONTINUE; else, REJECT.
    • Paymaster STRK balance should cover user bid balance(paymaster) ≥ max_amount Ă— (base_price(current_block)+tip). If success, CONTINUE; else, REJECT.
  2. Run account __validate__. If success CONTINUE, else REJECT.
  3. Run paymaster __validate_paymaster__. If success, CONTINUE; else, REVERT and skip to fee invocations below.
  4. Run account __execute__. If success, CONTINUE; else, REVERT and skip to fee invocations below.
  5. Fee invocations:
    • If failure was in __validate_paymaster__, user pays sequencer in designated fee token:
      • Burn used_amount Ă— r Ă— base_price.
      • Transfer used_amount Ă— r Ă— tip.
    • If failure was later:
      1. Paymaster pays transaction fee to sequencer:
        • Burn used_amount Ă— base_price.
        • Transfer used_amount Ă— tip.
      2. User pays paymaster: transfer used_amount Ă— r Ă— (base_price(current_block)+tip).
    • In case of failure during fee invocations, REVERT all state changes and return to it directly (skipping __validate_paymaster__ and execute). This ensures both user and paymaster have sufficient balances due to both balance checks.

Optimization: discounted paymaster rates

In the above flow, the paymaster always charges at the user’s max rate r. This encourages strategic “bidding” on the rate. It’s better to allow paymaster contracts to charge users at a smaller rate than their submitted max rate. To this end, we allow __validate_paymaster__ to output a charging_rate satisfying charging_rate ≤ r. If __validate_paymaster__ executes successfully, the fee invocation will proceed with the user’s max rate replaced by the paymaster’s charging_rate.

SDK and Wallet integration

For a truly seamless experience, wallets should:

  1. Integrate with paymasters to support paymaster transactions.
  2. Integrate with oracles to suggest accurate max rates.
  3. Integrate the above into their UI, so an end user can choose a payment token and receive a suggested bid (rate included) to confirm in one click.

Behind the scenes, SDKs should also support sending paymasters transactions.

Rationale

We submit that the above design facilitates simple and safe paymaster contracts providing the desired functionality of letting users pay transaction fees with arbitrary ERC-20 tokens.

Drawbacks

  1. A user who set their max rate in the range min_STRK/TOKEN_rate ≤ r ≤ min_STRK/TOKEN_rate_accepted_by_paymaster will experience a reverted __validate_paymaster__ and pay for the reversion. As noted above, this is easily remedied by submitting high rates, especially for paymasters who charge at discounted rates.

  2. The functionality of the proposed design is limited to ERC-20 tokens, and does not achieve any loftier goals of fee abstraction. Of course this is not a drawback compared to having no functionality at all.

  3. Sequencers will need to actively (re)configure their setup to support new tokens and/or paymaster contracts. Hence, if some token/contract is sparsely adopted, associated paymaster transactions will only be sporadically included within blocks.

  4. As with any protocol addition, the paymaster flow is another complication of Starknet. We think its benefits are worthwhile, and hope the drawback remains theoretical.

  5. This proposal does not facilitate AMMs as paymasters: the fee invocation logic is fixed.

Backwards Compatibility

This proposal is backward compatible as it merely proposes semantics for transactions with a non-empty paymaster field, which was hitherto unused.

Security Considerations

If the sequencer’s locally configured minimal rates are too low, it exposes itself to being potentially underpaid in case of failed __validate_paymaster__.

Copyright

Copyright and related rights waived via MIT.

I have two alternative suggestions:

  1. since the sequencer already has code to take payment in non-STRK tokens, it can be the paymaster. That is, users can sign transactions specifying they want to pay in a different token, including the maximum exchange rate, and the sequencer takes payment in that token.
  2. I believe that with the low transaction fees we have, the main problem for users is of bootstrapping. That is, what to do when they have no STRK. In this context, the main problem is that a user needs to have enough STRK before the tx is executed. But, given some enshrined endpoints, a sequencer can tell a user will have enough STRK after the tx. E.g. if the tx has one call, transferFrom on the STRK contract, with an amount high enough to cover the tx, then the sequencer can check the sender approved the transfer and know the user will have enough balance at the end of the tx. Then, once users bootstrap, they can maintain a small sum, that is sufficient for many transactions and every now and then run a swap to replenish (a $5 amount nowdays can last 5000 transactions), wallets can automatically add a replenish call when the balance runs too low (or, we may have autonomous transactions in the future).

Thanks @ittay! To record the remainder of our convo here:

  1. The advantage is a simplification of protocol changes: the paymaster field of v3 transaction need not include a paymaster contract address. Some drawbacks:
    a. Strategic bidding on the max rate r submitted by users (since there’s no on-chain logic for discount rates)
    b. Coupling between different economics actors: separation of paymaster contracts from sequencers allows non-sequencers to provide the conversion service in all but the edge case of failed __validate_paymaster__.
  2. deferring to @Leo_L :brain:

Hey @ittay, about 2.: the flow of the transferFrom already requires an entity which holds STRK and is willing to give an allowance to the user. In that case why don’t they directly do a transfer of STRK to the STRKless user?
I see the rationale if in the enshrined enpoints you include, say, swaps (assume that AMMs conform to some kind of standard interface of swap). But in that case how can you defend the sequencer from malicious code behind such a swap entrypoint? To accept such a tx in their mempool, the sequencer needs to run the entrypoint till the end to know that at the end the user has enough STRK in its balance - which runs into all the bad stuff caused by stateful validation logics, even after limiting the max resources for running this entrypoint.

the transferFrom was just an example of how the sequencer can examine a tx to know that no initial balance is required. You are correct that in this case the other party can just transfer the funds.

i have a couple though on this

  1. if Sequencers will need to actively (re)configure their setup to support new tokens and/or paymaster contracts whenever new paymaster is available, this will mean they can decide which ERC20 TOKEN they want to support or not.
  2. if this is another complication of Starknet, so why not let go all out to support paying transaction fees with NFTs or other assets via the paymaster
  3. will this proposal support Token bound account ERC20 asset, i mean using TBA ERC20 assets to cover for transaction fee via the paymaster.

Hey @mubarak23 thanks for the comments.

  1. That’s correct but that’s inherent to the “dictatorship” position of a block builder, who may enforce any policy they want on their blocks: e.g. if they really hate a token X, they can decide to censor all txs that perform any operations touching token X - regardless of paymasters.
  2. This is probably possible, but trickier: the fee invocations would need to support an interface which is different than an ERC20 and the mempools would need to store some minimum exchange rate for these NFT/other assets, to prevent spam, which may not be straightfoward for assets with particularly high volatility.
  3. As the proposal stands ATM, it wouldn’t support them - only vanilla ERC20s. But I may be missing something here since I’m not familiar with TBAs :slight_smile: