Account abstraction has quietly become one of the biggest shifts in how people use Ethereum. More than 26 million smart accounts and over 170 million UserOperations have gone through the ERC-4337 pipeline as of October 2026, according to ethereum.org’s account abstraction roadmap page. A separate tracker from blockeden.xyz put total smart-wallet adoption even higher, reporting that account abstraction had crossed 40 million wallets earlier in 2026 as EIP-7702 adoption compounded on top of ERC-4337. Either way, the pattern is the same: wallets no longer need a user to hold ETH just to pay gas. A paymaster contract covers that cost instead.
That convenience comes with a new attack surface that most Solidity engineers have never tested. A paymaster is a contract that agrees, during a short validation window, to cover gas for someone else’s transaction. If its validation logic is loose, an attacker does not need to steal a private key or flash-loan a pool. They just need to get the paymaster to pay for operations it should have rejected. The ERC-4337 documentation puts it plainly: “Paymasters introduce a new layer of gas abstraction, but they also open new attack surfaces,” according to the ERC-4337 documentation on security and griefing protection.
This tutorial walks through 12 Foundry steps for testing paymaster exploits against your own ERC-4337 contracts, not reproducing a public hack. No large, independently confirmed dollar-loss incident tied specifically to a paymaster has surfaced in security reporting as of late 2026, which is itself a reason to test now. The risk is documented in the specification, the attack classes are well understood, and almost nobody has written automated tests for them yet. By the end, you will have a working Foundry project with a deliberately vulnerable paymaster, a full exploit test suite, fuzz tests, invariant tests, and a hardened version you can compare against the original.
How big is the paymaster attack surface right now
Adoption numbers for account abstraction vary by tracker, and the gap between sources is wide enough that none of them should be treated as a settled count. ethereum.org’s own roadmap page cites over 26 million smart accounts and more than 170 million UserOperations. A February 2026 post from blockeden.xyz framed the same trend as crossing 40 million wallets once EIP-7702 adoption is folded in. Both point the same direction. Paymasters are no longer a niche feature bolted onto a handful of experimental wallets. They sit in the critical path of a large and growing share of Ethereum transactions.
What is genuinely unsettled is how much of that volume still runs through paymasters specifically, as opposed to other sponsorship mechanisms that have emerged since. One 2026 analysis put the paymaster-sponsored share at roughly 87% of UserOperations at one measurement point, then reported that same share falling toward one-third of sponsored volume by mid-2026 as alternative gas-sponsorship methods expanded. Gelato’s own infrastructure reporting cited a smaller adoption snapshot, around 25.5 million smart accounts and 132 million UserOperations, with roughly $5.7 million in gas sponsored through its paymaster infrastructure. Treat each of these as a snapshot from a different vantage point rather than a single authoritative figure, and expect the exact ratio to keep shifting as EIP-7702 pulls more ordinary wallets into the same sponsorship pipeline.
What has not shifted is the testing gap. Reentrancy, flash loan attacks, and oracle manipulation have been audited, fuzzed, and written about for years, with entire conference talks built around famous incidents. Paymaster validation logic is newer, less understood outside a small circle of account abstraction infrastructure teams, and rarely gets the same fuzzing or invariant coverage as a token contract or a lending pool. That gap, not a lack of real risk, is the actual reason to build the test suite in this tutorial before an attacker builds the exploit first.
What makes a paymaster exploit different from reentrancy or a flash loan bug
If you have already worked through reentrancy testing, flash loan attack simulations, or oracle manipulation drills, you might assume paymaster security is just another variation. It is not. Those bug classes exploit a contract’s own state machine or its reliance on external price data. A paymaster exploit instead targets the trust relationship between three separate actors: the user who submits a UserOperation, the bundler that relays it, and the EntryPoint contract that enforces the rules in between.
The EntryPoint calls two functions on every paymaster: validatePaymasterUserOp, which decides whether to sponsor an operation, and postOp, which runs after execution to settle the actual gas cost. Both functions run under tight gas budgets and strict calling-convention rules. Get the binding logic wrong in either one, and you create a path for someone to drain a deposit, trigger a denial of service, or get gas sponsored for calls that should never have qualified. The official ERC-4337 proposal is explicit about the stakes: “Maliciously crafted paymasters can DoS the system,” according to the ERC-4337 standard text on GitHub.
There is also an authorization rule that is easy to skip during a rushed implementation. The specification states that “all paymaster contracts MUST check that all calls to the validatePaymasterUserOp() and postOp() functions originate from the EntryPoint,” per the same standard text. Miss that check, and anyone can call your validation logic directly, bypassing the bundler and the mempool rules entirely. That single missing modifier turns a paymaster from a convenience feature into an open door.
Prerequisites: versions and tools you need before starting
You do not need a mainnet deployment or real ETH for any of this. Everything in this tutorial runs locally against Anvil, Foundry’s bundled local chain. You will need a terminal, a text editor, and roughly 90 minutes to work through all 12 steps including the troubleshooting detours.
| Tool or dependency | Minimum version | Why you need it |
|---|---|---|
| Foundry (forge, cast, anvil) | Latest stable release via foundryup | Compiles contracts, runs tests, fuzzing, and invariant testing |
| Solidity compiler | 0.8.24 or newer | Matches the pragma used by current EntryPoint reference implementations |
| Git | Any recent 2.x release | Pulls the account-abstraction reference repository as a dependency |
| eth-infinitism/account-abstraction | Tag matching your target EntryPoint version (v0.6, v0.7, or v0.8) | Provides the real EntryPoint and IPaymaster interfaces to test against |
| Node.js (optional) | LTS release | Only needed if you also want to build UserOperations off-chain with a library such as permissionless.js |
One habit will save you hours later: pin the exact EntryPoint version you are testing against, and get its deployed address from the official eth-infinitism repository or the ethereum.org roadmap page rather than copying an address from a blog post or a cached memory of “the EntryPoint address.” EntryPoint v0.8 shipped in 2025 with real behavioral changes, including native support for EIP-7702 accounts and a fix to how postOp calculates actualGasUsed, according to the EntryPoint v0.8 release notes. A test suite written against v0.6 semantics can pass cleanly while missing a bug that only exists in v0.7 or v0.8, or vice versa.
If your paymaster ships alongside a wallet built on an SDK such as thirdweb, Pimlico, or Alchemy’s account kit, keep in mind that the SDK usually only builds and submits the UserOperation. The validation logic you are testing here still lives in your own paymaster contract, so swapping SDKs does not change which checks you need to write. What does change is which EntryPoint version the SDK defaults to, so confirm that setting explicitly instead of assuming it matches whatever version you tested against last.
Step 1 through 3: scaffold the project and deploy a local EntryPoint
Start with a clean Foundry project. Step 1 initializes the repository, Step 2 pulls in the reference EntryPoint implementation as a dependency, and Step 3 deploys that EntryPoint to a local Anvil instance inside your test setup so every subsequent test runs against real EntryPoint bytecode instead of a hand-rolled mock.
# Step 1: scaffold the project
forge init paymaster-exploit-lab
cd paymaster-exploit-lab
# Step 2: install the account-abstraction reference implementation
# pin a tag that matches the EntryPoint version you intend to test
forge install eth-infinitism/[email protected] --no-commit
# confirm Foundry is current
foundryup
forge --version
Step 3 lives inside your test file. Rather than deploying the EntryPoint by hand in every test, put it in a shared setUp() function so each test starts from the same known-good state.
// test/PaymasterExploit.t.sol
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {VulnerablePaymaster} from "../src/VulnerablePaymaster.sol";
contract PaymasterExploitTest is Test {
EntryPoint entryPoint;
VulnerablePaymaster paymaster;
address attacker = makeAddr("attacker");
address sponsor = makeAddr("sponsor");
function setUp() public {
entryPoint = new EntryPoint();
paymaster = new VulnerablePaymaster(entryPoint);
vm.deal(sponsor, 10 ether);
vm.prank(sponsor);
paymaster.deposit{value: 5 ether}();
}
}
Step 4: build a deliberately vulnerable test paymaster
You cannot test an exploit against a contract that does not have the bug. Step 4 is writing a small, intentionally flawed paymaster that reproduces the two most common real-world mistakes: skipping the EntryPoint-only caller check, and sponsoring an operation without binding the signature to the fields that actually matter, such as the sender, the nonce, and the calldata hash.
// src/VulnerablePaymaster.sol
pragma solidity ^0.8.24;
import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol";
import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";
contract VulnerablePaymaster is IPaymaster {
address public immutable entryPoint;
mapping(address => bool) public sponsoredBefore;
constructor(address _entryPoint) {
entryPoint = _entryPoint;
}
function deposit() external payable {}
// BUG 1: no "require(msg.sender == entryPoint)" check
// BUG 2: approves any op from a sender that has paid once before,
// without checking calldata, gas limits, or an expiry window
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32,
uint256
) external override returns (bytes memory context, uint256 validationData) {
if (sponsoredBefore[userOp.sender]) {
return ("", 0);
}
sponsoredBefore[userOp.sender] = true;
return ("", 0);
}
function postOp(
PostOpMode,
bytes calldata,
uint256 actualGasCost
) external override {
// BUG 3: no accounting against a per-user or per-op spending cap
}
}
Keep this contract next to a clean, hardened version later in the tutorial so you can run the exact same test suite against both and see the difference in pass and fail counts.
Step 5: test deposit draining through repeated invalid operations
The ERC-4337 documentation lists three concrete attack patterns a malicious user can attempt against a loose paymaster: draining its deposit by spamming invalid operations, tricking it into sponsoring high-cost or reverted calls, and exploiting timeouts or race conditions in its verification logic, per the project’s paymaster documentation. Step 5 writes a test for the first pattern. The attacker submits the same type of operation repeatedly from freshly generated addresses, each one slipping past the weak sponsoredBefore check because it has never been seen before.
function testDepositDrainsViaRepeatedFreshSenders() public {
uint256 startBalance = address(paymaster).balance;
for (uint256 i = 0; i < 20; i++) {
address freshSender = makeAddr(string(abi.encodePacked("sender", i)));
vm.prank(freshSender);
// each call looks "new" to the paymaster, so validation keeps passing
(, uint256 validationData) = paymaster.validatePaymasterUserOp(
_buildMinimalOp(freshSender), bytes32(0), 0
);
assertEq(validationData, 0, "paymaster should have rejected a repeat pattern");
}
// in a full bundler simulation, each pass would also drain real gas
// reimbursement from the deposit; here we assert the logic flaw directly
assertTrue(address(paymaster).balance == startBalance, "deposit unchanged in this isolated check");
}
Notice that this test isolates the validation logic rather than running a full bundler simulation. That is intentional. Full UserOperation submission through handleOps is worth adding later as an integration test, but isolating validatePaymasterUserOp first lets you pinpoint exactly which check failed without debugging gas accounting and bundler reputation rules at the same time.
Step 6: test gas griefing in postOp accounting
Gas griefing works differently from deposit draining. Instead of exploiting a logic gap in validation, the attacker crafts calldata that is cheap to validate but expensive to execute, forcing the paymaster to cover a far larger bill than it priced in during validatePaymasterUserOp. The documentation frames the underlying rule simply: “If a sponsored op fails validation or execution, the Paymaster pays the gas,” according to the same ERC-4337 paymaster guide. Step 6 writes a test that submits an operation with a cheap validation path and an expensive, failing execution path, then checks whether the paymaster’s accounting correctly reflects the real cost.
function testGasGriefingThroughExpensiveFailingCall() public {
uint256 depositBefore = address(paymaster).balance;
// calldata that passes validation cheaply, then burns gas in a loop
// before reverting during actual execution
bytes memory expensiveFailingCallData = abi.encodeWithSignature(
"burnGasThenRevert()"
);
vm.prank(attacker);
try entryPoint.handleOps(
_buildOpWithCallData(expensiveFailingCallData),
payable(attacker)
) {
fail();
} catch {
// the call should revert, but check whether the paymaster's deposit
// dropped by more than a bounded, expected ceiling
uint256 depositAfter = address(paymaster).balance;
uint256 drained = depositBefore - depositAfter;
assertLt(drained, 0.01 ether, "a single op drained far more than expected");
}
}
If that assertion fails, you have found a real gas griefing path. The fix almost always involves capping the maximum gas a paymaster will sponsor per operation and rejecting UserOperations whose declared gas limits exceed a conservative ceiling, rather than trusting the caller’s own numbers.
Step 7: test signature-verification bypass
Many production paymasters add an off-chain signer: a backend service signs an approval for a specific operation, and the paymaster checks that signature on-chain before sponsoring gas. The bug almost never shows up in the signature math itself. It shows up in which fields the signature actually covers. If the signed message only commits to the sender’s address and not to the calldata, nonce, gas limits, or an expiry timestamp, an attacker can take a validly signed approval and replay it against a completely different operation.
function testSignatureApprovalIsReplayedAgainstDifferentOp() public {
// sponsor's backend signs approval for a cheap, benign op
bytes memory validSig = _signApproval(sponsor, attacker, /* nonce */ 1);
// attacker reuses that exact signature on a different, expensive call
// because the signed payload never bound calldata or gas limits
bool accepted = paymaster.checkSignatureBinding(
attacker,
validSig,
/* different calldata */ abi.encodeWithSignature("drainTreasury()")
);
assertFalse(accepted, "signature approval should not transfer across unrelated calldata");
}
A properly scoped fix hashes the sender, nonce, calldata, gas limits, chain ID, and a validity window into the exact message the off-chain signer approves, then checks every one of those fields again on-chain before accepting the signature. Skipping even one field, most often the chain ID or the calldata hash, reopens the door for a cross-context replay.
Step 8 and 9: fuzz validatePaymasterUserOp and write invariant tests
Hand-written tests catch the bugs you already suspect. Fuzzing and invariant testing catch the ones you did not think to ask about. Step 8 hands Foundry’s built-in fuzzer a wide range of gas limits, nonce values, and validity windows and asks it to find any combination that slips past validation when it should not.
function testFuzz_ValidationRejectsOutOfWindowOps(
uint48 validAfter,
uint48 validUntil,
uint256 callGasLimit
) public {
vm.assume(validUntil < validAfter); // an impossible, inverted window
vm.assume(callGasLimit > 2_000_000); // an unreasonably high gas request
(, uint256 validationData) = paymaster.validatePaymasterUserOp(
_buildOpWithWindow(validAfter, validUntil, callGasLimit),
bytes32(0),
0
);
assertTrue(validationData != 0, "paymaster accepted an impossible or oversized op");
}
Step 9 goes further with an invariant: a property that must hold true no matter what sequence of calls a randomized handler throws at the contract across hundreds of runs. For a paymaster, the invariant that matters most is simple to state and easy to violate in code: unauthorized UserOperations must never be able to reduce the deposit below the sum of all legitimately sponsored costs. Foundry’s invariant testing module runs a handler contract through randomized call sequences and checks that property after every one.
// test/invariants/PaymasterInvariant.t.sol
contract PaymasterInvariantTest is Test {
VulnerablePaymaster paymaster;
PaymasterHandler handler;
function setUp() public {
paymaster = new VulnerablePaymaster(address(0xBEEF));
handler = new PaymasterHandler(paymaster);
targetContract(address(handler));
}
function invariant_depositNeverDropsBelowLegitimateSpend() public {
assertGe(
address(paymaster).balance,
handler.legitimatelySponsoredTotal(),
"deposit dropped below what legitimate sponsorship should have cost"
);
}
}
Step 10: compare behavior across EntryPoint v0.6, v0.7, and v0.8
A test suite that only runs against one EntryPoint version gives you a false sense of coverage if your paymaster, or your users, might ever interact with another. v0.8’s fix to actualGasUsed calculation in postOp matters here directly: a paymaster that computed its own reimbursement logic around the older, buggy accounting could end up over-charging or under-charging users once it is paired with a v0.8 EntryPoint, according to the same release notes referenced earlier.
| EntryPoint version | Key change relevant to paymaster testing | What to re-run against it |
|---|---|---|
| v0.6 | Original UserOperation struct and gas accounting model | Baseline deposit draining, gas griefing, and signature-binding tests |
| v0.7 | Packed UserOperation struct, revised calldata encoding | Re-run every test, since struct packing changes can silently break decode logic |
| v0.8 | EIP-7702 support, fixed postOp actualGasUsed calculation, disallows zero-address paymaster | Re-test postOp accounting specifically, plus any EIP-7702 EOA interaction paths |
Use Foundry’s profile system to run the identical test file against each pinned EntryPoint tag rather than maintaining three separate copies of the suite. A single forge test --match-path test/PaymasterExploit.t.sol call per profile, with the dependency swapped underneath it, keeps the comparison honest.
Step 11: run the full suite and read the output
With every test written, Step 11 is running the whole thing with verbose tracing so failures actually tell you something useful instead of a bare revert.
forge test --match-path "test/**/*.sol" -vvv
Against the deliberately vulnerable paymaster from Step 4, expect output close to this:
Ran 6 tests for test/PaymasterExploit.t.sol:PaymasterExploitTest
[FAIL: paymaster accepted an impossible or oversized op] testFuzz_ValidationRejectsOutOfWindowOps (runs: 257, μ: 41203, ~: 40998)
[FAIL: signature approval should not transfer across unrelated calldata] testSignatureApprovalIsReplayedAgainstDifferentOp
[PASS] testDepositDrainsViaRepeatedFreshSenders (gas: 184221)
[FAIL: a single op drained far more than expected] testGasGriefingThroughExpensiveFailingCall
Ran 1 test suite: 3 passed, 3 failed
Three failures on an intentionally broken contract is the correct, healthy result. It confirms the test suite actually detects the three vulnerability classes it was written for. If every test passes against the vulnerable paymaster, the problem is in your tests, not a reason to celebrate.
Step 12: harden the paymaster and wire the suite into CI
The last step closes the loop. Fix the three bugs in a new contract, re-run the identical suite, and confirm every test flips from failing to passing. A minimal fix adds the EntryPoint-only check, binds the signature to calldata and an expiry window, and caps per-operation gas sponsorship.
// src/HardenedPaymaster.sol (excerpt)
modifier onlyEntryPoint() {
require(msg.sender == entryPoint, "caller is not the EntryPoint");
_;
}
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external override onlyEntryPoint returns (bytes memory, uint256) {
require(userOp.callGasLimit <= MAX_SPONSORED_GAS, "gas limit exceeds sponsorship cap");
require(block.timestamp <= validUntil(userOp), "approval window expired");
require(_isBoundSignature(userOp, userOpHash), "signature not bound to this op");
return ("", 0);
}
Then add the suite to continuous integration so a future change cannot silently reopen any of these three paths without a red build.
# .github/workflows/paymaster-security.yml
name: Paymaster security tests
on: [push, pull_request]
jobs:
foundry-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: foundry-rs/foundry-toolchain@v1
- run: forge test --match-path "test/**/*.sol" -vvv
- run: forge test --match-contract PaymasterInvariantTest
Common pitfalls when testing ERC-4337 paymasters
- Testing against a mock EntryPoint that skips the real authorization flow. A hand-rolled mock often does not enforce the EntryPoint-only caller check, which means a missing
onlyEntryPointmodifier in your paymaster can pass every test while remaining exploitable in production. - Writing tests against the wrong EntryPoint version. v0.6 and v0.7 pack the UserOperation struct differently. A test suite that compiles cleanly against one can silently exercise the wrong code path against the other, and look green while testing nothing meaningful.
- Fuzzing narrow, “reasonable” ranges only. Bounding your fuzz inputs to values that look sane, like small gas limits or sequential nonces, misses exactly the edge cases attackers target, like a
validUntiltimestamp set beforevalidAfter. - Writing invariant handlers with unrealistic permissions. A handler that calls privileged functions no real attacker could reach gives you false confidence. Scope the handler’s available functions to what an unauthenticated caller can actually invoke.
- Testing only the happy path of postOp. Most teams verify that gas settles correctly when an operation succeeds and skip the revert and out-of-gas branches, which is exactly where gas griefing hides.
- Treating a green test suite as proof of safety. Unit tests catch what you thought to test. Run the suite again against a mainnet fork using the real, deployed EntryPoint bytecode before trusting it fully.
Troubleshooting: 8 errors you’ll hit and how to fix them
- “EvmError: Revert” with no message. Re-run with
-vvvvto get a full call trace, then check whether the paymaster’s ownrequirestatements include revert reasons at all. - forge install fails to resolve the account-abstraction dependency. Confirm you pinned an actual released tag (such as
@v0.7.0), not a branch name, and that the submodule path matches what your import statements expect. - “Stack too deep” during compilation. Add
via_ir = trueto yourfoundry.tomlprofile, since paymaster validation functions tend to have many local variables packed into one call. - ABI decode failures when switching EntryPoint versions. This almost always means your UserOperation struct definition does not match the packed layout of the version you just switched to. Re-import the interface from the matching tagged release.
- Nonce collision errors inside a test. Each simulated sender needs a unique nonce key per test run. Derive it from the loop index or the fuzzed input rather than hardcoding zero everywhere.
- EntryPoint simulation reverts with an “AA” prefixed error code. These are ERC-4337 standardized revert reasons, for example a signature failure or an insufficient deposit. Look up the specific “AAxx” code against the EntryPoint source to see exactly which check failed.
- Fuzz tests that hang or run far longer than expected. Lower
fuzz.runsin your Foundry profile while iterating, then raise it back up for the CI run once the logic is settled. - Invariant tests reporting “no contracts to target.” Call
targetContract()explicitly insetUp()for your handler, since Foundry will not auto-discover it otherwise.
Advanced tips for production paymaster audits
Once the local suite passes cleanly, fork the real chain your paymaster will deploy to and re-run the same tests against the actual, deployed EntryPoint contract rather than a freshly compiled local copy. Bytecode-level behavior can diverge from the reference implementation in ways a fresh deployment never surfaces. Use forge test --fork-url $RPC_URL --match-path test/PaymasterExploit.t.sol for this pass.
It also pays to simulate bundler behavior, not just EntryPoint calls. Bundlers apply their own reputation and throttling rules to UserOperations before they ever reach the chain, and some griefing patterns only matter if they can get past that layer repeatedly. Infrastructure providers such as Gelato and Pimlico expose bundler simulation endpoints that let you dry-run a UserOperation through the real acceptance pipeline before it costs anything.
Finally, if a fuzz or invariant run on your own, already-deployed paymaster surfaces a real exploit, do not post it publicly. Most serious account abstraction infrastructure providers run bug bounty programs through platforms like Immunefi. Report through that channel first, with a minimal, isolated reproduction case drawn directly from the Foundry test that found it.
It also helps to pair the test suite with live monitoring once a paymaster actually goes to production. A deposit that drops faster than your own sponsorship logic predicts is an early warning sign long before anyone files a bug report. Set a simple on-chain alert that compares the paymaster’s EntryPoint deposit balance against a rolling baseline, and page someone the moment the drop rate crosses a threshold your own test suite already established as abnormal. The same invariant you wrote in Step 9 to catch a bug in Foundry doubles as the threshold definition for that alert. Teams that skip this step tend to find out about a griefing attack from a drained deposit and an angry user, rather than from a dashboard.
The complete working project
By the end of these 12 steps, your repository should look like this, with every exploit test paired against both the vulnerable and the hardened contract:
paymaster-exploit-lab/
├── lib/
│ └── account-abstraction/ # pinned reference implementation
├── src/
│ ├── VulnerablePaymaster.sol # Step 4
│ └── HardenedPaymaster.sol # Step 12
├── test/
│ ├── PaymasterExploit.t.sol # Steps 5, 6, 7, 8
│ └── invariants/
│ └── PaymasterInvariant.t.sol # Step 9
├── .github/workflows/
│ └── paymaster-security.yml # Step 12
└── foundry.toml
Run forge test against both contracts side by side, and keep the comparison in your audit notes. A security review that shows a specific test failing against the vulnerable version and passing against the hardened one is far more convincing to a reviewer, or a bounty program, than a plain description of the bug.
Frequently asked questions
What is a paymaster in ERC-4337?
A paymaster is a smart contract that agrees to cover gas costs for a UserOperation on behalf of a wallet, so users do not need to hold ETH to transact. It validates each operation through validatePaymasterUserOp and settles the real cost afterward through postOp.
Is ERC-4337 the same thing as EIP-7702?
No. ERC-4337 defines a full account abstraction pipeline with its own EntryPoint contract and UserOperation format. EIP-7702 lets a regular externally owned account temporarily behave like a smart contract. EntryPoint v0.8 added support so EIP-7702 accounts can also use ERC-4337 bundlers and paymasters.
Do I need real ETH or a testnet faucet to run these tests?
No. Every step in this tutorial runs against a local Anvil chain spun up inside Foundry’s test environment. The only time you need a real RPC endpoint is the optional mainnet-fork pass covered in the advanced tips section.
What actually changed between EntryPoint v0.6, v0.7, and v0.8?
v0.7 repacked the UserOperation struct and its calldata encoding. v0.8 added EIP-7702 support, fixed a bug in how postOp calculates actualGasUsed, and blocked paymasters from being set to the zero address.
Can gas griefing really drain a paymaster’s deposit?
Yes, if the contract sponsors operations without capping gas usage per call or rejecting unrealistic gas limits declared by the sender. The attacker does not need to steal anything directly. They just make the paymaster pay for far more execution than it priced in during validation.
Should I test against a mock EntryPoint or the real one?
Use the real reference implementation from the eth-infinitism/account-abstraction repository, pinned to your target version tag. A mock is faster to write but tends to skip exactly the authorization and gas-accounting rules you are trying to test.
How much Solidity and Foundry experience do I need for this tutorial?
You should be comfortable writing basic Foundry tests and reading a Solidity contract. You do not need prior ERC-4337 experience. The EntryPoint and UserOperation concepts are explained as they come up in each step.
Where do I report a paymaster vulnerability I find in a live contract?
Report it through the project’s bug bounty program if one exists, often hosted on a platform like Immunefi, rather than disclosing it publicly. Include a minimal Foundry reproduction case so the team can verify the issue quickly.




