# Sandwich Attacks और MEV से सुरक्षा कैसे करें
जब आप किसी automated market maker (AMM) पर tokens swap करते हैं, तो आपकी transaction mine होने से पहले public mempool में बैठी रहती है। उस mempool को देखने वाला कोई भी आपका intent पढ़ सकता है: amount, token pair, और सबसे अहम, आपकी slippage tolerance। एक sandwich attack ठीक इसी visibility का फायदा उठाती है। यह MEV (Maximal Extractable Value) के सबसे आम रूपों में से एक है, और किसी contract bug के विपरीत, इसके लिए आपके call किए जा रहे code में कोई खामी जरूरी नहीं है। बस इतना काफी है कि आप एक profitable trade को खुले रूप में broadcast कर दें।
यह लेख इसका mechanism समझाता है, बताता है कि एक ढीली slippage bound चुपचाप एक searcher को आपका पैसा क्यों दे देती है, और वे practical defenses क्या हैं जिन्हें आप user के तौर पर और protocol designer के तौर पर लागू कर सकते हैं।
Sandwich attack कैसे काम करती है
एक constant-product pool जैसा AMM किसी swap की कीमत x * y = k curve पर तय करता है। reserves की तुलना में आपका trade जितना बड़ा होगा, price उतना ही आपके खिलाफ हिलेगा। यह हलचल price impact है, और दिए गए reserves के लिए यह deterministic होती है।
एक sandwich आपकी transaction के इर्द-गिर्द क्रमबद्ध तीन transactions होती हैं:
Attacker उसी price से मुनाफा कमाता है जिसे उसने खुद हिलाया, और इसकी कीमत आप बिगड़े execution के रूप में चुकाते हैं। उसे भविष्य की भविष्यवाणी नहीं करनी होती। उसे केवल आपकी pending transaction और अपनी transactions को उसके आसपास क्रम में रखने की क्षमता चाहिए।
एक ठोस उदाहरण
मान लीजिए किसी pool में 1,000 ETH और 2,000,000 USDC हैं। आप 100,000 USDC से ETH खरीदना चाहते हैं। सादे curve पर आपको लगभग 47.6 ETH मिलते। अगर attacker पहले 200,000 USDC खर्च करके ETH खरीद ले, तो pool खिसक जाता है, और आपके वही 100,000 USDC अब काफी कम ETH खरीदते हैं। फिर attacker अपना ETH उस फूली हुई कीमत में वापस बेच देता है। आपको जो मिलना चाहिए था और जो असल में मिला, उसके बीच का फासला उसका margin है, gas घटाकर।
ढीली slippage bound क्यों नाकाम रहती है
हर गंभीर swap function एक minimum-output argument लेता है। Uniswap V2 शैली के routers में यह ऐसा दिखता है:
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
amountOutMin आपकी सुरक्षा है: अगर आपको कम मिलने वाला हो तो transaction revert हो जाती है। दिक्कत यह है कि आप उसे कितनी छूट देते हैं। कई front ends एक आरामदायक buffer default रखते हैं, कभी 0.5 percent, कभी 1 percent, या उससे भी बुरा, चरम मामलों में खुला 49 percent तक। यह buffer आपके लिए safety margin नहीं है। यह वह अधिकतम मुनाफा है जिसे निकालने की अनुमति आपने attacker को दी है।
ऐसे सोचिए: एक sandwich केवल उस बिंदु तक profitable है जहां तक आपका trade अब भी आपके amountOutMin को पार करता है। एक कसी हुई bound उस window को छोटा कर देती है। एक ऐसा नंबर तय करें जो बाजार की असल स्थिति और असली volatility दर्शाए, न कि कोई आलसी गोल आंकड़ा। अगर आप शांत pool पर 3 percent slippage की छूट देते हैं, तो आपने एक searcher को उसका बड़ा हिस्सा लेने का न्योता दे दिया है।
// कसी हुई tolerance के साथ मौजूदा quote से amountOutMin निकालें।
// उदाहरण: 30 basis points (0.30%)।
uint256 quoted = router.getAmountsOut(amountIn, path)[path.length - 1];
uint256 amountOutMin = quoted * 9970 / 10000;
Deadline भी मायने रखता है। एक बासी deadline किसी transaction को लटकने और भविष्य की, manipulate की गई कीमत पर execute होने देता है। इसे छोटा रखें, कुछ मिनटों के क्रम में।
Users के लिए सुरक्षा
amountOutMin को कसने से इनाम का आकार घटता है, पर आपका trade छिपता नहीं। मजबूत हल यह है कि खुले रूप में broadcast करना ही बंद कर दें।
- Private order flow / Flashbots Protect: public mempool के बजाय आप एक private relay के जरिए submit करते हैं जो transaction सीधे block builders को भेजता है। Searchers इसे inclusion से पहले नहीं देख पाते, इसलिए sandwich करने को कुछ बचता ही नहीं। एक अकेले swapper के लिए यह सबसे प्रभावी सुरक्षा है।
- कसा slippage और छोटा deadline: defense in depth, तब भी जब आप private route इस्तेमाल करें, क्योंकि सभी paths पूरी तरह private नहीं होते।
- बड़े trades को बांटें: एक बड़े swap को छोटे टुकड़ों में बांटने से प्रति-transaction price impact घटता है, पर कुल gas बढ़ता है। सोच-समझकर इस्तेमाल करें।
- गहरी liquidity वाले pools पर trade करें: वही नाममात्र trade तब price को कम हिलाता है जब reserves बड़े हों, जिससे attacker के लिए पतला margin बचता है।
Protocol designers के लिए सुरक्षा
अगर आप कोई ऐसा contract बनाते हैं जो users की ओर से swap करता है (एक vault, एक router, एक rebalancer), तो जिम्मेदारी आप पर आ जाती है। कभी भी amountOutMin = 0 के साथ swap call न करें। यह अकेली line हर interaction पर value निकाल लेने का स्थायी न्योता है।
असली minimums आगे भेजें, zero hardcode न करें
// Anti-pattern: caller के पास कोई सुरक्षा ही नहीं।
router.swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);
// बेहतर: caller से specify कराएं, और एक freshness window लागू करें।
function rebalance(uint256 amountIn, uint256 minOut, uint256 deadline) external {
require(deadline >= block.timestamp, "expired");
require(minOut > 0, "minOut required");
router.swapExactTokensForTokens(amountIn, minOut, path, address(this), deadline);
}
TWAP-aware बनें, spot-price पर भोले न रहें
अगर आपका contract किसी फैसले के लिए price पढ़ता है (collateral valuation, fair-value check, swap पर एक sanity bound), तो किसी AMM की तात्कालिक spot price पर कभी भरोसा न करें। spot price ठीक वही है जिसे एक sandwich manipulate करती है। एक time-weighted average price (TWAP) इस्तेमाल करें, ऐसी window पर जो इतनी लंबी हो कि उस अवधि भर price को हिलाए रखना, attack से होने वाली कमाई से ज्यादा महंगा पड़े।
Uniswap V3 cumulative tick data देता है जिसे आप TWAP के लिए पढ़ सकते हैं:
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 600; // 10 मिनट पहले
secondsAgos[1] = 0; // अभी
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
int24 avgTick = int24(tickDelta / int56(uint56(600)));
// avgTick को standard tick math से price में बदलें, फिर उसे इस्तेमाल करें।
एक TWAP manipulation की लागत बढ़ा देता है, क्योंकि attacker को price को fair value से दूर एक नहीं बल्कि कई blocks तक रोके रखना पड़ता है। यह जादू नहीं है। एक छोटी window या एक पतला pool अब भी हिलाया जा सकता है, इसलिए window का आकार दांव पर लगी value के हिसाब से रखें।
Order intent के लिए commit-reveal
जब trade खुद एक ही chain पर public होना ही हो, तो आप intent को समय में छिपा सकते हैं। एक commit-reveal scheme में, user पहले अपने order का hash submit करता है (commit) और केवल बाद में plaintext submit करता है (reveal)। जो searcher commit देखता है वह ऐसे trade को front-run नहीं कर सकता जिसकी दिशा और आकार अज्ञात हों।
mapping(address => bytes32) public commitments;
function commit(bytes32 orderHash) external {
commitments[msg.sender] = orderHash;
}
function reveal(uint256 amountIn, uint256 minOut, uint256 nonce) external {
bytes32 expected = keccak256(abi.encodePacked(amountIn, minOut, nonce, msg.sender));
require(commitments[msg.sender] == expected, "bad reveal");
delete commitments[msg.sender];
// reveal किए गए parameters के साथ swap execute करें
}
Commit-reveal एक transaction और latency जोड़ता है, और यह अकेले back-running हल नहीं करता, इसलिए यह एक अकेले retail swap के बजाय batch auctions और order systems में ज्यादा फिट बैठता है। इसे एक building block मानें, drop-in fix नहीं।
एक छोटी मानसिक checklist
- क्या मेरा
amountOutMinएक ताजा quote से, कसी और यथार्थवादी tolerance के साथ निकला है?
- क्या मेरा
deadlineछोटा है?
- क्या मैं public mempool को broadcast कर रहा हूं, या किसी private relay के जरिए?
- अगर कोई contract फैसले के लिए price पढ़ता है, तो क्या वह TWAP है, spot read नहीं?
- क्या मेरे system में कहीं कोई swap path minimum output के रूप में
0पास करता है?
इसे खुद अभ्यास करें
यह सब आत्मसात करने का सबसे तेज तरीका है पहले attack बनाना और फिर उससे बचाव करना। app.solingo-blockchain.xyz पर आप AMM mechanics से गुजर सकते हैं, constant-product curve पर price impact निकाल सकते हैं, और ऐसी swap logic लिख सकते हैं जो एक असली amountOutMin तय करे और spot के बजाय TWAP पढ़े। किसी exercise में अपनी ही ढीली slippage को sandwich होते देखना यह सबक अक्सर पक्का कर देता है।