Adventures in ERC4337 frontrunning
Imagine discovering a nuance in a Ethereum’s new transaction model that allows you to almost double your gas fees paid back — consistently. This is the wild west of ERC4337 frontrunning.
In sophomore year of college, I was reading through the specifications of ERC-4337 to learn more about Account Abstraction. ERC-4337 introduces UserOperations (UserOps), which are special transactions signed by smart wallets. These transactions are relayed on-chain by actors called bundlers. Since UserOps are signed, ERC-4337 verifies authorization by the smart wallet before executing them within its context.
Interestingly, smart wallets don’t directly cover the gas costs for on-chain relaying. Instead, bundlers front the gas fees, and the smart wallet reimburses them during the UserOp execution.
Understanding UserOps
Here’s the structure of a UserOp (as of version 0.6):
Notably, the UserOp contains the data on how much it’ll pay the bundler for relaying through the
maxFeePerGas and maxPriorityFeePerGas fields.But take this AA transaction: the bundler paid 0.0048 POL and got refunded 0.0091 POL, almost double what it paid!
AA bundles often overpay for gas to avoid getting stuck during on-chain congestion. But they regularly overpay significantly, and this leaves opportunity on the table:
We’ll take UserOps that are being relayed for cheap, below their refund value, bump up the gas price a bit, and then receive their refund — which will be more than what it costed to relay them onchain.
To do this, we have to execute before the original bundler’s userop takes the refund for themself. Whoever executes after the other will revert, ultimately taking a loss.
To do this strategy, there needs to be 4337 bundles being sent onchain, and we have to be able to see pending transactions. Fortunately, Polygon has decent AA usage and has an open mempool.
How is this different from regular frontrunning?
Unlike regular frontrunning, the end user / smart wallet doesn’t get harmed. Their UserOperation will successfully execute, just with a different bundler (which was the canonical design specified by the alt mempool in the 4337 spec anyways). It’s true that the original bundler loses in gas fees, but in an ideal world, bundlers would send the EOA bundles with the same gas price as specified in the UserOp — that way, there would be no exploitable opportunities, the original bundler wouldn’t have reverts, and the smart wallet gets the execution speed it’s paying for. But by underpaying, the original bundler is leaving the opportunity open to being frontrun. Either way, the smart wallet is overpaying, to one bundler or another.
Building the Bot
I was broke and needed to find a way snoop on the Polygon mempool without running my own node, which would be expensive.
BlockNative had this “explorer” for AA transactions that showed pending ones on Polygon. I poked around in my browser console and found that they were using their mempool APIs (which I didn’t want to pay for) to serve this, and their API keys were leaked in the requests. The explorer is down now, but here’s a snapshot from wayback machine: https://web.archive.org/web/20231201172934/https://4337.blocknative.com/
I dug through the network requests and found a sequence of websocket messages that would authenticate me to the endpoint, and wrote up a simple script using
ethers.js to run the strategy. What I developed based on the network requests:
Immediately, people noticed:
Some tricks
- I was only landing in the same block as the target tx about 50% of the time. I wanted to blast my frontrun txs as fast and wide as possible to make sure I could consistently land in the same block. I gathered a list of public polygon RPCs and made sure to send my txs asynchronously to as many as possible.
- Merkle was goated — they have a tx injection endpoint for Polygon that was my secret sauce for a while. They add this tx into the mempools of their network of nodes and broadcast it to their peers.
- Nonces for transactions have to be continuous and monotonically increasing. For example, if I send two transactions with nonce 3 and then nonce 5, nodes cannot include my nonce-5-tx because they haven’t yet seen a tx from me of nonce 4.
- I kept 8 addresses that I rotated between to send my txs, so that the average time between transactions per address went from ~3 to ~24 seconds, which was sufficient.
- Sometimes addresses would still get stuck if there were gas spikes on chain. For example, I sent a tx with 30 gwei, but the gas would sometimes suddenly spike to 40 gwei. To address this, I simply had a function on a timer that would check if any addresses were stuck, and check whether the next nonce the network expected aligned with my internal counter. If an address was stuck, I’d just invalidate it and wait for it to become unstuck. I suppose I could’ve “unstuck” it by sending a replacement transaction (0 ETH transfer) with higher gas (implemented this later!).
I was sending a tx every 3 seconds and found that nodes would sometimes not propogate my transactions fast enough for the entire network to see it before I sent my next tx. I was essentially sending transactions so fast, and multiple in the same block, that I couldn’t count on the network to reliably distribute them in the order I sent.
Extras
When you have pending transactions, getting the next nonce is tough. Glad that
ethers.js makes this easy with the pending parameter, and alloy-rs has an equivalent pattern.- I made do with websocket mempool APIs which meant I didn’t have to run my own node. To be fair, I did try running my own Polygon node (bor) with some free Azure credits I had, but the sync time was insane.
Writing v2 of the Bot
BlockNative discontinued their services. I experimented with Bitquery and Alchemy to get a websocket stream of the mempool, but both had very high latency and failed to pick up some transactions. By the time I’d receive and send my competing frontrun, it’d already be too late.
Alas, I needed to run my own node. To do this, I frankensteined some parts of reth (a highly modular ethereum node written in rust) together and added a websocket endpoint that would serve new txs from the mempool.
Big shoutout to Chainbound and Merkle — both use reth in production and had fantastic blogs about how to get started. In particular:
- [Merkle] Modifying reth to build the fastest transaction network on BSC and Polygon — incredible resource that lays out the steps to get a network-only node up and running
- [Chainbound] Diving into the Reth p2p stack — walks through how we can get a stream of pending transactions, requests from peers, and network events.
A few complications arose:
- If a peer requests data from us, i.e. like GetBlockHeaders or GetBlockBodies, we need to be able to return that data. Otherwise we’ll get disconnected.
Solution
Left as an exercise to the reader. If we don’t want to run a full node, is there another source where we can get the data we need to craft a response?
- Polygon nodes were stubborn in connecting. HUGE thank you to Merkle who offered me some support in their discord server and put me on the right track: turns out that Polygon still broadcasts new blocks over the network like proof-of-work. When we set the network stack in reth to use PoS, it treats this as a protocol violation and disconnects those peers. Setting back to PoW seems to work.
Once these were resolved, we were up and running.

