Development of Sandwich Attack Protection System
Sandwich attack—form of MEV (Maximal Extractable Value), where attacker sees pending user transaction in mempool, places their transaction with higher gas before it (frontrun) and one right after (backrun). User gets significantly worse exchange rate, difference goes to attacker.
In Uniswap-like AMM this works straightforwardly: attacker buys token before user (price rises), sells after (price returns down). Difference—MEV bot profit. According to EigenPhi, major sandwich bots on Ethereum earned hundreds of thousands of dollars weekly at expense of regular users.
Attack Mechanics and Weak Points
To understand protection, must clearly understand where vulnerability lies.
Public mempool: transactions visible to all before block inclusion. MEV bot reads swap parameters (token address, amount, slippage tolerance) and calculates attack profitability.
High slippage tolerance: user specifies acceptable price deviation (e.g., 5%). Higher tolerance—wider sandwich window. Swap with 1% slippage much harder to attack.
Deterministic price impact: for AMM with known formula (x*y=k or stable curve) attacker precisely calculates how much to buy before victim for maximum profit.
User-Level and Protocol-Level Defenses
Private RPC and MEV-Protection Providers
Simplest protection: send transactions not to public mempool but directly to block builders through private channels.
Flashbots Protect: transactions go into Flashbots bundle. Attacker doesn't see them in public mempool. Free for user.
MEV Blocker: service from CoW Protocol. Transactions sent to multiple searchers who compete for best execution (refund part of MEV back to user).
Bloxroute: paid service with protected channels to miners/validators.
// Using Flashbots Protect via ethers.js
const { FlashbotsBundleProvider } = require('@flashbots/ethers-provider-bundle');
const { ethers } = require('ethers');
async function protectedSwap(swapParams, wallet, provider) {
// Connect to Flashbots relay
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
wallet, // auth signer (can be separate wallet)
'https://relay.flashbots.net'
);
// Build swap transaction as usual
const swapTx = await buildSwapTransaction(swapParams);
// Send through Flashbots—transaction doesn't reach public mempool
const bundleSubmission = await flashbotsProvider.sendPrivateTransaction(
{
signer: wallet,
transaction: swapTx
},
{ maxBlockNumber: (await provider.getBlockNumber()) + 10 }
);
const receipt = await bundleSubmission.wait();
return receipt;
}
Minimize Slippage Tolerance
Protocols should recommend and apply reasonable slippage by default instead of inflated.
// In smart contract: hard limit on maximum slippage
contract SlippageProtectedRouter {
uint256 public constant MAX_SLIPPAGE_BPS = 300; // 3% maximum
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to
) external returns (uint256[] memory amounts) {
// Calculate current price and check slippage
uint256 expectedOut = getAmountOut(amountIn, path);
uint256 slippageBps = (expectedOut - amountOutMin) * 10000 / expectedOut;
require(slippageBps <= MAX_SLIPPAGE_BPS, "Slippage too high");
return _executeSwap(amountIn, amountOutMin, path, to);
}
}
Commit-Reveal Scheme
User in first transaction publishes commitment (hash of swap parameters), in second—reveals. MEV bots don't know parameters until second transaction, when too late to insert before.
contract CommitRevealSwap {
mapping(bytes32 => Commitment) public commitments;
struct Commitment {
address user;
uint256 blockNumber;
bool revealed;
}
uint256 public constant MIN_BLOCKS_BEFORE_REVEAL = 1;
uint256 public constant MAX_BLOCKS_BEFORE_REVEAL = 10;
// Step 1: user publishes hash of parameters
function commit(bytes32 commitHash) external {
commitments[commitHash] = Commitment({
user: msg.sender,
blockNumber: block.number,
revealed: false
});
}
// Step 2: reveal and execute
function reveal(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
bytes32 salt
) external {
bytes32 commitHash = keccak256(abi.encodePacked(
msg.sender, amountIn, amountOutMin,
keccak256(abi.encode(path)), salt
));
Commitment storage commitment = commitments[commitHash];
require(commitment.user == msg.sender, "Not your commit");
require(!commitment.revealed, "Already revealed");
require(
block.number >= commitment.blockNumber + MIN_BLOCKS_BEFORE_REVEAL,
"Too early"
);
require(
block.number <= commitment.blockNumber + MAX_BLOCKS_BEFORE_REVEAL,
"Expired"
);
commitment.revealed = true;
_executeSwap(amountIn, amountOutMin, path, msg.sender);
}
}
Downside: two-step process creates UX friction. Applicable for large swaps, inconvenient for frequent small operations.
Batch Auctions: Architectural Solution via CoW Protocol
CoW Protocol solves sandwich problem architecturally via batch auctions. Instead of immediate execution of each swap:
- Swap orders collected for period (several blocks)
- Off-chain solver finds optimal execution for entire batch (CoW—Coincidence of Wants: if A exchanges ETH for USDC and B exchanges USDC for ETH—can execute directly)
- Single transaction with batch results published on-chain
- All batch participants get uniform clearing price
In batch auction there's no point for sandwich: attacker can't insert between order and execution because they're separated in time and executed as batch.
Regular AMM:
Block N: frontrun_buy → user_swap → backrun_sell
(attacker earned at expense of user_swap)
CoW Batch Auction:
Block N: submit_order(user1), submit_order(user2), ...
Block N+3: solver publishes settlement_tx with uniform price
(no place for frontrun/backrun inside batch)
Dynamic Slippage Based on On-Chain Conditions
Smart contract or frontend can dynamically calculate reasonable slippage based on current market volatility and swap volume.
async function calculateSafeDynamicSlippage(
tokenIn, tokenOut, amountIn, provider
) {
// Get TWAP from Uniswap V3 oracle
const [twapPrice, spotPrice] = await Promise.all([
getTWAPPrice(tokenIn, tokenOut, 1800, provider), // 30-min TWAP
getSpotPrice(tokenIn, tokenOut, provider)
]);
// Current spot deviation from TWAP
const currentDeviation = Math.abs(spotPrice - twapPrice) / twapPrice;
// Estimate price impact from our swap
const priceImpact = await estimatePriceImpact(tokenIn, tokenOut, amountIn, provider);
// Base slippage + volatility buffer
const baseSlippage = priceImpact * 1.2; // 20% buffer above price impact
const volatilityBuffer = currentDeviation * 0.5;
const recommendedSlippage = Math.min(
baseSlippage + volatilityBuffer,
0.03 // maximum 3%
);
return {
recommendedSlippageBps: Math.ceil(recommendedSlippage * 10000),
priceImpact: priceImpact,
currentVolatility: currentDeviation
};
}
Monitoring and Attack Detection
Protection system should include monitoring component: tracking specific MEV bots, user loss statistics, alerts on sandwich activity spikes.
// Detect sandwich pattern via transaction analysis in block
async function detectSandwichInBlock(blockNumber, provider, uniswapAddress) {
const block = await provider.getBlock(blockNumber, true);
const uniswapTxs = block.transactions.filter(
tx => tx.to?.toLowerCase() === uniswapAddress.toLowerCase()
);
const sandwiches = [];
for (let i = 1; i < uniswapTxs.length - 1; i++) {
const prev = uniswapTxs[i - 1];
const current = uniswapTxs[i];
const next = uniswapTxs[i + 1];
// Sandwich signs: prev and next from same address,
// current—from different, opposite directions
if (prev.from === next.from && prev.from !== current.from) {
const prevDecoded = decodeSwap(prev.data);
const nextDecoded = decodeSwap(next.data);
// Frontrun buys what victim sells
if (prevDecoded && nextDecoded &&
prevDecoded.tokenIn === nextDecoded.tokenOut) {
sandwiches.push({
attacker: prev.from,
victim: current.from,
frontrunTx: prev.hash,
victimTx: current.hash,
backrunTx: next.hash,
estimatedProfit: calculateSandwichProfit(prevDecoded, nextDecoded)
});
}
}
}
return sandwiches;
}
Development Timeline
Integrating Flashbots Protect into existing swap interface—1–2 weeks. Commit-reveal mechanism in smart contract—2–3 weeks including tests. Dynamic slippage with TWAP oracle—2–3 weeks. Sandwich detection monitoring system—3–4 weeks. Full batch auction protocol per CoW model—2–4 months.







