check_sandwich_risk
Analyzes token trades on Base to detect sandwich attack patterns from bot activity, helping identify potential manipulation risks.
Instructions
Analyze a token's recent trades for sandwich attack patterns (bot activity).
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| token_address | Yes | Token contract address on Base | |
| blocks_back | No | Number of blocks to look back (default 100) |
Implementation Reference
- src/index.ts:1120-1310 (handler)The handler function for the 'check_sandwich_risk' tool, which scans for sandwich attack patterns in Uniswap V2/V3 liquidity pools.
server.tool( "check_sandwich_risk", "Analyze a token's recent trades for sandwich attack patterns (bot activity).", { token_address: z.string().describe("Token contract address on Base"), blocks_back: z .number() .default(100) .describe("Number of blocks to look back (default 100)"), }, async ({ token_address, blocks_back }) => { try { const symbol = await getSymbol(token_address); const currentBlock = await provider.getBlockNumber(); const fromBlock = currentBlock - blocks_back; // Find all V3 pools for this token and scan Swap events const v3Factory = new ethers.Contract( UNIV3_FACTORY, UNIV3_FACTORY_ABI, provider ); const swapEvents: Array<{ block: number; txHash: string; sender: string; pool: string; dex: string; amount0: string; amount1: string; }> = []; // Check V3 pools for (const fee of [500, 3000, 10000]) { try { const poolAddr = await v3Factory.getPool( WETH, token_address, fee ); if (poolAddr === ethers.ZeroAddress) continue; const pool = new ethers.Contract( poolAddr, UNIV3_POOL_ABI, provider ); const filter = pool.filters.Swap(); const logs = await withTimeout( pool.queryFilter(filter, fromBlock, currentBlock), 15000 ); for (const log of logs) { swapEvents.push({ block: log.blockNumber, txHash: log.transactionHash, sender: (log as ethers.EventLog).args?.[0] ?? "unknown", pool: poolAddr, dex: `V3-${fee}`, amount0: ((log as ethers.EventLog).args?.[2] ?? 0n).toString(), amount1: ((log as ethers.EventLog).args?.[3] ?? 0n).toString(), }); } } catch { // Pool doesn't exist or query failed } } // Check V2 pool Transfer events (simpler) try { const v2Factory = new ethers.Contract( UNIV2_FACTORY, UNIV2_FACTORY_ABI, provider ); const v2Pair = await v2Factory.getPair(WETH, token_address); if (v2Pair !== ethers.ZeroAddress) { const token = new ethers.Contract(token_address, ERC20_ABI, provider); const transferFilter = token.filters.Transfer(); const logs = await withTimeout( token.queryFilter(transferFilter, fromBlock, currentBlock), 15000 ); for (const log of logs) { const args = (log as ethers.EventLog).args; if ( args && (args[0]?.toLowerCase() === v2Pair.toLowerCase() || args[1]?.toLowerCase() === v2Pair.toLowerCase()) ) { swapEvents.push({ block: log.blockNumber, txHash: log.transactionHash, sender: args[0], pool: v2Pair, dex: "V2", amount0: (args[2] ?? 0n).toString(), amount1: "0", }); } } } } catch { // V2 query failed } // Analyze for sandwich patterns: same sender in same block with 2+ txs const blockGroups = new Map< number, Map<string, typeof swapEvents> >(); for (const evt of swapEvents) { if (!blockGroups.has(evt.block)) blockGroups.set(evt.block, new Map()); const senderMap = blockGroups.get(evt.block)!; const key = evt.sender.toLowerCase(); if (!senderMap.has(key)) senderMap.set(key, []); senderMap.get(key)!.push(evt); } const sandwichPatterns: Array<{ block: number; sender: string; txCount: number; txHashes: string[]; }> = []; for (const [block, senderMap] of blockGroups) { for (const [sender, events] of senderMap) { if (events.length >= 2) { const uniqueTxs = [...new Set(events.map((e) => e.txHash))]; if (uniqueTxs.length >= 2) { sandwichPatterns.push({ block, sender, txCount: uniqueTxs.length, txHashes: uniqueTxs, }); } } } } // Count unique senders const uniqueSenders = new Set(swapEvents.map((e) => e.sender.toLowerCase())); // Identify likely bots (addresses that appear in many blocks) const senderFrequency = new Map<string, number>(); for (const evt of swapEvents) { const key = evt.sender.toLowerCase(); senderFrequency.set(key, (senderFrequency.get(key) ?? 0) + 1); } const likelyBots = [...senderFrequency.entries()] .filter(([, count]) => count >= 5) .map(([addr, count]) => ({ address: addr, swapCount: count })) .sort((a, b) => b.swapCount - a.swapCount); const riskLevel = sandwichPatterns.length > 5 ? "HIGH" : sandwichPatterns.length > 0 ? "MEDIUM" : "LOW"; return { content: [ { type: "text" as const, text: JSON.stringify( { token: token_address, symbol, blocksScanned: blocks_back, fromBlock, toBlock: currentBlock, totalSwapEvents: swapEvents.length, uniqueTraders: uniqueSenders.size, sandwichRisk: riskLevel, sandwichPatternsFound: sandwichPatterns.length, sandwichDetails: sandwichPatterns.slice(0, 10), likelyBots: likelyBots.slice(0, 10), warning: "Sandwich detection is heuristic. Same-block multi-swap by one sender suggests but does not confirm sandwiching.", }, null, 2 ), }, ],