Разработка системы детекции аномалий TVL
TVL (Total Value Locked) — ключевая метрика здоровья DeFi протокола. Резкое падение TVL, аномальные депозиты/выводы, необъяснимые паттерны в движении средств — всё это может сигнализировать об эксплойте, rug pull или координированной атаке. Система детекции аномалий TVL работает как ранняя система предупреждения: протокол узнаёт о проблеме не из Twitter, а из автоматического алерта за несколько минут до того, как об этом напишут в новостях.
Что считается аномалией TVL
Нужно разделить: нормальная волатильность TVL (рыночные движения, крупный whale выход) и аномалия (эксплойт, flash loan атака, rug pull). Характеристики аномалий:
- Скорость: нормальные изменения занимают часы/дни. Аномалии — секунды или один блок
- Масштаб: 10–20% TVL изменение за несколько блоков — экстремально редко при нормальной работе
- Паттерн: эксплойт часто сопровождается серией транзакций по одному адресу
- Flash loan: резкий deposit → immediate withdrawal в том же или следующем блоке
Архитектура системы мониторинга
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 per block
import { createPublicClient, webSocket, parseAbiItem } from 'viem';
interface TVLSnapshot {
blockNumber: bigint;
timestamp: number;
totalTVL: bigint; // в USD, scaled 1e6
assetBreakdown: {
asset: string;
amount: bigint;
usdValue: bigint;
}[];
deltaFromPrev: bigint; // изменение от предыдущего блока
deltaPct: number; // процентное изменение
}
class TVLTracker {
private client: ReturnType<typeof createPublicClient>;
private snapshots: TVLSnapshot[] = [];
private poolAddresses: string[];
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);
// Храним только последние 1000 блоков в памяти
if (this.snapshots.length > 1000) this.snapshots.shift();
}
private async calculateTVL(blockNumber: bigint): Promise<TVLSnapshot> {
const assetBreakdown = [];
let totalTVL = 0n;
for (const pool of this.poolAddresses) {
const balance = await this.getPoolBalance(pool, blockNumber);
const usdValue = await this.getUSDValue(balance.asset, balance.amount);
assetBreakdown.push({ ...balance, usdValue });
totalTVL += usdValue;
}
const block = await this.client.getBlock({ blockNumber });
return {
blockNumber,
timestamp: Number(block.timestamp),
totalTVL,
assetBreakdown,
deltaFromPrev: 0n,
deltaPct: 0
};
}
}
Статистические методы детекции
Z-score детектор
Аномалия определяется как отклонение, превышающее N стандартных отклонений от исторического среднего:
class ZScoreDetector {
private windowSize = 100; // последние 100 блоков для rolling statistics
detectAnomaly(snapshots: TVLSnapshot[]): AnomalyAlert | null {
if (snapshots.length < this.windowSize) return null;
const recent = snapshots.slice(-this.windowSize);
const deltas = recent.map(s => s.deltaPct);
// Вычисляем rolling mean и std
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 — экстремально редко (~1 в 31,500 блоков)
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`,
blockNumber: latest.blockNumber,
tvlDelta: latest.deltaFromPrev
};
}
return null;
}
}
Rule-based детектор: жёсткие пороги
interface AnomalyRule {
name: string;
check: (current: TVLSnapshot, history: TVLSnapshot[]) => AnomalyAlert | null;
}
const ANOMALY_RULES: AnomalyRule[] = [
{
name: 'RAPID_DRAIN',
check: (current, history) => {
// TVL упал более чем на 20% за последние 10 блоков
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`,
blockNumber: current.blockNumber,
tvlDelta: current.totalTVL - tenBlocksAgo.totalTVL
};
}
return null;
}
},
{
name: 'SINGLE_BLOCK_ANOMALY',
check: (current) => {
// Изменение за один блок превышает 10%
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)}%`,
blockNumber: current.blockNumber,
tvlDelta: current.deltaFromPrev
};
}
return null;
}
},
{
name: 'FLASH_LOAN_PATTERN',
check: (current, history) => {
// Большой deposit в предыдущем блоке + большой withdrawal сейчас
if (history.length < 2) return null;
const prev = history[history.length - 1];
const prevIncrease = prev.deltaPct > 15; // +15% в предыдущем блоке
const currentDecrease = current.deltaPct < -10; // -10% сейчас
if (prevIncrease && currentDecrease) {
return {
type: 'FLASH_LOAN_PATTERN',
severity: 'critical',
message: `Flash loan pattern: +${prev.deltaPct.toFixed(1)}% then -${Math.abs(current.deltaPct).toFixed(1)}%`,
blockNumber: current.blockNumber,
tvlDelta: current.deltaFromPrev
};
}
return null;
}
}
];
Анализ крупных транзакций
Помимо aggregate TVL, важно отслеживать индивидуальные крупные транзакции:
class LargeTransactionMonitor {
// Порог для "whale" транзакции — 1% от TVL
private whaleTvlThreshold = 0.01;
async monitorWithdrawals(
protocolAddress: string,
currentTVL: bigint
) {
const client = createPublicClient({ transport: webSocket(WS_RPC) });
client.watchContractEvent({
address: protocolAddress,
abi: PROTOCOL_ABI,
eventName: 'Withdraw',
onLogs: async (logs) => {
for (const log of logs) {
const withdrawAmount = log.args.amount as bigint;
const usdValue = await getUSDValue(log.args.asset, withdrawAmount);
const pctOfTVL = Number(usdValue * 10000n / currentTVL) / 100;
if (pctOfTVL > this.whaleTvlThreshold * 100) {
await this.alertWhaleWithdrawal({
address: log.args.user,
amount: withdrawAmount,
usdValue,
pctOfTVL,
txHash: log.transactionHash,
blockNumber: log.blockNumber
});
}
}
}
});
}
}
Интеграция с DeFiLlama
DeFiLlama предоставляет исторические TVL данные для сравнения:
async function checkTVLConsistency(
protocolSlug: string,
ourTVL: bigint
): Promise<{ consistent: boolean; defiLlamaTVL: number; deviation: number }> {
const response = await fetch(`https://api.llama.fi/protocol/${protocolSlug}`);
const data = await response.json();
// Последнее значение из DeFiLlama (в USD)
const chainTvls = data.chainTvls;
const latestTVL = Object.values(chainTvls)[0] as any;
const defiLlamaTVL = latestTVL.tvl[latestTVL.tvl.length - 1].totalLiquidityUSD;
const ourTVLUSD = Number(ourTVL) / 1e6; // если scaled 1e6
const deviation = Math.abs(ourTVLUSD - defiLlamaTVL) / defiLlamaTVL * 100;
return {
consistent: deviation < 5, // допустимо 5% расхождение
defiLlamaTVL,
deviation
};
}
Alert система
interface AlertChannel {
send(alert: AnomalyAlert): Promise<void>;
}
class TelegramAlertChannel implements AlertChannel {
async send(alert: AnomalyAlert) {
const emoji = alert.severity === 'critical' ? '🚨' : '⚠️';
const message = `
${emoji} *TVL ANOMALY DETECTED*
Protocol: \`${PROTOCOL_NAME}\`
Type: \`${alert.type}\`
Block: \`${alert.blockNumber}\`
Message: ${alert.message}
TVL Delta: \`$${formatUSD(alert.tvlDelta)}\`
Time: ${new Date().toISOString()}
`;
await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: 'POST',
body: JSON.stringify({
chat_id: ALERT_CHAT_ID,
text: message,
parse_mode: 'Markdown',
disable_web_page_preview: true
})
});
}
}
class PagerDutyChannel implements AlertChannel {
async send(alert: AnomalyAlert) {
if (alert.severity !== 'critical') return; // PD только для critical
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
routing_key: PAGERDUTY_KEY,
event_action: 'trigger',
payload: {
summary: `TVL Anomaly: ${alert.type} on ${PROTOCOL_NAME}`,
severity: 'critical',
source: 'TVL Monitor',
custom_details: alert
}
})
});
}
}
Метрики для dashboard
Кроме алертов — real-time dashboard с ключевыми метриками:
| Метрика | Описание | Update frequency |
|---|---|---|
| Current TVL | Текущий TVL в USD | Per block |
| 24h TVL Change | Изменение за 24 часа | Per block |
| TVL Velocity | Rate of change ($/block) | Per block |
| Largest single withdrawal (24h) | Самый крупный вывод | Per block |
| Anomaly score | Composite score (0–100) | Per block |
| Alert history | Последние N алертов | Per alert |
Система детекции аномалий — не замена аудиту, а дополнительный слой. Аудит находит уязвимости до запуска. Мониторинг TVL даёт шанс среагировать на атаку в процессе — остановить контракт через emergency pause, уведомить пользователей, связаться с биржами для freeze аккаунтов атакующего.







