Back to articles
Advanced

Building a Forta Bot to Detect Flash Loan-Funded Governance Attacks in Real Time

This content is provided for **EDUCATIONAL** and **AUTHORIZED SECURITY TESTING** purposes only.

@0xrafasecFebruary 18, 2026detection_and_defense

Available in Português

Share:

Building a Forta Bot to Detect Flash Loan-Funded Governance Attacks in Real Time

Legal & Ethical Disclaimer

This content is provided for EDUCATIONAL and AUTHORIZED SECURITY TESTING purposes only.

DO
  • Use these techniques on systems you own or have explicit written permission to test
  • Practice in authorized lab environments (VulnHub, HackTheBox, DVWA, etc.)
  • Follow responsible disclosure practices when finding vulnerabilities
  • Use knowledge for defensive security and authorized penetration testing
DO NOT
  • Access systems without explicit authorization
  • Use these techniques for malicious purposes
  • Deploy exploits against production systems you don't own
  • Share working exploits for unpatched vulnerabilities

Legal warning

Unauthorized access to computer systems is illegal in most jurisdictions (e.g. CFAA in the US, Computer Misuse Act in the UK). Violators may face criminal prosecution and civil liability. The author and publisher assume no liability for misuse of this information. By continuing, you agree to use this knowledge ethically and legally.

Hook & Context

In April 2022, an attacker borrowed $1 billion in a flash loan, used the capital to acquire a controlling stake in Beanstalk's governance token, passed two malicious proposals in the same block, and drained $182 million from the protocol — all before a single human could react. The attack was not a bug in a Solidity function. It was a logical exploit of the governance system itself, and it executed entirely within the atomicity of a single Ethereum transaction bundle. No alarm fired. No circuit breaker tripped. The protocol was simply not listening for that pattern.

This is the class of attack that rule-based static auditing cannot prevent alone. You can't patch it with a require statement after the fact. The defense surface is temporal and behavioral: you need to detect the correlation between a massive token acquisition and a governance action and surface it fast enough to matter — ideally before the proposal executes, or at minimum before a second attack in the same campaign goes unnoticed. Real-time on-chain monitoring is the layer of the security stack that closes this gap.

Forta is purpose-built for this. It is a decentralized monitoring network where detection bots — Node.js/TypeScript programs that consume blockchain data block by block — run continuously and emit alerts when anomalous patterns are found. This piece walks through the design rationale, data correlation strategy, and threshold calibration for a Forta bot that specifically hunts the flash-loan-to-governance-action pattern. We use Compound and Beanstalk historical transaction data as calibration anchors.


🎯 TL;DR

DimensionSummary
What you're buildingA Forta detection bot that correlates flash loan token flows with governance votes/proposals within a configurable block window
Core techniqueEphemeral per-block state mapping addresses to token acquisition events, flushed and compared against governance event logs
Calibration anchorsBeanstalk block 14602790 (attack), Compound large-delegation history
Alert signalFlash loan receiver appears in governance action within N blocks of receiving tokens, where token delta exceeds quorum threshold
Key challengeDistinguishing legitimate large delegations from malicious flash loan voting — solved via loan source fingerprinting and block proximity
Tools usedForta SDK, ethers.js, The Graph (historical calibration), Tenderly (transaction simulation for testing)

Foundations & Theory

Why Governance Is the New Attack Surface

On-chain governance systems transfer protocol control to token holders. This was a democratizing design decision — but it introduced a latent assumption: that token holders are economically aligned with the protocol's long-term health. Flash loans shatter this assumption. A flash loan allows any address to temporarily hold an arbitrarily large token balance for the duration of a single transaction, with zero economic commitment beyond gas and fees. If a governance system accepts a snapshot of token balance at the moment of vote casting rather than at a prior block, that flash loan balance becomes a valid vote weight.

The attack pattern is deterministic:

[Block N]
  1. Flash loan: borrow X governance tokens
  2. Cast vote / create proposal using borrowed weight
  3. Repay flash loan
  ↓
[Block N or N+k]
  4. Proposal executes (if timelock is zero or bypassed)

The critical insight for detection is that steps 1–3 are traceable via event logs. Every ERC-20 transfer emits a Transfer(address indexed from, address indexed to, uint256 value) event. Every governance contract emits VoteCast or ProposalCreated. The correlation between these two streams, anchored in block time proximity, is the detection signal.

Why Real-Time Detection Matters Even Post-Attack

You might ask: if the attack is atomic, what does real-time detection even buy you? Three things. First, multi-step attacks — like Beanstalk, which required two proposals — have inter-proposal windows where alerting can trigger emergency pauses. Second, campaign detection: an attacker probing thresholds across multiple protocols will leave a cross-chain trail that a running bot accumulates. Third, forensic speed: faster alert generation compresses the time-to-post-mortem, which matters for insurance claims, fork decisions, and community response.


Where It Fits in the Workflow

In a DeFi security stack, detection bots occupy the continuous monitoring layer — distinct from pre-deployment audit (static analysis, fuzzing) and incident response (treasury multisig, protocol pause). Think of the overall stack as a defense-in-depth model:

Loading diagram…

The Forta bot sits in that third layer. Its job is not to prevent — it is to detect and escalate fast enough that the incident response layer has time to act. For protocols with timelocked governance (e.g., 48-hour delay on proposal execution), a bot alerting within seconds of a suspicious vote cast gives the multisig owners a meaningful intervention window.


Key Concepts in Depth

1. Dual-Stream Event Subscription and the Per-Block State Model

The foundation of this bot is simultaneous subscription to two event streams and stateful correlation across a sliding block window. In Forta, your bot's handleTransaction function is called for every transaction in every block. You also have access to handleBlock for per-block cleanup.

The state architecture looks like this:

typescript
// Ephemeral state: maps recipient address → {amount, blockNumber, txHash}
const flashLoanRecipients: Map<string, FlashLoanEvent[]> = new Map();

// Governance actions observed: maps address → {action, blockNumber, txHash}  
const governanceActions: Map<string, GovernanceEvent[]> = new Map();

const BLOCK_WINDOW = 3; // configurable — discussed in calibration section
const QUORUM_THRESHOLD = ethers.utils.parseUnits("100000", 18); // protocol-specific

On each transaction, you decode logs against two ABIs: the governance token (ERC-20 Transfer) and the governance contract (VoteCast, ProposalCreated). When a large Transfer event is detected from a known lending pool address (Aave, dYdX, Uniswap V2/V3 pool addresses), you record the recipient. When a governance event is detected, you check whether the sender appears in flashLoanRecipients within BLOCK_WINDOW blocks.

The key design decision is where you source the "flash loan fingerprint." You have two strategies:

  • Source address fingerprinting: maintain a list of known lending pool and flash loan provider addresses. A Transfer from one of these to a new address is likely a flash loan disbursement.
  • Return transfer pattern: flash loans require repayment, so you'll see a symmetric outbound Transfer from the recipient back to the pool within the same block. This is the stronger signal, but it requires intra-block ordering awareness.

For a production bot, use both. The source fingerprint fires early (on receipt); the return pattern fires as confirmation.

2. ABI Decoding and Log Filtering in the Forta SDK

Forta bots receive a TransactionEvent object that exposes filterLog(abi, address) — a convenience method wrapping ethers.js interface decoding. The elegant aspect of this design is that you can filter for multiple event signatures in a single pass:

typescript
import { Finding, FindingSeverity, FindingType, HandleTransaction, TransactionEvent } from "forta-agent";
import { GOVERNANCE_ABI, TOKEN_ABI, KNOWN_LENDING_POOLS } from "./constants";

const handleTransaction: HandleTransaction = async (txEvent: TransactionEvent) => {
  const findings: Finding[] = [];

  // Decode large token transfers FROM known flash loan sources
  const transferLogs = txEvent.filterLog(TOKEN_ABI, GOVERNANCE_TOKEN_ADDRESS);
  
  for (const log of transferLogs) {
    const { from, to, value } = log.args;
    if (KNOWN_LENDING_POOLS.has(from.toLowerCase()) && value.gte(QUORUM_THRESHOLD)) {
      recordFlashLoanRecipient(to, value, txEvent.blockNumber, txEvent.hash);
    }
  }

  // Decode governance actions
  const voteLogs = txEvent.filterLog(GOVERNANCE_ABI, GOVERNOR_ADDRESS);
  
  for (const log of voteLogs) {
    const voter = log.args.voter ?? log.args.proposer;
    const finding = checkCorrelation(voter, txEvent.blockNumber, txEvent.hash);
    if (finding) findings.push(finding);
  }

  return findings;
};

A critical implementation note: filterLog returns decoded events for the entire transaction, not just logs from the primary contract call. This matters because flash loan orchestration typically happens through a custom attacker contract that calls governance in the same transaction — the events appear in the same transaction log array regardless of call depth.

3. Sliding Window State Management and Block Cleanup

Ephemeral state that never expires creates memory leaks and false positives from stale data. The handleBlock hook is your cleanup mechanism:

typescript
const handleBlock = async (blockEvent: BlockEvent) => {
  const currentBlock = blockEvent.blockNumber;
  
  // Evict entries older than BLOCK_WINDOW
  for (const [address, events] of flashLoanRecipients.entries()) {
    const filtered = events.filter(e => currentBlock - e.blockNumber <= BLOCK_WINDOW);
    if (filtered.length === 0) {
      flashLoanRecipients.delete(address);
    } else {
      flashLoanRecipients.set(address, filtered);
    }
  }
  
  return [];
};

Why BLOCK_WINDOW matters architecturally: same-block attacks (like Beanstalk) require BLOCK_WINDOW = 0 to detect with certainty, but setting the window to 1–3 blocks also catches multi-transaction attacks where the attacker borrows in block N and votes in block N+1 before repaying in block N+2 (a pattern that can evade same-block detection if the governance contract only checks the current balance).

