To resolve ERC-4337 AA21 Didn, fix ERC-4337 EntryPoint AA21 didn. Ensure all derivation paths, environment parameters, and cryptographic signatures are verified client-side using deterministic tools before broadcasting live transactions.
The ERC-4337 bundler simulation error AA21 didn't pay prefund occurs during validateUserOp when an account’s EntryPoint deposit is smaller than the calculated maximum execution cost (totalGasLimit * maxFeePerGas). Even if the smart account holds sufficient ETH, the transaction reverts if validateUserOp omits the missingAccountFunds refund callback to msg.sender. Resolving the error requires returning the prefund value in Solidity or pre-funding the EntryPoint deposit.
To inspect packed UserOperation gas limits, paymaster fields, and call data in real time, run your transaction payload through our interactive ERC-4337 Calldata Decoder Tool. For modular smart account validation issues, see our research on ERC-7579 session key cross-chain replay protections.
1. Mathematical Anatomy of requiredPrefund
To protect decentralized bundlers against denial-of-service (DoS) attacks and unpaid gas consumption, the EntryPoint requires the user to front-load the absolute maximum fee the transaction could conceivably consume:
$$\text{totalGas} = \text{verificationGasLimit} + \text{callGasLimit} + \text{preVerificationGas}$$
$$\text{requiredPrefund} = \text{totalGas} \times \text{maxFeePerGas}$$
When the bundler calls EntryPoint.handleOps():
- The EntryPoint inspects the account’s existing deposit on the EntryPoint contract via
getDepositInfo(sender). - If
deposit < requiredPrefund, the EntryPoint calculates the deficit: $$\text{missingAccountFunds} = \text{requiredPrefund} - \text{deposit}$$ - The EntryPoint invokes
validateUserOp(userOp, userOpHash, missingAccountFunds)on the sender account. - The account contract is expected to transfer exactly
missingAccountFundsin native currency back tomsg.sender(the EntryPoint) during this call. - If the account fails to refund the deficit, the EntryPoint executes
revert("AA21 didn't pay prefund").
2. Primary Root Causes
Root Cause 1: Missing or Broken missingAccountFunds Callback
Many custom smart account implementations forget to wire up the refund transfer in Solidity:
// VULNERABLE IMPLEMENTATION
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override onlyEntryPoint returns (uint256 validationData) {
_validateSignature(userOp, userOpHash);
// FATAL BUG: missingAccountFunds is completely ignored!
// The contract holds 1 ETH, but never transfers the needed gas deposit to the EntryPoint.
return 0;
}
Root Cause 2: SDK Gas Multiplier Inflation
In high-throughput EVM environments (Arbitrum, Base, Polygon, or Ethereum Mainnet), front-end SDKs like Viem, Ethers, or Permissionless.js estimate fees using estimateFeesPerGas().
To prevent transaction drops, dApps frequently configure aggressive fee padding:
// DANGEROUS FEE PADDING
maxFeePerGas: baseFee * 250n / 100n, // 2.5x base fee
maxPriorityFeePerGas: parseGwei('3'),
When multiplied across a batch execution requiring 400,000 total gas, requiredPrefund skyrockets to 0.045 ETH. If the user account holds 0.038 ETH, the simulation fails with AA21 even though actual execution would have consumed only 0.006 ETH.
Root Cause 3: Paymaster Omission or Paymaster Rejection
If a dApp intends to sponsor transactions using a gas paymaster (e.g., Pimlico VerifyingPaymaster), but:
paymasterAndDatais left empty or malformed (0x),- The paymaster’s on-chain deposit inside the EntryPoint has run dry,
The EntryPoint falls back to charging the user’s account directly, triggering
AA21on accounts with 0 native ETH.
(For paymaster execution issues, see our companion guide on ERC-7579 Session Key Cross-Chain Signature Replay Exploits).
3. Step-by-Step Remediation Protocols
Solution 1: Fix Solidity validateUserOp Implementation
Ensure your account contract transfers the exact deficit back to msg.sender (the EntryPoint) using low-level call:
// HARDENED SOLDIER IMPLEMENTATION
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override returns (uint256 validationData) {
require(msg.sender == address(entryPoint()), "Only EntryPoint allowed");
// 1. Verify signatures and nonce
validationData = _validateSignature(userOp, userOpHash);
// 2. Pay the required prefund deficit back to the EntryPoint
if (missingAccountFunds > 0) {
(bool success, ) = payable(msg.sender).call{
value: missingAccountFunds,
gas: type(uint256).max
}("");
require(success, "Failed to pay prefund to EntryPoint");
}
return validationData;
}
Solution 2: Pre-Fund the Account’s EntryPoint Deposit Directly
Instead of relying on JIT (just-in-time) transfers during every transaction, maintain an on-chain deposit in the EntryPoint contract for high-frequency bots:
# Deposit 0.05 ETH directly into EntryPoint for your smart account via Foundry cast
cast send 0x0000000071727De22E5E9d8BAf0edAc6f37da032 \
"depositTo(address)" 0xYourSmartAccountAddress \
--value 0.05ether \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY
Once deposited, missingAccountFunds remains 0 for subsequent transactions, bypassing AA21 entirely.
Solution 3: Dynamic Fee Estimation & Gas Buffer Tuning
In your TypeScript client, normalize gas parameters and ensure you do not over-inflate maxFeePerGas:
import { createBundlerClient, toHex } from 'viem';
export async function prepareOptimizedUserOp(bundlerClient, smartAccount, callData) {
// 1. Fetch live gas price from RPC
const { maxFeePerGas, maxPriorityFeePerGas } = await bundlerClient.client.estimateFeesPerGas();
// 2. Query account balance
const accountBalance = await bundlerClient.client.getBalance({
address: smartAccount.address,
});
// 3. Estimate UserOp Gas
const gasEstimate = await bundlerClient.estimateUserOperationGas({
account: smartAccount,
calls: [callData],
});
const totalGas = gasEstimate.callGasLimit + gasEstimate.verificationGasLimit + gasEstimate.preVerificationGas;
const prefundRequired = totalGas * maxFeePerGas;
if (accountBalance < prefundRequired) {
throw new Error(
`Insufficient funds for prefund. Required: ${prefundRequired} wei, Available: ${accountBalance} wei. Consider attaching a Paymaster or lowering gas buffers.`
);
}
return {
...gasEstimate,
maxFeePerGas: (maxFeePerGas * 115n) / 100n, // Conservative 15% buffer rather than 200%
maxPriorityFeePerGas,
};
}
4. Production Diagnostic Matrix
| Symptom | Underlying Cause | Quick Fix |
|---|---|---|
| Account has 0 ETH, Paymaster used | Paymaster deposit in EntryPoint exhausted or paymaster signature expired. | Top up paymaster on-chain via entryPoint.depositTo(paymaster) or re-request sponsorship token. |
| Account has ETH, but AA21 throws instantly | validateUserOp lacks payable(msg.sender).call{value: missingAccountFunds}(""). | Update smart account implementation or pre-deposit funds via depositTo(account). |
| Occasional spikes during network congestion | maxFeePerGas multiplied by high gas limits exceeds account balance. | Implement dynamic prefund validation before dispatching to bundler RPC. |
ERC-4337 EntryPoint Error Code Reference Matrix
Cross-reference standard ERC-4337 EntryPoint revert codes across account abstraction and smart wallet layers:
| Error Code | Stage | Primary Failure Mechanism | Diagnostic Guide |
|---|---|---|---|
| AA13 | Factory Deployment | InitCode execution reverted or exceeded factory gas | ERC-4337 AA13 InitCode Failed Fix |
| AA21 | Prefund Validation | Sender account balance or deposit below required maxCost | ERC-4337 AA21 Prefund Payment Fix |
| AA22 | Timestamp Validity | Clock drift or expired session key validity window | ERC-4337 AA22 Clock Drift Fix |
| AA23 | User Verification | P-256 passkey signature or WebCrypto auth failed | ERC-4337 AA23 Signature Revert Fix |
| AA33 | Paymaster PostOp | Post-execution token transfer or context reverted | ERC-4337 AA33 Paymaster PostOp Fix |
| Paymaster | Gas Sponsorship | Paymaster rejected UserOp or sponsorship signature invalid | ERC-4337 Paymaster Revert Resolution |
[!TIP] Unpack packed UserOperations, calculate prefund requirements, and isolate simulation hazards in our air-gapped /tools/erc4337-calldata-decoder/.