Uniswap v3 और Sushiswap के पूल्स में एक ही ट्रेडिंग पेयर की कीमतों में अंतर दिखना एक आम बात है। यह आमतौर पर मार्केट में हाई वोलाटिलिटी के समय होता है, जब कोई बड़ा ऑर्डर किसी एक पूल में प्राइस को डंप या पंप कर देता है, और दूसरा पूल तुरंत एडजस्ट नहीं हो पाता। Flash Loan (इंस्टेंट लोन) आपको बिना अपना कैपिटल लगाए इस प्राइस डिफ़रेंस से प्रॉफिट कमाने का मौका देता है।
फ़्लैश लोन EVM पर एक एटॉमिक ट्रांजैक्शन है: आप किसी लैंडिंग प्रोटोकॉल (Aave, Balancer, Uniswap) से $1,000,000 उधार लेते हैं, DEX A पर एसेट सस्ते में खरीदते हैं, DEX B पर महंगे में बेचते हैं, फीस के साथ लोन का प्रिंसिपल अमाउंट वापस करते हैं और अपना नेट प्रॉफिट जेब में डालते हैं। अगर कॉल चेन में एक भी स्टेप फेल होता है या फाइनल बैलेंस रीपेमेंट अमाउंट से कम रहता है, तो EVM पूरे ट्रांजैक्शन को रिवर्ट (revert) कर देता है। आपका नुकसान सिर्फ गैस फीस तक सीमित रहता है।
1. मार्केट मैकेनिक और “रिस्क-फ्री” आर्बिट्रेज के छिपे हुए खतरे
“रिस्क-फ्री” शब्द केवल इस बात पर लागू होता है कि इसमें कोई डायरेक्शनल रिस्क नहीं है और आपका खुद का डिपॉजिट खोने का डर नहीं रहता। हालांकि, असल दुनिया में एक ट्रेडर को कई क्रिटिकल फैक्टर्स का सामना करना पड़ता है।