4. Threshold Calibration Using Historical Data

This is where the bot moves from toy to production-grade. Miscalibrated thresholds are the primary reason monitoring bots fail in practice — either drowning operators in noise or missing real attacks.

Calibrating against Beanstalk (block 14602790): The attacker acquired approximately 79 million Stalk (governance weight). At the time of the attack, the total Stalk supply was roughly 170 million, making the attack weight ~46% of total supply. Your QUORUM_THRESHOLD for Beanstalk should have been set well below 46% of supply — something like 10–15% of circulating supply as a trigger, since no legitimate single voter would hold that proportion via a flash loan.

Calibrating against Compound legitimate large delegations: Compound's governance history includes addresses like 0xa2869... (a16z delegation) holding 8–10% of COMP via legitimate means. The distinguishing features are:

  • The delegation was not acquired in the same or adjacent block as a vote
  • The token source was a multisig or known VC wallet, not a lending pool
  • The holding persisted across thousands of blocks

From this, derive two calibration axes:

AxisMalicious SignalLegitimate Signal
Token sourceKnown lending poolMultisig / exchange / long-held wallet
Block proximitySame or ≤3 blocks between acquisition and voteWeeks/months of prior holding
Token delta>10% of circulating supply<5% in a single acquisition event
Address historyFirst-time voter, no prior governance participationAddress with governance history

Using The Graph, query historical VoteCast events and cross-reference voter address ages against token acquisition timestamps. Any address that first received governance tokens in the same block it voted is a perfect calibration point for BLOCK_WINDOW = 0.

5. Alert Structure and Escalation Design

A Finding in Forta is not just a log entry — it is a structured alert with severity, type, metadata, and protocol labels that downstream consumers (Tenderly webhooks, PagerDuty, Discord bots) can act on. Design your alert to carry maximum forensic context:

typescript
Finding.fromObject({
  name: "Flash Loan Governance Attack Detected",
  description: `Address ${voter} cast governance vote within ${blockDelta} blocks of receiving ${formattedAmount} tokens from flash loan source ${loanSource}`,
  alertId: "FLASH-LOAN-GOV-1",
  severity: blockDelta === 0 ? FindingSeverity.Critical : FindingSeverity.High,
  type: FindingType.Exploit,
  metadata: {
    voter,
    loanSource,
    tokenAmount: amount.toString(),
    acquisitionBlock: acquisitionBlock.toString(),
    voteBlock: voteBlock.toString(),
    proposalId: proposalId ?? "unknown",
    blockDelta: blockDelta.toString(),
  },
  protocol: "compound-v2", // or dynamic
  addresses: [voter, loanSource],
});

Severity gradation matters for operator response. A Critical alert (same-block) should trigger an automated emergency action if your protocol has one. A High alert (1–3 block window) should page an on-call responder. Medium (>3 blocks, large acquisition, first-time voter) should log to a dashboard for human review.


Alternatives & Comparison

ApproachStrengthsWeaknesses
Forta Bot (this approach)Real-time, decentralized, composable, structured alertsRequires maintenance, bots can miss events if poorly subscribed
Tenderly WebhooksEasy setup, excellent UICentralized, limited correlation logic, alert fatigue
The Graph + cron pollingRich historical queries, great for multi-block windowsLatency (minutes, not seconds), not truly real-time
Custom RPC mempool watcherLowest latency (pre-confirmation)Infrastructure-heavy, requires archive node, no decentralization
Chainalysis / TRM LabsCompliance-grade, cross-chainExpensive, not open, not customizable for protocol-specific logic

For DeFi protocols with live governance, Forta + Tenderly alert routing is the practical stack: Forta handles the detection logic; Tenderly handles the delivery and human-readable simulation of the detected transaction so responders can verify in under 60 seconds.


Takeaways & Further Reading

The Beanstalk attack is not a historical anomaly — it is a blueprint that other attackers will follow as governance power continues to concentrate in liquid, borrowable tokens. The detection bot described here is not a silver bullet: it cannot prevent an atomic attack from completing, but it can detect the pattern, alert in time for multi-step campaigns, and build a forensic record that collapses post-incident investigation from weeks to hours.

The three design principles that make this bot production-grade rather than a proof-of-concept:

  1. Dual-stream correlation over single-event alerting — one event in isolation is never enough signal
  2. Historically calibrated thresholds — build your tuning from real on-chain data, not intuition
  3. Structured, actionable alerts — an alert that doesn't tell the responder exactly what to look at is noise

📚 Further Reading:

The governance attack surface will only grow as DeFi protocols mature and governance power accretes real economic value. Build the monitoring layer now, calibrate it against history, and treat it as a first-class engineering artifact — not an afterthought bolted on after the exploit has already run.

Found this article interesting? Follow me on X and LinkedIn.