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.
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()orsendTransaction(), andquoteTransfer()andquoteSendTransaction()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.
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
AA20is therefore not about funding, allowances, the paymaster or the token. The only variable is whether the authorization was present.Diagnosis
_buildSponsoredUserOperationalready accepts a pre-signed authorization and includes it when givenone (
wallet-account-read-only-evm-7702-gasless.js:450, applied at:462). The estimation path neverpasses 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.
_getUserOperationGasCostis defined onWalletAccountReadOnlyEvm7702Gasless, which has no_ownerAccountand no_getAuthorization. A read only account holds no key and cannot sign an authorization. The signing class inherits itsestimator from a class that cannot produce the input the estimator needs.
quoteTransfershows 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.quoteSendTransactionis overridden in the signing class (:233) to add quote caching, and stillomits the authorization.
Suggested fix
transfer()andsendTransaction(): resolve the authorization before pricing and pass it through the existingoverrides, as{ nonce, eip7702Auth: await this._getAuthorization(mergedConfig) }. One line each._getAuthorizationalready returnsnullonce 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 deployedreads as an infrastructure fault, so we spent a while looking at the bundler and the paymaster before lookinghere. It also rules out quote then confirm for new accounts, which is the shape the
quoteTransfer/transferpairing invites.The way through is
signTransaction(tx)thensendTransaction(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.jsthere is the funded version of the same repro and asserts both halves,AA20while undelegated and aworking quote once delegated.
Happy to open a PR if this looks right.