Automated DeFi Yield Farm Risk Scanner: Rug Pull and Smart Contract Vulnerability Detection

How to build an AI agent that scans new yield farming protocols for rug pull indicators, contract vulnerabilities, and liquidity manipulation — before you deposit.

$2.8B was lost to DeFi rug pulls in 2025. Most showed at least 3 detectable on-chain signals 48 hours before the exit. Here's how to scan for them automatically.

Your yield farming strategy is a masterpiece of leverage, curve optimization, and ve-tokenomics. You’re chasing that Top Curve pool APY of 4–12% from fees plus CRV emissions, or maybe you’ve parked funds in a Yearn vault averaging 5–18% net APY. The math is perfect. The only thing standing between you and that sweet, sweet yield is a developer with admin keys and a sudden, profound lack of morals. Manual due diligence is a losing game; you need a machine to do it. This is how you build an automated risk scanner that sniffs out rug pulls and contract vulnerabilities before your liquidity becomes exit liquidity.

The 7 On-Chain Signals That Precede Most Rug Pulls

Forget the vague "team is anonymous" FUD. Real rug pulls leave forensic evidence on-chain, often days in advance. Your scanner needs to look for these seven concrete signals.

  1. Owner Balance Drain: The contract owner's wallet, or a multi-sig signer, suddenly swaps large amounts of the project's native token for a stablecoin or ETH. This often happens in chunks just below obvious thresholds.
  2. Liquidity Unlock and Removal: The LP tokens for the main trading pair (e.g., in a Uniswap v3 pool) are unlocked from a timelock or vesting contract and withdrawn. This is the kill shot, but it's often preceded by...
  3. Fee Manipulation: A sudden, unauthorized change to project fee parameters—setting a protocolFee to 99% or disabling a withdrawalFee right before the exit.
  4. Minting Authority Abuse: For tokens with a mint function, a massive, out-of-schedule mint directly into the owner's wallet, immediately diluting all holders.
  5. Proxy Admin Upgrade: For upgradeable contracts (like many using Transparent or UUPS proxies), an upgrade is submitted that changes the logic to a malicious contract. This is a silent killer.
  6. Critical Function Pausing: The pause() function is called on deposit/withdraw functions, trapping user funds while the attackers drain other parts of the system.
  7. Anomalous Large Transfers: Large, atypical transfers of the project token to a fresh, unlabeled wallet (often a pre-staged CEX deposit address).

The goal isn't to catch the rug as it happens—by then, it's too late. The goal is to detect the preparatory steps (Signals 1, 3, 5, 6) that create a 24-48 hour window to get out.

Setting Up the AI Risk Scanner Architecture

This isn't a monolithic script. It's a pipeline: fetch data, analyze with rules and heuristics, score, and alert. We'll use a combination of The Graph for efficient historical queries and direct RPC calls for real-time state.

Your core stack: Node.js/Python for orchestration, The Graph for event history, an Ethereum RPC provider (like Alchemy or Infura), and GitHub Copilot or Continue.dev in VS Code to help you navigate the sprawling ABIs. Use F12 (Go to Definition) relentlessly on contract addresses to jump to their source on Etherscan.

First, structure your project. Open your terminal in VS Code (`Ctrl+``) and run:

mkdir defi-risk-scanner && cd defi-risk-scanner
npm init -y
npm install ethers graphql-request node-cron dotenv

Create a .env file for your secrets:

RPC_URL=your_mainnet_rpc_url
GRAPH_API_KEY=your_the_graph_api_key
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id

Now, let's build the core query engine. This first code block uses The Graph to efficiently fetch the owner's historical token transfers, which is far cheaper and faster than filtering logs via RPC.

// scripts/fetchOwnerTransfers.js
const { request } = require('graphql-request');
const { ethers } = require('ethers');

const THE_GRAPH_URL = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3';

// Example: Track owner wallet for a suspicious token (use a real token address)
const OWNER_WALLET = '0x...';
const SUSPECT_TOKEN = '0x...';

const query = `
  query ($owner: String!, $token: String!) {
    transfers(
      where: {
        from: $owner,
        token: $token
      },
      orderBy: timestamp,
      orderDirection: desc,
      first: 100
    ) {
      id
      from
      to
      value
      timestamp
    }
  }
`;

async function fetchOwnerTransfers() {
  try {
    const variables = { owner: OWNER_WALLET.toLowerCase(), token: SUSPECT_TOKEN.toLowerCase() };
    const data = await request(THE_GRAPH_URL, query, variables);

    console.log(`Last 100 transfers OUT of owner wallet for token:`);
    data.transfers.forEach(tx => {
      const value = ethers.formatUnits(tx.value, 18); // Adjust decimals as needed
      console.log(`- ${new Date(tx.timestamp * 1000).toISOString()}: ${value} tokens to ${tx.to}`);
    });

    // Heuristic: Flag if >5% of circulating supply moved in last 24h
    // You would integrate circulating supply logic here.
    return data.transfers;
  } catch (error) {
    console.error('GraphQL query failed:', error);
  }
}

fetchOwnerTransfers();

This gives you a cheap, fast view of the owner's behavior. For real-time checks and contract state, you'll need the RPC.

Querying Contract Metadata and Ownership Patterns

Ownership is everything. Is the contract owned by a 4/7 Gnosis Safe, a 1/1 EOA (red flag), or is it immutable? Use ethers.js to interrogate.

// scripts/checkOwnership.js
require('dotenv').config();
const { ethers } = require('ethers');

const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);

// Common contract ABIs (simplified)
const OWNABLE_ABI = ['function owner() view returns (address)'];
const PROXY_ADMIN_ABI = ['function getProxyAdmin(address proxy) view returns (address)'];
const TIMELOCK_ABI = ['function getMinDelay() view returns (uint256)'];

async function auditOwnership(contractAddress) {
  console.log(`\n=== Auditing Ownership for ${contractAddress} ===`);

  // 1. Check direct owner
  const ownableContract = new ethers.Contract(contractAddress, OWNABLE_ABI, provider);
  try {
    const owner = await ownableContract.owner();
    console.log(`Direct Owner: ${owner}`);
    await analyzeWallet(owner);
  } catch (e) {
    console.log('No direct `owner()` function or call failed.');
  }

  // 2. Check if it's a proxy and who the admin is
  // This is a critical check. Many hacks happen via proxy upgrade.
  const proxyAdminContract = new ethers.Contract('0x...ProxyAdminAddress...', PROEL_ADMIN_ABI, provider);
  try {
    const admin = await proxyAdminContract.getProxyAdmin(contractAddress);
    console.log(`Proxy Admin: ${admin}`);
    await analyzeWallet(admin);
  } catch (e) {
    console.log('Not a detectable proxy or admin check failed.');
  }

  // 3. Check if owner is a timelock
  async function analyzeWallet(address) {
    const code = await provider.getCode(address);
    if (code === '0x') {
      console.log(`  ⚠️  Owner is an Externally Owned Account (EOA). Higher risk.`);
    } else if (code.length > 100) {
      console.log(`  ✅ Owner is a contract.`);
      // Optional: Try to see if it's a timelock
      const timelockContract = new ethers.Contract(address, TIMELOCK_ABI, provider);
      try {
        const delay = await timelockContract.getMinDelay();
        console.log(`  ✅ Contract is a Timelock with ${delay} second delay.`);
      } catch (e) {
        console.log(`  ⚠️  Owner is a contract, but not a standard timelock.`);
      }
    }
  }
}

// Example: Check a Convex-style booster contract
auditOwnership('0xF403C135812408BFbE8713b5A23a04b3D48AAE31').catch(console.error);

This script reveals the chain of control. A finding like "Owner is an EOA" immediately adds risk points.

Liquidity Lock Verification with AI Assistance

"Liquidity is locked" is the most common lie in DeFi. Verification means checking the actual LP token holder and its vesting schedule. For a Uniswap v3 position, the LP token is an NFT. You need to check:

  1. Who holds the NFT for the main pool?
  2. Is that holder a vesting or timelock contract?
  3. What are the withdrawal conditions?

This is where an AI coding assistant like Continue.dev in VS Code becomes invaluable. The logic is protocol-specific. Prompt it: "How do I fetch the holder of a Uniswap v3 NFT position and check if the holder contract has a vesting schedule?" It will guide you through the Uniswap V3 NonfungiblePositionManager ABI.

Real Error & Fix:

Error: Impermanent loss exceeds fee income when price moves >20% Fix: This is a core risk, not a bug. Your scanner should flag pools where this is likely. For stable pairs (e.g., USDC/USDT), using narrow tick ranges on Uniswap v3 is efficient. For volatile pairs, narrow ranges are an impermanent loss trap. The scanner should warn if a high-volatility pair farm recommends a sub-5% price range.

Building the Scoring Model: Risk 0–100

Raw signals are useless without a score. Assign weighted points to create a 0-100 risk scale.

Risk FactorWeightExample TriggerPoints
Owner TypeHighOwner is an EOA (vs. 7-day Timelock)+25
Liquidity LockCriticalLP tokens held by EOA (vs. vesting contract)+30
Proxy Upgrade RiskHighUpgradeable proxy with admin not in timelock+20
Fee Change AbilityMediumsetFeePercentage function exists, no timelock+15
Minting FunctionHighmint() function with no daily limit+20
ConcentrationMediumTop 10 holders own >60% of supply (pre-liquidity)+15
Anomalous ActivityDynamicOwner sells >5% of supply in 24h+15 to +30

Scoring Logic: 0-30: Green, 31-60: Yellow (Monitor), 61-100: Red (Exit).

A new farm with an EOA owner, unlocked liquidity, and a mint function hits 25+30+20 = 75 immediately—a hard avoid.

Real Case Study: Scanning a Live Protocol (Anonymized)

Let's call it "FroggyFinance." It's a new yield aggregator on Arbitrum promising 40% APY on ETH-stables LP. Our scanner runs its checks.

  1. Ownership Query: owner() returns an EOA. The wallet has no prior history as a deployer. (+25)
  2. Liquidity Check: The "locked" LP NFT is held by the same EOA. A graphql query shows it was transferred from the deployer wallet 2 hours after pool creation. (+30)
  3. Contract Analysis: The vault contract is upgradeable via a UUPS proxy. The upgradeTo function is callable by... the same EOA. (+20)
  4. Fee Check: The contract has a setPerformanceFee(10000) function (100% fee), protected only by the onlyOwner modifier. (+15)
  5. Activity Monitor: Our hourly transfer scan catches a series of large minting events to the owner, increasing their share of the governance token by 15% in one block.

Total Score: 90/100. Critical Risk. The scanner auto-sends a Telegram alert. Two days later, setPerformanceFee(10000) is called, and the owner drains the vault. The exit was flagged 48 hours prior.

Another Real Error & Fix:

Error: Out-of-range Uniswap v3 position earns 0 fees Fix: This is a strategy risk, not a scam, but it kills returns. Your scanner, when evaluating a farm based on Uniswap v3, should query the pool's current price and check if it's within the position's tick range. If it's out of range, flag it. The mitigation is to monitor and have a rebalancing strategy when the price exits the range.

Integrating Alerts into Telegram or Discord

A risk score is useless in a log file. It needs to scream at you. A Telegram bot is perfect.

// scripts/alertBot.js
const TelegramBot = require('node-telegram-bot-api');
const cron = require('node-cron');
require('dotenv').config();

const bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false }); // Use for sending only

async function sendRiskAlert(protocolName, score, findings, contractAddress) {
    let emoji = '🟢';
    if (score > 60) emoji = '🔴';
    else if (score > 30) emoji = '🟡';

    const message = `
${emoji} *DeFi Risk Alert* ${emoji}
*Protocol:* ${protocolName}
*Contract:* \`${contractAddress}\`
*Risk Score:* ${score}/100

*Critical Findings:*
${findings.map(f => `• ${f}`).join('\n')}

[View on Etherscan](https://etherscan.io/address/${contractAddress})
    `;

    await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, { parse_mode: 'Markdown', disable_web_page_preview: true });
    console.log('Alert sent.');
}

