Skip to content

In token paymaster mode, an account can never make its first transaction #32

Description

@G9NCUE

In token paymaster mode, every method that prices an operation does so before the EIP-7702 authorization is signed, and the estimation call has no way to carry one. The bundler simulates an EOA with no code and returns AA20 account not deployed.

So a new account cannot transact through transfer() or sendTransaction(), and quoteTransfer() and quoteSendTransaction() fail for any account that has not already transacted. Everything works from the second transaction onward, which is why it survives most testing.

1.0.0-beta.3, Node 24, Arbitrum One, Pimlico bundler with Candide paymaster on EntryPoint v0.8.

Reproduction

The account is generated fresh each run and holds nothing. Nothing is broadcast.

import { Interface } from 'ethers'
import { generateMnemonic } from '@scure/bip39'
import { wordlist } from '@scure/bip39/wordlists/english.js'
import WalletManagerEvm7702Gasless from '@tetherto/wdk-wallet-evm-7702-gasless'

const USDT0 = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'
const wallet = new WalletManagerEvm7702Gasless(generateMnemonic(wordlist, 128), {
  provider: 'https://arb1.arbitrum.io/rpc',
  delegationAddress: '0xe6Cae83BdE06E4c305530e199D7217f42808555B',
  bundlerUrl: 'https://public.pimlico.io/v2/42161/rpc',
  paymasterUrl: 'https://api.candide.dev/public/v3/42161',
  paymasterToken: { address: USDT0 }
})

const alice = await wallet.getAccount(0)
const to = '0x000000000000000000000000000000000000dEaD'
const amount = 100_000n
const data = new Interface(['function transfer(address to, uint256 amount) returns (bool)'])
  .encodeFunctionData('transfer', [to, amount])

await alice.quoteTransfer({ token: USDT0, recipient: to, amount })   // AA20 account not deployed
await alice.transfer({ token: USDT0, recipient: to, amount })        // AA20 account not deployed
await alice.signTransaction({ to: USDT0, value: 0, data })           // ERC20: transfer amount exceeds balance

The third line is the interesting one. Same account, same amount, same bundler, same run. signTransaction() builds the authorization before estimating, so simulation gets far enough to run the ERC-20 transfer and reject it for the real reason, which is that the account holds no USD₮. Without the authorization, simulation never reaches the token.

The AA20 is therefore not about funding, allowances, the paymaster or the token. The only variable is whether the authorization was present.

Diagnosis

_buildSponsoredUserOperation already accepts a pre-signed authorization and includes it when given
one (wallet-account-read-only-evm-7702-gasless.js:450, applied at :462). The estimation path never
passes it. Its three callers send { nonce } or nothing: quoteSendTransaction
(wallet-account-evm-7702-gasless.js:250), sendTransaction (:301), transfer (:339).

There is a structural reason rather than an oversight. _getUserOperationGasCost is defined on
WalletAccountReadOnlyEvm7702Gasless, which has no _ownerAccount and no _getAuthorization. A read only account holds no key and cannot sign an authorization. The signing class inherits its
estimator from a class that cannot produce the input the estimator needs.

quoteTransfer shows this most clearly. It is defined only on the read only class (:258) and is never overridden, so calling it on a fully keyed account still runs the keyless version. quoteSendTransaction is overridden in the signing class (:233) to add quote caching, and still
omits the authorization.

Suggested fix

transfer() and sendTransaction(): resolve the authorization before pricing and pass it through the existing overrides, as { nonce, eip7702Auth: await this._getAuthorization(mergedConfig) }. One line each. _getAuthorization already returns null once the EOA is delegated, so the warm path is unaffected.

quoteSendTransaction(): the same, in the existing override.

quoteTransfer(): needs an override on the signing class, since the inherited version has no key.

Read only accounts cannot be fixed this way. An explicit error naming delegation would at least let a caller tell "this account must transact once first" from "your transaction is bad".

Why it is worth prioritising

It lands on the first transaction of every new account, which is the first thing a new user does and the moment a wallet has the least credibility to spend. AA20 account not deployed reads as an infrastructure fault, so we spent a while looking at the bundler and the paymaster before looking
here. It also rules out quote then confirm for new accounts, which is the shape the
quoteTransfer/transfer pairing invites.

The way through is signTransaction(tx) then sendTransaction(signedOp), which builds the authorization and broadcasts without re-estimating. That works. It moved 0.1 USD₮0 for a 0.0105 USD₮
fee from an account holding zero wei
(0xad810dc2…).
But it means the documented primary API is the one path a new account cannot use, and every integrator has to work that out alone.

Found building Moor, an open source USD₮ wallet on WDK. lab/t10-send.js there is the funded version of the same repro and asserts both halves, AA20 while undelegated and a
working quote once delegated.

Happy to open a PR if this looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions