Мониторинг on-chain метрик проекта
Ваш смарт-контракт задеплоен. Что происходит с ним в реальном времени — знает ли команда? Сколько уникальных пользователей взаимодействовало за последние 24 часа? Не аномально ли вырос объём транзакций прямо сейчас? Нет ли паттернов указывающих на попытку эксплойта? On-chain мониторинг отвечает на эти вопросы до того как появится проблема, а не после.
Что мониторить и зачем
Разделю метрики на три категории по назначению:
Операционные метрики — для DevOps и команды:
- Частота вызовов ключевых функций контракта
- Gas consumption по методам (аномальный рост = дорогие операции или атака)
- Количество failed транзакций (рост failed/total ratio = возможная атака или баг)
- TVL (Total Value Locked) если контракт держит активы
Бизнес-метрики — для продукта:
- DAU/MAU on-chain (уникальные адреса за период)
- Retention — адреса вернувшиеся через N дней
- Объём в USD через конкретные функции
- Топ-N адресов по активности (whale monitoring)
Безопасность — для Security:
- Крупные выводы (> threshold) за короткий период
- Необычные паттерны вызовов (flash loan + контракт в одном блоке)
- Изменения в ownership/admin роли
- Вызовы с новых адресов с большим балансом
Источники данных
Event logs — основной источник. Хорошо спроектированный контракт эмитирует событие для каждого значимого действия. Если событий нет — добавьте их (если контракт обновляемый) или используйте trace calls.
Trace calls — внутренние вызовы между контрактами. Нужен debug_traceTransaction или trace_transaction. Используется для отслеживания движения средств через несколько контрактов в одной транзакции.
Storage slots — прямое чтение состояния контракта. eth_getStorageAt(address, slot, blockNumber). Полезно для метрик которые не эмитируются в события: текущий TVL, размер пулов.
Dune Analytics / Flipside — SQL-запросы к проиндексированным блокчейн-данным. Быстрый старт для аналитики, но vendor lock-in и задержка данных (не реалтайм).
Архитектура мониторинга
┌─────────────────┐
│ Blockchain RPC │
│ (Alchemy/own) │
└────────┬────────┘
│ eth_getLogs / WebSocket
┌────────────▼──────────────┐
│ Event Collector │
│ (подписка на контракт) │
└────────────┬──────────────┘
│
┌────────────▼──────────────┐
│ Metrics Processor │
│ Декодирование ABI, │
│ агрегация, enrichment │
└──────┬────────────┬────────┘
│ │
┌────────────▼──┐ ┌──────▼──────────────┐
│ TimescaleDB │ │ Prometheus/VictoriaDB│
│ (история) │ │ (реалтайм метрики) │
└───────────────┘ └──────────────────────┘
│
┌─────────▼──────────┐
│ Grafana Dashboard │
│ + Alertmanager │
└────────────────────┘
Реализация event collector
import { createPublicClient, webSocket, parseAbiItem, decodeEventLog } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
});
const CONTRACT_ABI = [
parseAbiItem('event Deposit(address indexed user, uint256 amount)'),
parseAbiItem('event Withdraw(address indexed user, uint256 amount)'),
parseAbiItem('event Swap(address indexed user, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut)'),
];
// WebSocket подписка на события контракта
const unwatch = client.watchContractEvent({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
onLogs: async (logs) => {
for (const log of logs) {
await processEvent(log);
}
},
onError: (error) => {
console.error('WS error, reconnecting...', error);
// viem автоматически переподключается
},
});
async function processEvent(log: any) {
const decoded = decodeEventLog({ abi: CONTRACT_ABI, ...log });
// Запись в TimescaleDB
await db.query(`
INSERT INTO contract_events
(time, block_number, tx_hash, event_name, user_address, amount_usd)
VALUES (NOW(), $1, $2, $3, $4, $5)
`, [log.blockNumber, log.transactionHash, decoded.eventName,
decoded.args.user, await convertToUSD(decoded.args.amount)]);
// Инкремент Prometheus счётчика
eventCounter.labels(decoded.eventName).inc();
}
Prometheus метрики и алерты
import { Counter, Gauge, Histogram, Registry } from 'prom-client';
const registry = new Registry();
const eventCounter = new Counter({
name: 'contract_events_total',
help: 'Total contract events by type',
labelNames: ['event_name'],
registers: [registry],
});
const tvlGauge = new Gauge({
name: 'contract_tvl_usd',
help: 'Total Value Locked in USD',
registers: [registry],
});
const largeWithdrawalCounter = new Counter({
name: 'contract_large_withdrawals_total',
help: 'Withdrawals above threshold',
labelNames: ['threshold_category'],
registers: [registry],
});
// HTTP endpoint для Prometheus scraping
app.get('/metrics', async (req, res) => {
res.set('Content-Type', registry.contentType);
res.send(await registry.metrics());
});
Alertmanager правила:
groups:
- name: contract_security
rules:
# Крупный вывод — немедленный алерт
- alert: LargeWithdrawal
expr: rate(contract_large_withdrawals_total[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Large withdrawal detected from contract"
# Аномальный рост failed транзакций
- alert: HighFailureRate
expr: |
rate(contract_failed_txns_total[10m]) /
rate(contract_total_txns_total[10m]) > 0.1
for: 5m
annotations:
summary: "More than 10% of transactions failing"
# TVL падает быстро
- alert: TVLDrop
expr: |
(contract_tvl_usd - contract_tvl_usd offset 1h) /
contract_tvl_usd offset 1h < -0.2
for: 2m
annotations:
summary: "TVL dropped by more than 20% in 1 hour"
Whale и аномалия детектор
import asyncio
from web3 import AsyncWeb3
WHALE_THRESHOLD_USD = 100_000
async def monitor_large_transactions(contract, event_name: str):
async for event in contract.events[event_name].get_logs(fromBlock='latest'):
amount_usd = await get_usd_value(event.args.amount, event.args.token)
if amount_usd > WHALE_THRESHOLD_USD:
await send_telegram_alert(
f"🐋 Whale {event_name}: ${amount_usd:,.0f}\n"
f"Address: {event.args.user}\n"
f"Tx: https://etherscan.io/tx/{event.transactionHash.hex()}"
)
# Проверка flash loan паттерна: deposit + withdraw в одном блоке
if await is_flash_loan_pattern(event):
await send_telegram_alert(
f"⚠️ Flash loan pattern detected in block {event.blockNumber}",
level='WARNING'
)
Grafana дашборд
Ключевые панели для DeFi протокола:
Overview: TVL (gauge + time series), 24h Volume, DAU, Total Users (cumulative)
Activity: Events per minute (time series, разбивка по типу), Gas used per block, Failed tx ratio
Security: Large transactions (table с последними whale транзакциями), New whale addresses (адреса с первой транзакцией в контракте + крупная сумма), Flash loan detection events
Economics: Fee revenue over time, Token price correlation с активностью контракта
Дашборд в JSON — версионируется в git вместе с кодом контракта.
Исторические данные и ретроспективный анализ
Реалтайм мониторинг ловит аномалии сейчас. Для ретроспективного анализа нужна история:
-- Ежедневные активные пользователи
SELECT
date_trunc('day', time) AS day,
COUNT(DISTINCT user_address) AS dau,
SUM(amount_usd) AS volume_usd
FROM contract_events
WHERE event_name IN ('Deposit', 'Swap')
GROUP BY 1
ORDER BY 1 DESC;
-- Retention: пользователи вернувшиеся через 7 дней
WITH first_use AS (
SELECT user_address, MIN(time) AS first_time
FROM contract_events GROUP BY 1
),
return_use AS (
SELECT DISTINCT e.user_address
FROM contract_events e
JOIN first_use f ON e.user_address = f.user_address
WHERE e.time > f.first_time + INTERVAL '7 days'
AND e.time < f.first_time + INTERVAL '14 days'
)
SELECT
COUNT(r.user_address)::float / COUNT(f.user_address) AS week1_retention
FROM first_use f
LEFT JOIN return_use r ON f.user_address = r.user_address;
Процесс развёртывания
День 1: настройка event collector, подключение к RPC, запись сырых событий в TimescaleDB. Тест на нескольких блоках с реальными транзакциями.
День 2: Prometheus метрики, первый Grafana дашборд, настройка базовых алертов (TVL drop, high failure rate).
День 3: whale detector, security алерты, тестирование алертов через симуляцию транзакций, runbook для команды.
Итого 1-3 дня в зависимости от сложности контракта и количества мониторируемых метрик.







