Разработка системы мониторинга смарт-контрактов
Задеплоенный контракт без мониторинга — это производственный сервер без логов. Что-то сломается, и вы узнаете об этом последними — из Twitter. Система мониторинга смарт-контрактов решает три задачи: реальное время (алерты о важных событиях), историческая аналитика (паттерны использования, аномалии) и операционный контроль (gas usage, failed transactions, upgrade события).
Что мониторить
Не все события одинаково важны. Правильная приоритизация:
Critical (немедленный алерт, on-call):
- Любое изменение ownership или roles
- Вызов функций upgrade/migrate
- Вывод средств выше threshold (настраивается)
- Вызов pause/unpause
- Failed транзакции к критичным функциям (могут сигнализировать об атаке)
High (алерт в Telegram/Slack, 15 минут):
- Нетипично большие транзакции
- Резкое изменение TVL (> 10% за блок)
- Новые адреса с крупными позициями
- Oracle price deviation
Medium (digest раз в час):
- Объём транзакций, уникальные пользователи
- Gas usage trends
- Error rates по типам функций
Low (ежедневный отчёт):
- Общая статистика протокола
- New users, retention
- Fee collected
Архитектура системы
Event Listener сервис
import { ethers } from 'ethers';
import { Redis } from 'ioredis';
interface ContractConfig {
address: string;
abi: any[];
startBlock: number;
name: string;
}
class ContractMonitor {
private provider: ethers.WebSocketProvider;
private redis: Redis;
private contracts: Map<string, ethers.Contract> = new Map();
private alertQueue: AlertQueue;
constructor(wsUrl: string, redisUrl: string) {
this.provider = new ethers.WebSocketProvider(wsUrl);
this.redis = new Redis(redisUrl);
this.alertQueue = new AlertQueue();
// Автоматический reconnect при обрыве WS
this.provider.websocket.on('close', () => {
setTimeout(() => this.reconnect(), 5000);
});
}
async addContract(config: ContractConfig): Promise<void> {
const contract = new ethers.Contract(config.address, config.abi, this.provider);
this.contracts.set(config.address.toLowerCase(), contract);
// Подписка на все события контракта
contract.on('*', async (event) => {
await this.processEvent(config.name, event);
});
console.log(`Monitoring ${config.name} at ${config.address}`);
}
private async processEvent(contractName: string, event: ethers.EventLog): Promise<void> {
const enriched = {
contractName,
eventName: event.eventName,
args: Object.fromEntries(
Object.entries(event.args).filter(([k]) => isNaN(Number(k)))
),
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
timestamp: Date.now(),
};
// Сохраняем в Redis stream для аналитики
await this.redis.xadd(
`events:${contractName}`,
'*',
'data', JSON.stringify(enriched)
);
// Оцениваем severity
const severity = this.assessSeverity(enriched);
if (severity !== 'low') {
await this.alertQueue.push({ ...enriched, severity });
}
}
private assessSeverity(event: any): 'low' | 'medium' | 'high' | 'critical' {
const criticalEvents = ['OwnershipTransferred', 'RoleGranted', 'Upgraded', 'Paused'];
const highEvents = ['LargeWithdrawal', 'FlashLoan', 'OracleUpdate'];
if (criticalEvents.includes(event.eventName)) return 'critical';
if (highEvents.includes(event.eventName)) return 'high';
// Проверяем на крупные суммы
if (event.args?.amount && BigInt(event.args.amount) > LARGE_AMOUNT_THRESHOLD) {
return 'high';
}
return 'low';
}
}
Invariant checker
Запускается каждый блок, проверяет финансовые инварианты:
interface Invariant {
name: string;
severity: 'high' | 'critical';
check: (state: ProtocolState) => Promise<boolean>;
message: (state: ProtocolState) => string;
}
const invariants: Invariant[] = [
{
name: 'tvl_drop',
severity: 'critical',
check: async (state) => {
const drop = (state.prevTVL - state.currentTVL) / state.prevTVL;
return drop < 0.15; // не более 15% падения за один блок
},
message: (s) => `TVL dropped from ${s.prevTVL} to ${s.currentTVL}`,
},
{
name: 'total_supply_consistency',
severity: 'critical',
check: async (state) => {
// totalSupply должен совпадать с суммой балансов (на выборке)
return Math.abs(state.totalSupply - state.sumOfTopBalances) < state.totalSupply * 0.001;
},
message: () => 'Total supply inconsistency detected',
},
{
name: 'oracle_freshness',
severity: 'high',
check: async (state) => {
const staleSeconds = Date.now() / 1000 - state.oracleLastUpdate;
return staleSeconds < 3600; // oracle обновлялся не позже часа назад
},
message: (s) => `Oracle stale: last update ${s.oracleLastUpdate}`,
},
];
Алертинг и интеграции
Telegram Bot
Telegram — стандарт для крипто-команд. Структурированные алерты с markdown:
async function sendTelegramAlert(alert: Alert): Promise<void> {
const emoji = {
critical: '🚨',
high: '⚠️',
medium: 'ℹ️',
}[alert.severity];
const message = `
${emoji} *${alert.severity.toUpperCase()}: ${alert.eventName}*
Contract: \`${alert.contractName}\`
Block: [${alert.blockNumber}](https://etherscan.io/block/${alert.blockNumber})
Tx: [${alert.txHash.slice(0, 10)}...](https://etherscan.io/tx/${alert.txHash})
${formatArgs(alert.args)}
_${new Date(alert.timestamp).toISOString()}_
`.trim();
await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: ALERT_CHAT_ID,
text: message,
parse_mode: 'Markdown',
disable_web_page_preview: true,
}),
});
}
PagerDuty интеграция для critical
Critical алерты должны будить дежурного. PagerDuty Events API v2:
async function triggerPagerDuty(alert: Alert): Promise<void> {
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
routing_key: PAGERDUTY_INTEGRATION_KEY,
event_action: 'trigger',
payload: {
summary: `${alert.contractName}: ${alert.eventName}`,
severity: 'critical',
source: 'smart-contract-monitor',
custom_details: alert,
},
links: [
{ href: `https://etherscan.io/tx/${alert.txHash}`, text: 'Etherscan' }
],
}),
});
}
Grafana Dashboards
Метрики хранятся в Prometheus, визуализируются в Grafana. Ключевые панели:
| Панель | Метрики | Назначение |
|---|---|---|
| Protocol Health | TVL, active users, tx/hour | Общее состояние |
| Transaction Flow | Deposits, withdrawals, fees | Финансовые потоки |
| Gas Usage | avg gas, gas price trends | Операционные расходы |
| Error Rate | failed txs по типам | Качество UX |
| Security | large transfers, role changes | Безопасность |
// Prometheus metrics
import { Counter, Gauge, Histogram } from 'prom-client';
const txCounter = new Counter({
name: 'contract_transactions_total',
help: 'Total transactions by contract and event type',
labelNames: ['contract', 'event_name', 'status'],
});
const tvlGauge = new Gauge({
name: 'protocol_tvl_usd',
help: 'Total Value Locked in USD',
labelNames: ['contract'],
});
const transferAmountHist = new Histogram({
name: 'transfer_amount_usd',
help: 'Distribution of transfer amounts',
labelNames: ['contract', 'direction'],
buckets: [100, 1000, 10000, 100000, 1000000],
});
Технический стек
| Компонент | Выбор | Обоснование |
|---|---|---|
| Event streaming | Alchemy Webhooks | Надёжный WebSocket, автоматический retry |
| Storage | Redis Streams + PostgreSQL | Redis для real-time, PG для исторических данных |
| Alerting | Telegram Bot + PagerDuty | Команда + on-call дежурный |
| Metrics | Prometheus + Grafana | Стандарт, богатые dashboards |
| Hosting | Railway / Fly.io | Дешёвые always-on сервисы |
| Multi-chain | Отдельный инстанс per chain | Изоляция проблем |
Процесс работы
Аналитика (2-3 дня). Список контрактов, карта событий, severity классификация, threshold значения для алертов.
Разработка (2-3 недели). Event listener + invariant checker + alerting pipeline + Prometheus metrics + Grafana dashboards.
Тестирование (3-5 дней). Симуляция critical событий через fork, проверка delivery алертов, нагрузочный тест на высоком объёме событий.
Деплой и настройка (2-3 дня). Production deploy, alert channel setup, runbook для команды.
Сроки зависят от количества контрактов, цепей и сложности бизнес-логики мониторинга.







