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.
Available in Português
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.
- •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
- •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
| Dimension | Summary |
|---|---|
| What you're building | A Forta detection bot that correlates flash loan token flows with governance votes/proposals within a configurable block window |
| Core technique | Ephemeral per-block state mapping addresses to token acquisition events, flushed and compared against governance event logs |
| Calibration anchors | Beanstalk block 14602790 (attack), Compound large-delegation history |
| Alert signal | Flash loan receiver appears in governance action within N blocks of receiving tokens, where token delta exceeds quorum threshold |
| Key challenge | Distinguishing legitimate large delegations from malicious flash loan voting — solved via loan source fingerprinting and block proximity |
| Tools used | Forta 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:
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:
// 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
Transferfrom 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
Transferfrom 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:
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:
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:
| Axis | Malicious Signal | Legitimate Signal |
|---|---|---|
| Token source | Known lending pool | Multisig / exchange / long-held wallet |
| Block proximity | Same or ≤3 blocks between acquisition and vote | Weeks/months of prior holding |
| Token delta | >10% of circulating supply | <5% in a single acquisition event |
| Address history | First-time voter, no prior governance participation | Address 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:
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
| Approach | Strengths | Weaknesses |
|---|---|---|
| Forta Bot (this approach) | Real-time, decentralized, composable, structured alerts | Requires maintenance, bots can miss events if poorly subscribed |
| Tenderly Webhooks | Easy setup, excellent UI | Centralized, limited correlation logic, alert fatigue |
| The Graph + cron polling | Rich historical queries, great for multi-block windows | Latency (minutes, not seconds), not truly real-time |
| Custom RPC mempool watcher | Lowest latency (pre-confirmation) | Infrastructure-heavy, requires archive node, no decentralization |
| Chainalysis / TRM Labs | Compliance-grade, cross-chain | Expensive, 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:
- Dual-stream correlation over single-event alerting — one event in isolation is never enough signal
- Historically calibrated thresholds — build your tuning from real on-chain data, not intuition
- Structured, actionable alerts — an alert that doesn't tell the responder exactly what to look at is noise
📚 Further Reading:
- Forta SDK Documentation — start with the
forta-agentTypeScript SDK and thehandleTransactionlifecycle - Beanstalk Post-Mortem — primary source for attack mechanics and block-level forensics
- Compound Governance Analytics via The Graph — use this subgraph for calibration dataset queries
- Euler Finance Flash Loan Monitoring — a reference implementation of production DeFi monitoring
- "Flash Boys 2.0" (Daian et al., 2020) — the academic foundation for understanding MEV and atomic transaction composability, essential context for why flash loans are powerful
- OpenZeppelin Defender Sentinels — a complementary centralized alternative worth understanding before deciding on Forta
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.