Skip to main content
Glama
lordbasilaiassistant-sudo

base-flash-arb-mcp

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
NameRequiredDescriptionDefault
token_addressYesToken contract address on Base
blocks_backNoNumber of blocks to look back (default 100)

Implementation Reference

  • 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
                ),
              },
            ],

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lordbasilaiassistant-sudo/base-flash-arb-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server