The reth node was so good that I was winning more than 80% of frontruns.
I migrated from AWS free tier to Hetzner and refactored so that I could have multiple reth network nodes running and they’d forward to the same execution engine written in Typescript.
Talking to others

After BlockNative removed their mempool streaming APIs, a few people were looking for alternatives. I reached out and collected interest for a hosted replacement service.
I noticed a lot of txs I was frontrunning involved the transfer of a Piggybox NFT.
I reached out to EARN’M, the team behind Piggybox, to let them know whichever bundler service they were using was overpaying for gas, and I’d write them a better bundler, but their ‘support’ didn’t get back to me after forwarding my message to their team.
Writing v3 (incomplete) 🦀
I started porting my execution engine from Typescript to Rust since it seemed like the natural next step (alloy-rs is great!), but I found that I was making a tradeoff between prototyping/experimentation speed and system reliability.
Rust would have been great — it cut my critical path down to <3 ms in my experiments. But it was much harder for me to work with to quickly test some new logic or refactor.
Facing Competition
The frontrunning was not without competition! In summer of 2024, I faced competition from another searcher that had the following addresses:
These addresses aren’t listed on BundleBear’s operator registry either.
They ceased their operations for the most part after I wrote my reth network node.
Another searcher (let’s call them searcher B) started operating about a month ago (April/May 2025). Their execution is very, very good. In particular, they have the ability to pick up all transactions on Polygon very well, and they can immediately outbid any competing frontruns.
For example, imagine Alchemy posts a
handleOps transaction with 30 gwei. I’d send a frontrun for +1 wei and searcher B would then send a frontrun for +2 wei. Or if I waited until searcher B’s frontrun came through (+1 wei), I’d bid +2 wei, and then searcher B would immediately do either of the following:- If they can replace their tx directly by bidding 10%+ more gas price, they’d do that
- If cheaper, replace their tx with a zero ETH self-transfer (cancel) and resend with +3 wei
I’d get into bidding wars with this searcher, but they’d almost always win because of their superior network coverage:
I had some back-and-forths with this searcher:
- I was curious how they were identifying 4337 calls. I created a contract that would just forward all calls to the Polygon entrypoint, and sent transactions to this proxy contract instead.
- Searcher B didn’t pick up my transactions for a few hours. I could consistently outbid them when I saw their txs come in. But after ~8 hours or so, they upgraded their system to pick up all transactions to proxy contracts.
- How did they know a transaction was going to be proxied? Did they examine the call trace of all txs to all addresses? I thought about what tricks I’d use — could it be possible they were looking for the
handleOpsfunction selector? Sure enough, I made a proxy contract to append the function selector before proxying the call, and left out the function selector in the calldata to the proxy, and searcher B didn’t pick it up again. - They fixed this within a few hours as well.
I believe they examine the trace of every transaction now. Or they just have a list of all my addresses and get their system to watch all new proxy contracts I deploy or txs I send.
If you’re searcher B: congrats! You win. Reach out to me and I’d like to talk to you further. To be honest, if you’re capable of investing time into this, at your skill level, you should be doing something else that’d surely make more money. There’s an upper cap on how many userOps are sent on Polygon and the total profits of this strategy.
For me, I think if I had taken all the time I spent on this and prepared for some interviews more, I could’ve surely passed the final rounds for better trading firms with massive sign on bonuses, more than I earned from this entire strategy 😄. But the adventure was worth it.
Where this space is going to go
- Thank you to 0xKofi! BundleBear is a great resource for tracking 7702 and 4337 adoption. I used to check it weekly to get an overview. Their analytics will continue to be very useful to the community.
- 0xKofi published a great article about 4337 frontrunning back in September 2024: https://web.archive.org/web/20241012214743/https://www.bundlebear.com/posts/polygon-mev
- I wanted to talk to you for the longest time about this. I left you a few responses on X a few times 😄
- FastLane saw the frontrunning and noticed that it generated revenue for validators, and rather than attempting to stop the frontrunning, they built some infrastructure to democratize it so anyone could frontrun bundles. They added an endpoint to their PFL protocol that offers revert protection for bundlers but subject to a speedbump of 2 blocks where the transactions would be broadcasted publicly. This ensures all UserOps are made public and every bundler can attempt to frontrun, maximizing revenue to validators while addressing bundler reverts.
- Thogard of FL also talks about the intersection of MEV and 4337 here, it’s a great listen:
- FL is now working on AA infra on Monad — they have a LST that integrates with their bundler services, check it out here: https://github.com/FastLane-Labs/4337-bundler-paymaster-script
- Pimlico tried to address by frontrunning with a couple ways:
- They looked for private relays on Polygon but since there isn’t proposer-builder-separation (PBS), there’s no way to privately submit transactions to block builders, as there aren’t any. They mentioned trying Merkle and Bloxroute, but both simply broadcast into the public mempool since there aren’t any private relays.
- They also ran experiments using FastLane’s conditional endpoint, but found that its validator coverage wasn’t high enough for their use case.
- They’ve found a good solution now! The EntryPoint will call a Paymaster contract when paymaster data is included, and there’s a EOA address check that will be performed. This prevents frontrunning and will result in a revert if tried. Here’s an example.

- In a world where smart wallets dominate, UserOps will generate more types of MEV than just overpaying for bundler gas. For example, consider a UserOp that makes a large trade on a DEX or overborrows on a lending protocol. Bundlers will have to evolve to capture these types of MEV, or searchers will have to evolve to capture backruns and liquidations that occur because an internal call inside a UserOps. This orderflow would be valuable.
As the landscape evolves, there will always be new cracks to exploit and strategies to refine. For those willing to get their hands dirty, there’s always another opportunity waiting in the mempool.