मुख्य चुनौतियाँ (Pitfalls)
- Slippage (स्लिपेज): फ़्लैश लोन का वॉल्यूम जितना बड़ा होगा, आपका खुद का ट्रेड पूल के अंदर प्राइस को उतना ही ज्यादा हिलाएगा (price impact)। अगर लोन का साइज ऑप्टिमल से ज्यादा हो जाता है, तो पूरा स्प्रेड स्लिपेज में ही खत्म हो जाता है।
- MEV और Front-running: पब्लिक मेमपूल (mempool) में आपके ट्रांजैक्शन को सर्चर बॉट्स (searchers) आसानी से ट्रैक कर लेते हैं। वे ज्यादा गैस फीस (Priority Fee) देकर अपना स्वैप पहले एग्जीक्यूट करा लेते हैं और आपका ट्रेड फ्रंट-रन हो जाता है।
- गैस का खर्च (Gas Overhead): मल्टीपल एक्सटर्नल स्वैप वाले कॉम्प्लेक्स स्मार्ट कॉन्ट्रैक्ट को कॉल करने में 250,000 से 450,000 यूनिट गैस बर्न होती है। Ethereum L1 पर जब गैस 30-50 Gwei चल रही हो, तो ट्रांजैक्शन कॉस्ट छोटे-मोटे स्प्रेड को पूरी तरह खा जाती है।
2. ट्रेड इकोनॉमिक्स और लोन वॉल्यूम कैलकुलेट करने का गणित
सफल आर्बिट्रेज के लिए आपको लोन अमाउंट x को सटीक रूप से कैलकुलेट करना होगा, जिससे आपका नेट प्रॉफिट मैक्सिमम हो सके।
Constant Product पूल्स (x · y = k, जैसे Uniswap v2) में प्राइस रिज़र्व Rx और Ry पर निर्भर करती है। Δx अमाउंट का स्वैप करने पर खरीदार को मिलता है:
Δy = (Ry · Δx · (1 - γ)) / (Rx + Δx · (1 - γ))
जहाँ γ पूल की फीस है (उदाहरण के लिए, 0.3% के लिए 0.003)।
प्रॉफिट का फाइनेंशियल मॉडल
Profit(x) = SwapOutDEX2(SwapOutDEX1(x)) - x - FlashLoanFee(x) - GasCost
प्रमुख प्रोवाइडर्स की फ़्लैश लोन फीस:
- Balancer v2: 0%
- Uniswap v3 (Flash Swaps): पूल टियर पर निर्भर करता है (0.05%, 0.3%, 1.0%)
- Aave v3: 0.05%
3. लिक्विडिटी प्रोवाइडर्स की तुलना
| प्रोवाइडर | लोन फीस | गैस कॉस्ट | खास बातें |
|---|---|---|---|
| Balancer v2 | 0.00% | कम | Vault के जरिए सिंगल एंट्री पॉइंट की जरूरत होती है, शुरुआत के लिए बेस्ट |
| Aave v3 | 0.05% | मीडियम | हाई लिक्विडिटी, दर्जनों नेटवर्क का सपोर्ट (Polygon, Arbitrum, Optimism) |
| Uniswap v3 | 0.05% – 0.30% | ज्यादा | बिना किसी एक्सटर्नल लैंडिंग प्लेटफॉर्म के सीधे पूल से फ़्लैश स्वैप करने की सुविधा |
4. आर्बिट्रेज एग्जीक्यूशन का स्टेप-बाय-स्टेप एल्गोरिदम
- स्प्रेड चेक करना: नोड से WebSocket कनेक्शन या gRPC स्ट्रीम्स के जरिए DEX A (सस्ता प्राइस) और DEX B (महंगा प्राइस) के बीच प्राइस डिफरेंस मॉनिटर करना।
- प्रॉफिट कैलकुलेशन: पूल फीस, Balancer/Aave कट और करंट Base Fee + Priority Fee को ध्यान में रखते हुए ऑप्टिमल लोन साइज
xकैलकुलेट करना। - ट्रांजैक्शन ट्रिगर करना: कॉन्ट्रैक्ट Balancer Vault से फ़्लैश लोन की रिक्वेस्ट करता है।
- फंड्स रिसीव होना: Vault मांगे गए टोकन आर्बिट्रेज कॉन्ट्रैक्ट को भेजता है और
receiveFlashLoanकॉलबैक फंक्शन को ट्रिगर करता है। - स्वैप 1: कॉन्ट्रैक्ट मिले हुए टोकन को DEX A (Uniswap v3) पर भेजता है और बदले में टारगेट एसेट हासिल करता है।
- स्वैप 2: कॉन्ट्रैक्ट मिले हुए टारगेट एसेट को DEX B (Sushiswap/Uniswap v2) पर भेजकर उसे वापस ओरिजिनल टोकन में स्वैप कर लेता है।
- लोन रीपेमेंट: कॉन्ट्रैक्ट लोन की राशि Balancer Vault को वापस ट्रांसफर करने के लिए अप्रूव (approve) करता है या सीधे भेज देता है।
- प्रॉफिट वेरिफिकेशन: कॉन्ट्रैक्ट चेक करता है कि फाइनल बैलेंस शुरुआती बैलेंस से ज्यादा है या नहीं। अगर नहीं, तो ट्रांजैक्शन revert हो जाता है।
5. Solidity में रेडी-टू-यूज स्मार्ट कॉन्ट्रैक्ट (OpenZeppelin + Balancer v2)
यह कॉन्ट्रैक्ट EVM नेटवर्क के लिए लिखा गया है। यह जीरो-फीस फ़्लैश लोन के लिए Balancer v2 का इस्तेमाल करता है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IBalancerVault {
function flashLoan(
address recipient,
IERC20[] memory tokens,
uint256[] memory amounts,
bytes memory userData
) external;
}
interface ISwapRouterV3 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(
ExactInputSingleParams calldata params
) external returns (uint256 amountOut);
}
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
}
contract FlashArbEngine {
using SafeERC20 for IERC20;
address private immutable owner;
IBalancerVault private immutable balancerVault;
ISwapRouterV3 private immutable uniswapV3Router;
IUniswapV2Router private immutable sushiswapRouter;
bool private arbitrageActive;
error NotOwner();
error ZeroAddress();
error IdenticalTokens();
error InvalidAmount();
error ArbitrageActive();
error InvalidCaller();
error InvalidArrayLength();
error InsufficientProfit();
struct ArbParams {
address tokenA;
address tokenB;
uint24 poolFeeV3;
uint256 loanAmount;
uint256 minAmountOutV3; // 1st स्वैप के प्रोटेक्शन के लिए ऑफ-चेन कैलकुलेट किया जाता है
uint256 minProfit;
}
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
constructor(
address _balancerVault,
address _uniswapV3Router,
address _sushiswapRouter
) {
if (_balancerVault == address(0) || _uniswapV3Router == address(0) || _sushiswapRouter == address(0)) {
revert ZeroAddress();
}
owner = msg.sender;
balancerVault = IBalancerVault(_balancerVault);
uniswapV3Router = ISwapRouterV3(_uniswapV3Router);
sushiswapRouter = IUniswapV2Router(_sushiswapRouter);
}
function executeArbitrage(ArbParams calldata params) external onlyOwner {
if (arbitrageActive) revert ArbitrageActive();
if (params.tokenA == address(0) || params.tokenB == address(0)) revert ZeroAddress();
if (params.tokenA == params.tokenB) revert IdenticalTokens();
if (params.loanAmount == 0 || params.minProfit == 0 || params.minAmountOutV3 == 0) revert InvalidAmount();
arbitrageActive = true;
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(params.tokenA);
uint256[] memory amounts = new uint256[](1);
amounts[0] = params.loanAmount;
balancerVault.flashLoan(
address(this),
tokens,
amounts,
abi.encode(params)
);
arbitrageActive = false;
}
function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external {
if (msg.sender != address(balancerVault)) revert InvalidCaller();
if (!arbitrageActive) revert ArbitrageActive();
// Balancer से मिले ऐरे स्ट्रक्चर की वैलिडिटी चेक करना
if (tokens.length != 1 || amounts.length != 1 || feeAmounts.length != 1) {
revert InvalidArrayLength();
}
ArbParams memory params = abi.decode(userData, (ArbParams));
uint256 loanAmount = amounts[0];
uint256 repayAmount = loanAmount + feeAmounts[0];
// स्वैप से पहले का ओरिजिनल बैलेंस कैप्चर करना (लोन + पुराना बचा हुआ फंड शामिल है)
uint256 balanceBefore = IERC20(params.tokenA).balanceOf(address(this));
// 1. Uniswap v3 पर TokenA -> TokenB स्वैप (डाइनैमिक स्लिपेज प्रोटेक्शन)
IERC20(params.tokenA).forceApprove(address(uniswapV3Router), loanAmount);
ISwapRouterV3.ExactInputSingleParams memory swapParams =
ISwapRouterV3.ExactInputSingleParams({
tokenIn: params.tokenA,
tokenOut: params.tokenB,
fee: params.poolFeeV3,
recipient: address(this),
deadline: block.timestamp,
amountIn: loanAmount,
amountOutMinimum: params.minAmountOutV3,
sqrtPriceLimitX96: 0
});
uint256 tokenBBalanceBefore = IERC20(params.tokenB).balanceOf(address(this));
uniswapV3Router.exactInputSingle(swapParams);
uint256 tokenBReceived = IERC20(params.tokenB).balanceOf(address(this)) - tokenBBalanceBefore;
// 2. SushiSwap पर TokenB -> TokenA स्वैप (मिनिमम आउटपुट = repayAmount + minProfit सेट करना)
IERC20(params.tokenB).forceApprove(address(sushiswapRouter), tokenBReceived);
address[] memory path = new address[](2);
path[0] = params.tokenB;
path[1] = params.tokenA;
sushiswapRouter.swapExactTokensForTokens(
tokenBReceived,
repayAmount + params.minProfit,
path,
address(this),
block.timestamp
);
// 3. लोन चुकाने से पहले फंड्स की अवेलेबिलिटी चेक करना
uint256 balanceBeforeRepayment = IERC20(params.tokenA).balanceOf(address(this));
if (balanceBeforeRepayment < repayAmount + params.minProfit) {
revert InsufficientProfit();
}
// 4. Balancer Vault को लोन वापस करना
IERC20(params.tokenA).safeTransfer(
address(balancerVault),
repayAmount
);
// 5. पुराने बैलेंस को ध्यान में रखते हुए नेट प्रॉफिट रिटेंशन वेरीफाई करना
uint256 balanceAfter = IERC20(params.tokenA).balanceOf(address(this));
if (balanceAfter < balanceBefore - loanAmount + params.minProfit) {
revert InsufficientProfit();
}
}
function withdrawToken(address token) external onlyOwner {
if (arbitrageActive) revert ArbitrageActive();
uint256 balance = IERC20(token).balanceOf(address(this));
if (balance == 0) revert InvalidAmount();
IERC20(token).safeTransfer(owner, balance);
}
}स्टैंडर्ड ERC-20 टोकन्स के लिए इस कोड में स्वैप और लोन रीपेमेंट का सीक्वेंस लॉजिकली पूरी तरह सेट है। मेननेट पर डिप्लॉय करने से पहले राउटर एड्रेस, Balancer की रियल-टाइम फीस और specific ट्रेड के लिए minAmountOutV3 की वैल्यूज को डबल-चेक जरूर कर लें।
6. MEV से सुरक्षा (Front-running से बचाव)
आर्बिट्रेज ट्रांजैक्शन को स्टैंडर्ड पब्लिक RPC (Infura, Alchemy) के जरिए पब्लिक मेमपूल में भेजना सीधे तौर पर सैंडविच बॉट्स को प्रॉफिट थाली में सजाकर देने जैसा है। सर्चर्स आने वाले ट्रेड्स को लगातार ट्रैक करते हैं और Front-running / Back-running जैसी तकनीकों का इस्तेमाल करते हैं।
प्रैक्टिकल सॉल्यूशन: Private RPC (Flashbots Builder)
- प्राइवेट एंडपॉइंट्स (
https://rpc.flashbots.net) का इस्तेमाल करके ट्रांजैक्शन सीधे ब्लॉक बिल्डर्स (Flashbots, Beaverbuild, Titan) को भेजें। - Node.js / Python डेवलपर्स बंडल्स (
eth_sendBundle) का इस्तेमाल करें, जिससे वैलिडेटर को दी जाने वाली टिप (Priority Fee) तभी पे होगी जब ट्रेड सफलतापूर्वक एग्जीक्यूट हो जाएगा।
7. Flashbots के जरिए MEV Infrastructure से कनेक्ट करना (Node.js / Viem)
2026 में स्टैंडर्ड eth_sendTransaction का यूज करके पब्लिकली आर्बिट्रेज ट्रांजैक्शन भेजना सीधे-सीधे MEV बॉट्स के हाथों नुकसान उठाने की गारंटी है। अपनी ट्रेड को सेफ रखने का एकमात्र तरीका है — एक एटॉमिक बंडल (Bundle) बनाकर सीधे ब्लॉक बिल्डर्स (Flashbots, Titan, Beaverbuild, BuilderNet) को भेजना।
अगर DEX पर कंडीशंस बदल जाती हैं और ट्रांजैक्शन ब्लॉक में शामिल होने से पहले स्प्रेड खत्म हो जाता है, तो सिमुलेशन स्क्रिप्ट बंडल को ड्रॉप कर देगी और बिल्डर उसे ब्लॉक में शामिल ही नहीं करेगा। इससे आपका एक भी Wei गैस फीस में वेस्ट नहीं होगा।
import {
FlashbotsBundleProvider,
FlashbotsBundleResolution
} from '@flashbots/ethers-provider-bundle';
import { providers, Wallet, utils } from 'ethers';
const CHAIN_ID = 1;
const RELAY_URL = 'https://relay.flashbots.net';
const INITIAL_GAS_LIMIT = 400000;
const GAS_BUFFER_PERCENT = 120;
const PRIORITY_FEE_GWEI = '3';
if (
!process.env.ETH_RPC_URL ||
!process.env.FLASHBOTS_AUTH_KEY ||
!process.env.EXECUTOR_PRIVATE_KEY
) {
throw new Error('[Flashbots] जरूरी एनवायरनमेंट वेरिएबल्स सेट नहीं हैं');
}
const provider = new providers.JsonRpcProvider(process.env.ETH_RPC_URL);
const authSigner = new Wallet(process.env.FLASHBOTS_AUTH_KEY);
const executorWallet = new Wallet(
process.env.EXECUTOR_PRIVATE_KEY,
provider
);
// एक ही Node.js प्रोसेस के अंदर बंडल्स की सीक्वेंशियल प्रोसेसिंग।
let executionQueue = Promise.resolve();
export function queueArbitrageBundle(
arbContractAddress,
calldata,
targetBlockNumber
) {
const task = executionQueue.then(() =>
processBundle(arbContractAddress, calldata, targetBlockNumber)
);
executionQueue = task.catch((error) => {
console.error('[Bundle Queue Error]', error);
});
return task;
}
async function processBundle(
arbContractAddress,
calldata,
targetBlockNumber
) {
if (!utils.isAddress(arbContractAddress)) {
throw new Error('[Flashbots] इनवैलिड आर्बिट्रेज कॉन्ट्रैक्ट एड्रेस');
}
if (
typeof calldata !== 'string' ||
!utils.isHexString(calldata)
) {
throw new Error('[Flashbots] इनवैलिड calldata');
}
if (
!Number.isSafeInteger(targetBlockNumber) ||
targetBlockNumber < 1
) {
throw new Error('[Flashbots] इनवैलिड टारगेट ब्लॉक नंबर');
}
const network = await provider.getNetwork();
if (network.chainId !== CHAIN_ID) {
throw new Error(
`[Flashbots] गलत नेटवर्क: ${network.chainId}; Mainnet (${CHAIN_ID}) की उम्मीद थी`
);
}
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
authSigner,
RELAY_URL,
CHAIN_ID
);
const latestBlock = await provider.getBlockNumber();
if (targetBlockNumber <= latestBlock) {
console.warn(
`[Flashbots] टारगेट ब्लॉक ${targetBlockNumber} पहले ही मिस हो चुका है`
);
return;
}
// पब्लिक RPC के साथ nonce सिंक करना।
// यह कतार (queue) केवल इसी Node.js इंस्टेंस को प्रोटेक्ट करती है।
const nonce = await provider.getTransactionCount(
executorWallet.address,
'pending'
);
const feeData = await provider.getFeeData();
const maxPriorityFeePerGas = utils.parseUnits(
PRIORITY_FEE_GWEI,
'gwei'
);
const maxFeePerGas = feeData.lastBaseFeePerGas
? feeData.lastBaseFeePerGas.mul(2).add(maxPriorityFeePerGas)
: utils.parseUnits('60', 'gwei');
const transaction = {
chainId: CHAIN_ID,
type: 2,
to: arbContractAddress,
data: calldata,
value: 0,
nonce,
gasLimit: INITIAL_GAS_LIMIT,
maxFeePerGas,
maxPriorityFeePerGas
};
// शुरुआती गैस लिमिट के साथ पहला सिमुलेशन।
let signedBundle = await flashbotsProvider.signBundle([
{
signer: executorWallet,
transaction
}
]);
let simulation = await flashbotsProvider.simulate(
signedBundle,
targetBlockNumber
);
if ('error' in simulation) {
console.error(
`[Simulation Error] ${simulation.error.message}`
);
return;
}
if (simulation.firstRevert) {
console.warn(
'[Simulation Revert]',
simulation.firstRevert
);
return;
}
const firstResult = simulation.results?.[0];
if (!firstResult || firstResult.error || firstResult.gasUsed == null) {
console.error(
'[Simulation Error] सक्सेसफुल ट्रांजैक्शन रिजल्ट गायब है'
);
return;
}
const txGasUsed = utils.BigNumber.from(firstResult.gasUsed);
if (txGasUsed.lte(0)) {
console.error('[Simulation Error] इनवैलिड गैस यूसेज');
return;
}
// गैस लिमिट को 20% बढ़ाना (Buffer)।
const adjustedGasLimit = txGasUsed
.mul(GAS_BUFFER_PERCENT)
.div(100);
if (adjustedGasLimit.gt(INITIAL_GAS_LIMIT)) {
console.error(
`[Gas Limit] कैलकुलेटेड लिमिट ${adjustedGasLimit.toString()} शुरुआती लिमिट ${INITIAL_GAS_LIMIT} से ज्यादा है`
);
return;
}
transaction.gasLimit = adjustedGasLimit;
// gasLimit बदलने के बाद फिर से साइन करना।
signedBundle = await flashbotsProvider.signBundle([
{
signer: executorWallet,
transaction
}
]);
// साइन की हुई ट्रांजैक्शन का दोबारा सिमुलेशन।
simulation = await flashbotsProvider.simulate(
signedBundle,
targetBlockNumber
);
if ('error' in simulation) {
console.error(
`[Second Simulation Error] ${simulation.error.message}`
);
return;
}
if (simulation.firstRevert) {
console.warn(
'[Second Simulation Revert]',
simulation.firstRevert
);
return;
}
const secondResult = simulation.results?.[0];
if (
!secondResult ||
secondResult.error ||
secondResult.gasUsed == null
) {
console.error(
'[Second Simulation Error] ट्रांजैक्शन वेरिफिकेशन फेल हो गया'
);
return;
}
// चेक करें कि ब्लॉक अभी तक माइंड/माइन नहीं हुआ है।
const blockBeforeSubmission = await provider.getBlockNumber();
if (targetBlockNumber <= blockBeforeSubmission) {
console.warn(
`[Flashbots] सिमुलेशन के बाद ब्लॉक ${targetBlockNumber} पहले ही मिस हो चुका है`
);
return;
}
// बंडल को केवल स्पेसिफाइड ब्लॉक में ही भेजें।
const submission = await flashbotsProvider.sendRawBundle(
signedBundle,
targetBlockNumber
);
if ('error' in submission) {
console.error(
`[Submission Error] ${submission.error.message}`
);
return;
}
console.log(
`[Flashbots] ब्लॉक ${targetBlockNumber} के लिए बंडल सबमिट किया गया; nonce=${nonce}; gasLimit=${adjustedGasLimit.toString()}`
);
const resolution = await submission.wait();
switch (resolution) {
case FlashbotsBundleResolution.BundleIncluded:
console.log(
`[Success] बंडल ब्लॉक ${targetBlockNumber} में शामिल हो गया`
);
break;
case FlashbotsBundleResolution.BlockPassedWithoutInclusion:
console.log(
`[Missed] ब्लॉक ${targetBlockNumber} बिना बंडल के माइन हो गया`
);
break;
case FlashbotsBundleResolution.AccountNonceTooHigh:
console.error(
'[Nonce Error] एग्जीक्यूटर का Nonce एक्सपेक्टेड से आगे निकल गया है'
);
break;
default:
console.warn(
`[Flashbots] एग्जीक्यूशन रिजल्ट: ${resolution}`
);
}
}8. ऑफ-चैन मॉनिटरिंग और स्प्रेड खोजना (Architecture)
पोलिंग (Polling - जैसे setInterval या REST API) के जरिए अपडेट्स का इंतजार करने से 500–1500 ms का लेटेंसी गैप आ जाता है, जिससे आपके सक्सेसफुल आर्बिट्रेज के चांसेज पूरी तरह खत्म हो जाते हैं। प्रोडक्शन सिस्टम्स में हमेशा Node.js, Go, या Rust पर बनी रिएक्टिव आर्किटेक्चर का इस्तेमाल किया जाता है।

