The ERC-4337 EntryPoint AA22 revert (‘AA22 expired or not due’) occurs during session key UserOperation validation when the packed validAfter and validUntil timestamps fail against block.timestamp on L2 rollups. Because rollup sequencers can exhibit up to 15 seconds of block timestamp clock drift relative to client system clocks, strict timestamp bounds cause premature invalidation. Adding a 60-second grace buffer to validAfter parameters prevents the revert.
Before executing state changes or signing transactions, verify your cryptographic parameters using our interactive ERC-4337 Calldata Decoder utility.
Architectural Breakdown: EntryPoint Validation Data Encoding
In the ERC-4337 specification (both EntryPoint v0.6 and v0.7), validateUserOp returns a packed 32-byte validationData integer:
// ERC-4337 EntryPoint specification
// uint256 validationData = authorizer (20 bytes) | validUntil (6 bytes) | validAfter (6 bytes)
function _intersectTimeRange(
uint256 validationData,
uint256 paymasterValidationData
) internal view returns (uint256) {
uint48 validUntil = uint48(validationData >> 160);
uint48 validAfter = uint48(validationData >> 208);
// CRITICAL VALIDATION CHECK
if (block.timestamp < validAfter || block.timestamp >= validUntil) {
revert("AA22 expired or not yet valid");
}
}
The Three Timing Trap Vectors
- Client System Clock Skew: If a client device’s clock is running 8 seconds ahead of real-world UTC time and sets
validAfter = Math.floor(Date.now() / 1000), the UserOperation reaches the node while the canonical L2block.timestampis still in the past. - Sub-Second L2 Block Dynamics: On networks like Arbitrum One (which can mint multiple micro-blocks per second) or Base (2-second blocks), block timestamps can drift slightly relative to wall-clock NTP time due to sequencer batching lag.
- Mempool Staging Latency: If an algorithmic agent sets
validUntilto a narrow window (e.g. 30 seconds for MEV protection), network congestion or bundler re-queuing can cause the simulation to execute whenblock.timestamp == validUntil, emittingAA22.
Step-by-Step Resolution Protocol
To resolve AA22 temporal rejections across automated trading agents and modular account implementations, apply this three-tier defensive strategy.
1. Fetch Canonical Block Timestamp from RPC
Never construct session policies using client-side Date.now(). Query the latest mined block’s timestamp directly from your RPC provider:
// src/utils/getCanonicalTimestamp.ts
import { type PublicClient } from 'viem';
export async function getCanonicalBlockTimestamp(publicClient: PublicClient): Promise<number> {
try {
const latestBlock = await publicClient.getBlock({ blockTag: 'latest' });
return Number(latestBlock.timestamp);
} catch (error) {
console.warn('[SessionKeyGuard] Failed to fetch latest block, falling back to UTC wall time:', error);
return Math.floor(Date.now() / 1000);
}
}
2. Apply Defensive Retrospective Buffers in Session Policies
When initializing or signing an ERC-7579 or Kernel v3 session key, configure a 300-second retrospective buffer for validAfter and an adequate expiration margin for validUntil:
// src/sessionKeys/createSessionPolicy.ts
import { type PublicClient } from 'viem';
import { getCanonicalBlockTimestamp } from '../utils/getCanonicalTimestamp';
interface SessionTimeWindowOptions {
durationSeconds?: number;
}
export async function calculateSafeSessionTimestamps(
publicClient: PublicClient,
options: SessionTimeWindowOptions = { durationSeconds: 86400 } // Default 24 hours
) {
const currentBlockTime = await getCanonicalBlockTimestamp(publicClient);
// CRITICAL FIX 1: Subtract 300 seconds (5 mins) to absorb clock drift & sequencer skew
const validAfter = Math.max(0, currentBlockTime - 300);
// CRITICAL FIX 2: Add 120s buffer to validUntil to prevent boundary race conditions
const validUntil = currentBlockTime + options.durationSeconds! + 120;
console.log(`[SessionKeyGuard] Policy Temporal Bounds:`);
console.log(`- validAfter: ${validAfter} (BlockTime - 300s)`);
console.log(`- validUntil: ${validUntil} (BlockTime + ${options.durationSeconds}s + 120s)`);
return { validAfter, validUntil };
}
3. Bundler Pre-Flight Simulation Interceptor
In autonomous agents running transaction loops, simulate the UserOperation locally using publicClient.simulateUserOperation and detect temporal proximity prior to public broadcast:
// src/agents/safeUserOpBroadcaster.ts
import { type UserOperation } from 'permissionless';
export function assertTimeWindowSafety(validUntil: number, minRemainingSeconds = 60) {
const now = Math.floor(Date.now() / 1000);
const remaining = validUntil - now;
if (remaining < minRemainingSeconds) {
throw new Error(
`SESSION_EXPIRED_RISK: Only ${remaining}s remaining before validUntil (${validUntil}). ` +
`Aborting broadcast to prevent AA22 simulation revert.`
);
}
}
Temporal Parameter Safety Matrix
| Configuration Scenario | validAfter Setting | validUntil Setting | Simulation Outcome |
|---|---|---|---|
| Strict Wall-Clock | Date.now() / 1000 | Date.now() / 1000 + 30s | High Revert Risk (AA22) on L2 skew |
| Buffered On-Chain | block.timestamp - 300 | block.timestamp + 3600 | 100% Reliable Execution |
Zero-Value (0) | 0 (Always valid from genesis) | Defined timestamp | Permitted for permanent session signers |
Zero-Value Until (0) | Defined timestamp | 0 (Never expires) | Allowed by EntryPoint, but high risk if compromised |
Frequently Asked Questions
Q: Why does the EntryPoint specification use uint48 for timestamps?
uint48 provides sufficient range to represent Unix timestamps in seconds up to the year 8,921,556 AD, while allowing two timestamps and an Ethereum authorizer address to fit compactly inside a single 32-byte EVM storage word (uint256).
Q: Can an attacker exploit a retrospective validAfter (e.g. block.timestamp - 300)?
No. Setting validAfter in the past merely indicates that the session key became mathematically authorized 5 minutes ago. It does not allow an attacker to alter the on-chain execution order of transactions, because transactions are still ordered and sequenced by the current block builder.
Q: Does AA22 consume gas fees from the smart account or paymaster?
No. AA22 occurs during the simulateValidation or estimateUserOperationGas RPC calls executed off-chain by the bundler. Bundlers drop transactions failing validation without broadcasting them on-chain, protecting accounts from gas drain.
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/.