// Simulate a cron job that runs every hour
cron.schedule('0 * * * *', async () => {
    console.log('Running hourly scan...');
    // ... your scanning logic here ...
    const testFindings = [
        'Owner is an EOA (0xabc...123)',
        'Liquidity LP NFT not in timelock',
        'Proxy admin can upgrade without delay'
    ];
    await sendRiskAlert('SuspiciousFarm', 85, testFindings, '0x123...abc');
});

Run this, and your phone will buzz with a formatted alert the moment a protocol's risk spikes from yellow to red.

Next Steps: From Scanner to Sentinel

You now have a scanner that checks static properties and historical signals. This is week one. To make it a true sentinel, you need to move from polling to listening.

  1. Real-Time Event Streaming: Use your RPC provider's WebSocket endpoint (wss://) to subscribe to pending transactions and logs for your watched addresses. Listen for the Upgraded(address) event on proxies, or the RoleGranted() event on AccessControl contracts.
  2. Simulate Transactions: Before an owner's transaction is mined, you can use eth_call to simulate its effect. Simulate a setFee(10000) call from the owner's address. Does it drain the contract? This is advanced but possible with Foundry's cast or Tenderly simulations.
  3. Cross-Reference with DeFiLlama: Use the DeFiLlama API to get a protocol's TVL. A TVL spike in a high-risk protocol is a major red flag—it means the trap is being baited.
  4. Monitor for Code Similarities: Many rug pulls are forks of earlier, legitimate projects (like PancakeSwap forks). Use bytecode similarity checks or source code comparison to see if a new farm is a 99% copy of a known scam.

The goal is not to live in fear, but to farm with confidence. Let the machine handle the paranoia. You've optimized every other part of your yield strategy—now optimize your survival. Point your scanner at that next "too good to be true" farm, and let the score tell you if it's alpha or ashes.