बॉट के मुख्य कंपोनेंट्स (Main Components)
- 1. यूनिफाइड In-Memory State: Uniswap v2 के करंट रिजर्व्स और Uniswap v3 के वेक्टर टिक्स को सीधे Node.js/Rust प्रोसेस की RAM में स्टोर करना।
- 2. पूल लॉग्स की सबस्क्रिप्शन: WebSocket के ज़रिए
Sync(v2) औरSwap(v3) इवेंट्स से रियल-टाइम में कनेक्ट रहना। - 3. इंस्टेंट री-कैलकुलेशन: नया लॉग मिलते ही पूरे कॉन्ट्रैक्ट्स को पोल (poll) करने के बजाय सिर्फ उसी पेयर को री-कैलकुलेट किया जाता है जिसमें बदलाव हुआ है।
// arbitrage-monitor.mjs
// Reactive off-chain Uniswap V2/V3 pool monitor.
// WebSocket events, unified in-memory state, pair-local recalculation.
// Spread detection only; no transaction execution.
import {
createPublicClient,
webSocket,
parseAbi,
getAddress,
} from 'viem';
import { mainnet } from 'viem/chains';
// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------
const RPC_URL = process.env.ETH_WS_URL;
if (!RPC_URL) {
throw new Error('ETH_WS_URL is required');
}
const client = createPublicClient({
chain: mainnet,
transport: webSocket(RPC_URL, {
reconnect: true,
retryCount: 10,
retryDelay: 1000,
keepAlive: {
interval: 15_000,
},
}),
});
const V2_FACTORY = getAddress(
'0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f',
);
const V3_FACTORY = getAddress(
'0x1F98431c8aD98523631AE4a59f267346ea31F984',
);
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
// Set POOLS_JSON to a JSON array of known pools.
// Example:
// [{"version":2,"address":"0x..."},
// {"version":3,"address":"0x...","fee":3000}]
const INITIAL_POOLS = JSON.parse(process.env.POOLS_JSON || '[]');
const V2_FEE_PPM = 3000;
const FEE_DENOMINATOR = 1_000_000;
// -----------------------------------------------------------------------------
// ABIs
// -----------------------------------------------------------------------------
const ERC20_ABI = parseAbi([
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
]);
const V2_FACTORY_ABI = parseAbi([
'event PairCreated(address indexed token0, address indexed token1, address pair, uint256)',
]);
const V2_PAIR_ABI = parseAbi([
'event Sync(uint112 reserve0, uint112 reserve1)',
'function token0() view returns (address)',
'function token1() view returns (address)',
'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
]);
const V3_FACTORY_ABI = parseAbi([
'event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)',
]);
const V3_POOL_ABI = parseAbi([
'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)',
'function token0() view returns (address)',
'function token1() view returns (address)',
'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
'function liquidity() view returns (uint128)',
]);
// -----------------------------------------------------------------------------
// Unified in-memory state
// -----------------------------------------------------------------------------
// pools: checksummed pool address -> pool state
// pairs: normalized token pair -> Set of pool addresses
// unwatchPool: pool address -> unsubscribe function
// initializing: prevents duplicate concurrent initialization
const pools = new Map();
const pairs = new Map();
const unwatchPool = new Map();
const initializing = new Set();
function normalizeAddress(address) {
return getAddress(address);
}
function pairKey(token0, token1) {
const a = normalizeAddress(token0).toLowerCase();
const b = normalizeAddress(token1).toLowerCase();
return a < b ? `${a}:${b}` : `${b}:${a}`;
}
function registerPool(pool) {
pools.set(pool.address, pool);
const key = pairKey(pool.token0, pool.token1);
if (!pairs.has(key)) {
pairs.set(key, new Set());
}
pairs.get(key).add(pool.address);
}
function registerMetadata(pool, token0Metadata, token1Metadata) {
pool.token0Decimals = token0Metadata.decimals;
pool.token1Decimals = token1Metadata.decimals;
pool.token0Symbol = token0Metadata.symbol;
pool.token1Symbol = token1Metadata.symbol;
}
async function readTokenMetadata(address) {
const [decimals, symbol] = await Promise.all([
client.readContract({
address,
abi: ERC20_ABI,
functionName: 'decimals',
}),
client.readContract({
address,
abi: ERC20_ABI,
functionName: 'symbol',
}).catch(() => address.slice(0, 10)),
]);
if (decimals > 36) {
throw new Error(`Unsupported token decimals: ${address}`);
}
return { decimals, symbol };
}
// -----------------------------------------------------------------------------
// Price calculation
// -----------------------------------------------------------------------------
// Prices are token1 per token0, expressed in human-readable token units.
// Number is used only for preliminary ranking, not transaction amounts.
function getV2Price(pool) {
if (pool.reserve0 === 0n || pool.reserve1 === 0n) {
return null;
}
const rawRatio =
Number(pool.reserve1) / Number(pool.reserve0);
return rawRatio *
10 ** (pool.token0Decimals - pool.token1Decimals);
}
function getV3Price(pool) {
if (pool.sqrtPriceX96 === 0n) {
return null;
}
const sqrtRatio =
Number(pool.sqrtPriceX96) / 2 ** 96;
const rawRatio = sqrtRatio * sqrtRatio;
return rawRatio *
10 ** (pool.token0Decimals - pool.token1Decimals);
}
function getPoolPrice(pool) {
if (pool.version === 2) return getV2Price(pool);
if (pool.version === 3) return getV3Price(pool);
return null;
}
function getFeeRate(pool) {
// Uniswap V2: 0.30% swap fee.
// Uniswap V3 fee values are denominated in millionths.
const fee = pool.version === 2 ? V2_FEE_PPM : pool.fee;
return fee / FEE_DENOMINATOR;
}
// -----------------------------------------------------------------------------
// Candidate detection: recalculate only the affected token pair
// -----------------------------------------------------------------------------
function findSpreadCandidates(poolStates) {
const candidates = [];
for (let i = 0; i < poolStates.length; i++) {
for (let j = i + 1; j < poolStates.length; j++) {
const a = poolStates[i];
const b = poolStates[j];
const priceA = getPoolPrice(a);
const priceB = getPoolPrice(b);
if (
priceA === null ||
priceB === null ||
!Number.isFinite(priceA) ||
!Number.isFinite(priceB) ||
priceA <= 0 ||
priceB <= 0
) {
continue;
}
const [buyPool, sellPool, buyPrice, sellPrice] =
priceA <= priceB
? [a, b, priceA, priceB]
: [b, a, priceB, priceA];
const feeBuy = getFeeRate(buyPool);
const feeSell = getFeeRate(sellPool);
if (
feeBuy < 0 || feeBuy >= 1 ||
feeSell < 0 || feeSell >= 1
) {
continue;
}
// Approximate fee-adjusted spread.
// Does not account for price impact or trade size.
const effectiveBuyPrice = buyPrice / (1 - feeBuy);
const effectiveSellPrice = sellPrice * (1 - feeSell);
const grossSpreadPercent =
((sellPrice - buyPrice) / buyPrice) * 100;
const estimatedSpreadPercent =
((effectiveSellPrice - effectiveBuyPrice) /
effectiveBuyPrice) * 100;
if (grossSpreadPercent <= 0) continue;
candidates.push({
token0: a.token0,
token1: a.token1,
token0Symbol: a.token0Symbol,
token1Symbol: a.token1Symbol,
buyPool: buyPool.address,
sellPool: sellPool.address,
buyVersion: buyPool.version,
sellVersion: sellPool.version,
buyPrice,
sellPrice,
grossSpreadPercent,
estimatedSpreadPercent,
});
}
}
return candidates;
}
function onPoolUpdated(pool) {
const addresses = pairs.get(
pairKey(pool.token0, pool.token1),
);
if (!addresses) return;
const states = [];
for (const address of addresses) {
const state = pools.get(address);
if (state) states.push(state);
}
for (const candidate of findSpreadCandidates(states)) {
console.log('[Spread candidate]', {
pair: `${candidate.token0Symbol}/${candidate.token1Symbol}`,
buyPool: candidate.buyPool,
sellPool: candidate.sellPool,
buyVersion: candidate.buyVersion,
sellVersion: candidate.sellVersion,
buyPrice: candidate.buyPrice,
sellPrice: candidate.sellPrice,
grossSpreadPercent: candidate.grossSpreadPercent,
estimatedSpreadPercent: candidate.estimatedSpreadPercent,
});
// Next stage:
// 1. Calculate optimal trade size x*.
// 2. Simulate both swaps against current pool state.
// 3. Deduct gas, flash-loan costs and execution costs.
// 4. Simulate the complete transaction.
// 5. Submit only if expected net profit exceeds the configured threshold.
}
}
// -----------------------------------------------------------------------------
// V2 pool subscription
// -----------------------------------------------------------------------------
async function subscribeV2Pool(rawAddress) {
const address = normalizeAddress(rawAddress);
if (
pools.has(address) ||
initializing.has(address) ||
unwatchPool.has(address)
) {
return;
}
initializing.add(address);
let queuedLogs = [];
let ready = false;
let unwatch;
try {
// Subscribe before reading the snapshot to reduce the initialization gap.
unwatch = client.watchContractEvent({
address,
abi: V2_PAIR_ABI,
eventName: 'Sync',
strict: true,
onLogs(logs) {
if (!ready) {
queuedLogs.push(...logs);
return;
}
processV2Logs(address, logs);
},
onError(error) {
console.error(`[V2 ${address}]`, error.message);
},
});
unwatchPool.set(address, unwatch);
const [token0, token1] = await Promise.all([
client.readContract({
address,
abi: V2_PAIR_ABI,
functionName: 'token0',
}),
client.readContract({
address,
abi: V2_PAIR_ABI,
functionName: 'token1',
}),
]);
const normalizedToken0 = normalizeAddress(token0);
const normalizedToken1 = normalizeAddress(token1);
const [metadata0, metadata1, reserves, blockNumber] =
await Promise.all([
readTokenMetadata(normalizedToken0),
readTokenMetadata(normalizedToken1),
client.readContract({
address,
abi: V2_PAIR_ABI,
functionName: 'getReserves',
}),
client.getBlockNumber(),
]);
const pool = {
address,
version: 2,
token0: normalizedToken0,
token1: normalizedToken1,
reserve0: reserves[0],
reserve1: reserves[1],
fee: V2_FEE_PPM,
lastBlock: blockNumber,
lastLogIndex: -1,
};
registerMetadata(pool, metadata0, metadata1);
registerPool(pool);
ready = true;
// Logs already reflected in the snapshot are discarded.
// Later logs are applied in block/log order.
queuedLogs.sort(compareLogs);
for (const log of queuedLogs) {
if (log.blockNumber > blockNumber) {
processV2Logs(address, [log]);
}
}
queuedLogs = [];
onPoolUpdated(pool);
console.log('[V2 subscribed]', address);
} catch (error) {
unwatch?.();
unwatchPool.delete(address);
console.error(`[V2 init ${address}]`, error.message);
throw error;
} finally {
initializing.delete(address);
}
}
function compareLogs(a, b) {
if (a.blockNumber < b.blockNumber) return -1;
if (a.blockNumber > b.blockNumber) return 1;
return (a.logIndex ?? 0) - (b.logIndex ?? 0);
}
function processV2Logs(address, logs) {
const pool = pools.get(address);
if (!pool) return;
for (const log of [...logs].sort(compareLogs)) {
if (log.removed) {
console.error('[V2 reorg detected]', address);
continue;
}
const block = log.blockNumber ?? 0n;
const index = log.logIndex ?? 0;
if (
block < pool.lastBlock ||
(block === pool.lastBlock && index <= pool.lastLogIndex)
) {
continue;
}
const { reserve0, reserve1 } = log.args;
if (reserve0 === undefined || reserve1 === undefined) {
continue;
}
pool.reserve0 = reserve0;
pool.reserve1 = reserve1;
pool.lastBlock = block;
pool.lastLogIndex = index;
onPoolUpdated(pool);
}
}
// -----------------------------------------------------------------------------
// V3 pool subscription
// -----------------------------------------------------------------------------
async function subscribeV3Pool(rawAddress, fee) {
const address = normalizeAddress(rawAddress);
if (
pools.has(address) ||
initializing.has(address) ||
unwatchPool.has(address)
) {
return;
}
initializing.add(address);
let queuedLogs = [];
let ready = false;
let unwatch;
try {
unwatch = client.watchContractEvent({
address,
abi: V3_POOL_ABI,
eventName: 'Swap',
strict: true,
onLogs(logs) {
if (!ready) {
queuedLogs.push(...logs);
return;
}
processV3Logs(address, logs);
},
onError(error) {
console.error(`[V3 ${address}]`, error.message);
},
});
unwatchPool.set(address, unwatch);
const [token0, token1] = await Promise.all([
client.readContract({
address,
abi: V3_POOL_ABI,
functionName: 'token0',
}),
client.readContract({
address,
abi: V3_POOL_ABI,
functionName: 'token1',
}),
]);
const normalizedToken0 = normalizeAddress(token0);
const normalizedToken1 = normalizeAddress(token1);
const [metadata0, metadata1, slot0, liquidity, blockNumber] =
await Promise.all([
readTokenMetadata(normalizedToken0),
readTokenMetadata(normalizedToken1),
client.readContract({
address,
abi: V3_POOL_ABI,
functionName: 'slot0',
}),
client.readContract({
address,
abi: V3_POOL_ABI,
functionName: 'liquidity',
}),
client.getBlockNumber(),
]);
const pool = {
address,
version: 3,
token0: normalizedToken0,
token1: normalizedToken1,
sqrtPriceX96: slot0[0],
tick: slot0[1],
liquidity,
fee,
lastBlock: blockNumber,
lastLogIndex: -1,
};
registerMetadata(pool, metadata0, metadata1);
registerPool(pool);
ready = true;
queuedLogs.sort(compareLogs);
for (const log of queuedLogs) {
if (log.blockNumber > blockNumber) {
processV3Logs(address, [log]);
}
}
queuedLogs = [];
onPoolUpdated(pool);
console.log('[V3 subscribed]', address, 'fee:', fee);
} catch (error) {
unwatch?.();
unwatchPool.delete(address);
console.error(`[V3 init ${address}]`, error.message);
throw error;
} finally {
initializing.delete(address);
}
}
function processV3Logs(address, logs) {
const pool = pools.get(address);
if (!pool) return;
for (const log of [...logs].sort(compareLogs)) {
if (log.removed) {
console.error('[V3 reorg detected]', address);
continue;
}
const block = log.blockNumber ?? 0n;
const index = log.logIndex ?? 0;
if (
block < pool.lastBlock ||
(block === pool.lastBlock && index <= pool.lastLogIndex)
) {
continue;
}
const { sqrtPriceX96, tick, liquidity } = log.args;
if (
sqrtPriceX96 === undefined ||
tick === undefined ||
liquidity === undefined
) {
continue;
}
pool.sqrtPriceX96 = sqrtPriceX96;
pool.tick = tick;
pool.liquidity = liquidity;
pool.lastBlock = block;
pool.lastLogIndex = index;
onPoolUpdated(pool);
}
}
// -----------------------------------------------------------------------------
// Factory subscriptions: discover newly created pools
// -----------------------------------------------------------------------------
const unwatchV2Factory = client.watchContractEvent({
address: V2_FACTORY,
abi: V2_FACTORY_ABI,
eventName: 'PairCreated',
strict: true,
onLogs(logs) {
for (const log of logs) {
const address = log.args.pair;
if (address && address !== ZERO_ADDRESS) {
subscribeV2Pool(address).catch(() => {});
}
}
},
onError(error) {
console.error('[V2 factory]', error.message);
},
});
const unwatchV3Factory = client.watchContractEvent({
address: V3_FACTORY,
abi: V3_FACTORY_ABI,
eventName: 'PoolCreated',
strict: true,
onLogs(logs) {
for (const log of logs) {
const { pool, fee } = log.args;
if (
pool &&
fee !== undefined &&
pool !== ZERO_ADDRESS
) {
subscribeV3Pool(pool, fee).catch(() => {});
}
}
},
onError(error) {
console.error('[V3 factory]', error.message);
},
});
// -----------------------------------------------------------------------------
// Startup
// -----------------------------------------------------------------------------
async function main() {
for (const entry of INITIAL_POOLS) {
if (!entry.address || ![2, 3].includes(entry.version)) {
throw new Error(
'Each initial pool needs an address and version 2 or 3',
);
}
if (entry.version === 2) {
await subscribeV2Pool(entry.address);
} else {
if (!Number.isInteger(entry.fee)) {
throw new Error(
`V3 pool ${entry.address} requires its fee tier`,
);
}
await subscribeV3Pool(entry.address, entry.fee);
}
}
console.log('[Monitor running] Ethereum mainnet');
}
function shutdown() {
console.log('[Monitor stopping]');
unwatchV2Factory();
unwatchV3Factory();
for (const unwatch of unwatchPool.values()) {
unwatch();
}
unwatchPool.clear();
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
main().catch((error) => {
console.error('[Startup failed]', error);
shutdown();
});
रन कैसे करें (Execution):
npm install viemexport ETH_WS_URL='wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'
export POOLS_JSON='[]'
node arbitrage-monitor.mjsPOOLS_JSON='[]' का मतलब है कि मॉनिटर सिर्फ उन पूल्स को सब्सक्राइब करेगा जो स्क्रिप्ट स्टार्ट होने के बाद बनेंगे। अगर आप पहले से मौजूद पूल्स को ट्रैक करना चाहते हैं, तो POOLS_JSON में उनके एड्रेस और वर्ज़न पास कर दें।
9. गैस ऑप्टिमाइजेशन चेकलिस्ट (Gas Optimization Checklist)
जब बात स्मार्ट कॉन्ट्रैक्ट के हैवी लॉजिक की आती है, तो बाज़ी वही मारता है जिसका ट्रांजैक्शन सबसे कम गैस यूनिट्स बर्न करता है। बचाई गई हर 10,000 गैस आपको मेमपूल (PGA auction) में बेहतर Priority Fee सेट करके कॉम्पिटिटर्स को पछाड़ने का मौका देती है।
- Require की जगह Custom Errors यूज़ करें: ट्रेडिशनल
require(condition, "error string")स्ट्रक्चर्स एरर स्ट्रिंग को स्टोर करने के चक्कर में फालतू की 100–200 गैस फूंक देते हैं। इसकी जगहif (!condition) revert InsufficientProfit()पर शिफ्ट होना काफी सस्ता पड़ता है। - Immutable और Constant वैरिएबल्स का यूज़: राउटर एड्रेस, Balancer Vault और बेस टोकन्स को
immutableफ्लैग के साथ डिक्लेयर करें। इससे वैल्यूज़ सीधे कॉन्ट्रैक्ट बाइटकोड में हार्डकोड हो जाती हैं, जिससे स्टोरेज री्ड्स (SLOAD— 2100 gas) का भारी-भरकम खर्चा बच जाता है। - transferFrom के बजाय डायरेक्ट Transfers: अगर आपका कॉन्ट्रैक्ट इंटरमीडिएट टोकन्स के साथ डील कर रहा है, तो उन्हें
transferके ज़रिए सीधे अगले पूल के एड्रेस पर भेजें। फालतू केapprove/transferFromकॉल्स करके गैस बर्न करने से बचें। Transient Storage (TSTORE / TLOAD EIP-1153):
EIP-1153 सपोर्ट करने वाले नेटवर्क पर री-एंट्रेंसी गार्ड्स (reentrancy guard) के लिए ट्रांजिएंट स्टोरेज का इस्तेमाल करें। इससे स्टेट राइट करने की कॉस्ट सीधे 20,000 गैस से घटकर महज़ 100 गैस रह जाती है।
10. डिप्लॉयमेंट और टेस्टिंग की स्टेप-बाय-स्टेप गाइड
स्टेप 1. मेननेट फ़ॉर्क बनाना (Foundry / Hardhat)
रियल नेटवर्क पर कॉन्ट्रैक्ट डिप्लॉय करने से पहले, लाइव पूल्स की स्टेट को बनाए रखते हुए लोकल Mainnet फ़ॉर्क पर पूरी सिमुलेशन रन करना बेहद ज़रूरी है।
# Anvil (Foundry) के ज़रिए लोकल फ़ॉर्क नोड स्टार्ट करें anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY --fork-block-number 19800000स्टेप 2. लोकल फ़ॉर्क पर कॉन्ट्रैक्ट डिप्लॉय करना
रियल इंफ्रास्ट्रक्चर एड्रेस पास करके
FlashArbEngineकॉन्ट्रैक्ट को डिप्लॉय करें:- Balancer Vault v2:
0xBA12222222228d8Ba445958a75a0704d566BF2C8 - Uniswap v3 SwapRouter02:
0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 - Sushiswap V2 Router:
0xd9e1cE17f2641f24aE83637ab66a2cca9C378804
- Balancer Vault v2:
स्टेप 3. स्प्रेड (Price Spread) सिमुलेट करना
सिमुलेशन स्क्रिप्ट के ज़रिए Sushiswap पूल में WETH की भारी सेलिंग करें, ताकि मैन्युअली प्राइस डिस्क्रिपेंसी और लिक्विडिटी इम्बैलेंस क्रिएट किया जा सके।
स्टेप 4. executeArbitrage कॉल करें
टारगेट लोन के सटीक पैरामीटर्स पास करके एग्जीक्यूटर कॉन्ट्रैक्ट के फ़ंक्शन को ट्रिगर करें। यह पक्का करें कि ट्रांजैक्शन बिना किसी एरर के निकल जाए, लोन का प्रिंसिपल अमाउंट Balancer को वापस मिल जाए, और आपका नेट प्रॉफ़िट
FlashArbEngineके बैलेंस में क्रेडिट हो जाए।
प्रैक्टिकल फ़्लैश लोन आर्बिट्राज (Flash Loan Arbitrage) के लिए Solidity में लो-लेवल कोडिंग, DeFi प्रोटोकॉल्स की गहरी समझ और MEV के लिए ऑफ-चेन इंफ्रास्ट्रक्चर सेटअप का कॉम्बिनेशन चाहिए होता है। Balancer v2 के ज़ीरो-फ़ीस लोन के साथ एक ऑप्टिमाइज्ड स्मार्ट कॉन्ट्रैक्ट और प्राइवेट ट्रांजैक्शन रिले (relays) आपको एक fully automated ट्रेडिंग बॉट बनाने के लिए तगड़ा बेस देते हैं।