Quicksilver vs Iron Man - The $100M+ Sandwich Heist
Danger (In 0.12 seconds…)
Quicksilver saw Iron Man’s trade in the public mempool,
bought before him,
sold after him,
and walked away with risk-free profit.
Iron Man never even noticed.
One transaction in the mempool.
One bot faster than light.
One victim - screwed.
--- 🥪 ---
TL;DR
Vulnerability: Public mempool → anyone can see and reorder your trade
Attack: Front-run (buy) → Victim trade → Back-run (sell)
Profit: 0.5–10% per sandwich → billions yearly
Real losses: $1B+ across Uniswap, SushiSwap, etc.
Fixes:
- Commit-Reveal
- Batch auctions
- Private relays (Flashbots Protect)
- Tight slippage
Story Time - The Meme-Speed Heist
Tony Stark wants to buy 1,000 $STARK tokens on StarkSwap.
He submits:
“Buy 1,000 $STARK with 10 ETH - slippage 1%”
Transaction lands in the public mempool.
JARVIS: “Sir, the mempool is public. Anyone can see this.”
Tony: “Relax, JARVIS. Who’s fast enough to-”
Quicksilver (MEV bot) sees it instantly.
He calculates:
- Tony’s trade will push price up 8%
- Perfect sandwich opportunity
Quicksilver fires three transactions:
- Front-run: Buy 100 $STARK with higher gas → price +4%
- Tony’s trade: Executes at worse price → gets 7% fewer tokens
- Back-run: Sell 100 $STARK → pocket 3.9% profit
All in one block.
Tony pays 10 ETH → gets 930 $STARK instead of 1000.
Quicksilver earns 0.39 ETH - risk-free.
He does this 10,000 times a day.
That’s $50M/year from one bot.
The Vulnerable DEX - StarkSwap.sol
39 collapsed lines
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract StarkSwap { IERC20 public token;
uint256 public reserveETH = 1000 ether; uint256 public reserveToken = 10000 ether;
mapping(address => uint256) public balances;
constructor(IERC20 _token) { token = _token; }
// INTERNAL AMM MATH
function getAmountOut(uint256 inputETH) public view returns (uint256) { // x * y = k return (inputETH * reserveToken) / (reserveETH + inputETH); }
function getAmountOutToken(uint256 inputToken) public view returns (uint256) { return (inputToken * reserveETH) / (reserveToken + inputToken); }
function _swap(uint256 ethIn, uint256 tokenOut) internal { reserveETH += ethIn; reserveToken -= tokenOut; }
function _swapTokens(uint256 tokenIn, uint256 ethOut) internal { reserveToken += tokenIn; reserveETH -= ethOut; }
// BUY TOKENS
function buy(uint256 minTokens) external payable { uint256 tokensOut = getAmountOut(msg.value); require(tokensOut >= minTokens, "Slippage");
_swap(msg.value, tokensOut); token.transfer(msg.sender, tokensOut); }
// SELL TOKENS
function sell(uint256 tokenAmount, uint256 minEth) external returns (uint256 ethOut) { ethOut = getAmountOutToken(tokenAmount); require(ethOut >= minEth, "Slippage");
// Pull tokens from user require(token.transferFrom(msg.sender, address(this), tokenAmount), "Transfer failed");
_swapTokens(tokenAmount, ethOut);
payable(msg.sender).transfer(ethOut); }}One public transaction → entire intent exposed.
Foundry Test - Watch Quicksilver Win
35 collapsed lines
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "forge-std/Test.sol";import {StarkSwap} from "../../src/Sandwich-MEV/StarkSwap.sol";import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 { constructor() ERC20("StarkToken", "STRK") { _mint(msg.sender, 1_000_000 ether); } function mint(address to, uint256 amount) external { _mint(to, amount); }}
contract StarkSwapTest is Test { MockERC20 token; StarkSwap dex;
address IronMan = makeAddr("IronMan"); address Quicksilver = makeAddr("Quicksilver");
function setUp() public { token = new MockERC20(); dex = new StarkSwap(token);
// DEX liquidity token.mint(address(dex), 10_000 ether);
// User balances vm.deal(IronMan, 10 ether); vm.deal(Quicksilver, 10 ether); }
function testSandwichAttack() public { // FRONT RUN vm.startPrank(Quicksilver); dex.buy{value: 1 ether}(1); uint256 Q_tokens = token.balanceOf(Quicksilver); vm.stopPrank();
// VICTIM TRADE vm.prank(IronMan); dex.buy{value: 1 ether}(1); uint256 IronManTokens = token.balanceOf(IronMan);
// BACK RUN vm.startPrank(Quicksilver); token.approve(address(dex), Q_tokens); uint256 ethBack = dex.sell(Q_tokens, 0); vm.stopPrank();
uint256 profit = ethBack - 1 ether;
console.log("Quicksilver tokens bought:", Q_tokens); console.log("IronMan tokens received:", IronManTokens); console.log("ETH returned to Quicksilver:", ethBack); console.log("Attacker profit:", profit);
assertLt(IronManTokens, 9990 ether); // victim gets worse price assertGt(profit, 0); // attacker profits }}-
Profit: 3–10% per sandwich
-
Logs:
- Quicksilver tokens bought: 9.990009990009990009
- IronMan tokens received: 9.970069850309371267
- ETH returned to Quicksilver: 1.001998000003992007
- Attacker profit: 0.001998000003992007
-
One test → one ruined trade.
Three Ways to Beat Quicksilver
1. Commit-Reveal - Hide Your Intent
// Commit phasefunction commit(bytes32 hash) external { ... }
// Reveal phasefunction reveal(uint256 amount, uint256 minTokens, uint256 salt) external payable { require(keccak256(abi.encode(amount, minTokens, salt)) == commitments[msg.sender]); // Execute trade - Quicksilver blind}Quicksilver can’t see amount → can’t sandwich.
2. Batch Auctions - Everyone Gets Same Price
function submitOrder(uint256 amount, uint256 maxPrice) external { orders[msg.sender] = Order(amount, maxPrice);}
function executeBatch() external onlyOwner { uint256 totalETH = 0; // Clear all at average price}No ordering → no sandwich.
3. Private Relays - Flashbots Protect
Submit via Flashbots Protect RPC → never hits public mempool.
Quicksilver blind.
Real-World Sandwich Victims
| Victim | Year | Loss per tx | Total extracted |
|---|---|---|---|
| Uniswap traders | 2024 | 0.5–5% | $500M+ |
| SushiSwap | 2023 | ~8% | $200M+ |
| Average DeFi user | Daily | $10–10,000 | Billions yearly |
Source: Flashbots MEV-Share data
Auditor’s Sandwich Checklist
Warning
- Large trades visible in public mempool? → High
- No slippage protection
(<0.5%)?→ Critical - No commit-reveal or batching? → High
- Tested with Flashbots bundle simulation? → Must do
- Users warned about MEV? → UX fail
Quiz - Are You Faster Than Quicksilver?
Exercise (How does Quicksilver know Tony’s trade amount?)
a) He hacks Stark’s wallet
b) Public mempool shows full tx data
c) He bribes the miner
d) Chainlink oracle
Answer
b) Every pending tx is public - including amount
Exercise (Best user-side protection?)
a) Use 1inch
b) Set 0.1% slippage
c) Use Flashbots Protect RPC
d) Trade on CEX
Answer
c) Private tx → invisible to bots
Tip
Full repo - DEX + attacker + 3 defenses + tests:
github.com/thesandf/thesandf.xyz
Quicksilver didn’t need super speed.
He just needed the mempool to be public.
Don’t be Iron Man.
Hide your trades.
Use commit-reveal.
Use private relays.
Or Quicksilver will eat your sandwich - and your profits.