TVL Anomaly Detection System
TVL (Total Value Locked) is key protocol health metric. Sharp TVL drops, anomalous deposits/withdrawals, unexplained fund movement patterns—all signal exploit, rug pull or coordinated attack. System works as early warning: protocol learns about problem from automated alert within minutes, not Twitter hours later.
What Counts as TVL Anomaly
Must distinguish: normal volatility (market moves, whale exit) vs anomaly (exploit, rug pull).
Anomaly characteristics:
- Speed: Normal changes take hours/days. Anomalies—seconds or one block
- Scale: 10–20% TVL change in blocks—extremely rare in normal operation
- Pattern: Exploit often accompanied by series of tx from one address
- Flash loan: Rapid deposit → immediate withdrawal same/next block
Monitoring Architecture
Blockchain Events (WebSocket)
↓
Event Processor (Node.js)
↓
TVL Calculator (per-block)
↓
Anomaly Detector (statistical + rule-based)
↓
Alert Engine (Telegram, PagerDuty, Slack)
↓
Dashboard (real-time visualization)
TVL Indexing Per Block
interface TVLSnapshot {
blockNumber: bigint;
timestamp: number;
totalTVL: bigint;
assetBreakdown: { asset: string; amount: bigint; usdValue: bigint }[];
deltaFromPrev: bigint;
deltaPct: number;
}
class TVLTracker {
async onNewBlock(blockNumber: bigint) {
const snapshot = await this.calculateTVL(blockNumber);
if (this.snapshots.length > 0) {
const prev = this.snapshots[this.snapshots.length - 1];
snapshot.deltaFromPrev = snapshot.totalTVL - prev.totalTVL;
snapshot.deltaPct = Number(snapshot.deltaFromPrev * 10000n / prev.totalTVL) / 100;
}
this.snapshots.push(snapshot);
await this.detectAnomalies(snapshot);
}
}
Statistical Detection
Z-Score Detector
Anomaly defined as deviation exceeding N standard deviations:
class ZScoreDetector {
detectAnomaly(snapshots: TVLSnapshot[]): AnomalyAlert | null {
const recent = snapshots.slice(-100); // rolling window
const deltas = recent.map(s => s.deltaPct);
const mean = deltas.reduce((a, b) => a + b, 0) / deltas.length;
const variance = deltas.reduce((sum, d) => sum + Math.pow(d - mean, 2), 0) / deltas.length;
const std = Math.sqrt(variance);
const latest = snapshots[snapshots.length - 1];
const zScore = std > 0 ? (latest.deltaPct - mean) / std : 0;
if (Math.abs(zScore) > 4) { // 4 sigma—extremely rare
return {
type: 'STATISTICAL_ANOMALY',
severity: Math.abs(zScore) > 6 ? 'critical' : 'high',
message: `TVL change ${latest.deltaPct.toFixed(2)}% is ${Math.abs(zScore).toFixed(1)}σ from mean`
};
}
return null;
}
}
Rule-Based Detector
const ANOMALY_RULES = [
{
name: 'RAPID_DRAIN',
check: (current, history) => {
if (history.length < 10) return null;
const tenBlocksAgo = history[history.length - 10];
const changePct = Number((current.totalTVL - tenBlocksAgo.totalTVL) * 10000n / tenBlocksAgo.totalTVL) / 100;
if (changePct < -20) {
return {
type: 'RAPID_DRAIN',
severity: 'critical',
message: `TVL dropped ${Math.abs(changePct).toFixed(1)}% in 10 blocks`
};
}
return null;
}
},
{
name: 'SINGLE_BLOCK_ANOMALY',
check: (current) => {
if (Math.abs(current.deltaPct) > 10) {
return {
type: 'SINGLE_BLOCK_ANOMALY',
severity: current.deltaPct < -10 ? 'critical' : 'high',
message: `Single block TVL change: ${current.deltaPct.toFixed(2)}%`
};
}
return null;
}
}
];
Metrics for Dashboard
| Metric | Description | Update |
|---|---|---|
| Current TVL | Current TVL in USD | Per block |
| 24h TVL Change | Change over 24 hours | Per block |
| TVL Velocity | Rate of change ($/block) | Per block |
| Largest withdrawal (24h) | Biggest single withdrawal | Per block |
| Anomaly score | Composite score (0–100) | Per block |
| Alert history | Last N alerts | Per alert |
Monitoring TVL anomalies—not replacement for audit but additional layer. Audit finds vulnerabilities before launch. Monitoring TVL gives chance to react in-process—stop contract, notify users, freeze exchange accounts.







