Мониторинг 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 мониторинг)
Безпека — для 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 (таблиця з останніми whale транзакціями), New whale addresses (адреси з першою транзакцією в контракті + крупна сума), Flash loan detection события
Economics: Fee revenue over time, Token price кореляція з активністю
Дашборд у 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 детектор, security алерти, тестування алертів через симуляцію транзакцій, runbook для команди.
Усього 1-3 дні залежно від складності контракту та кількості мониторюваних метрик.







