Разработка системы паузы смарт-контрактов при аномалиях
Pause механизм — базовый элемент безопасности для любого DeFi протокола, работающего с пользовательскими средствами. Когда начинается эксплойт, у команды есть минуты, иногда секунды, чтобы остановить утечку. Ручная реакция невозможна — нужна автоматика. Но автоматическая пауза, сработавшая ошибочно, тоже наносит ущерб: blocked transactions, недоверие пользователей, потенциальные liquidations из-за невозможности управлять позициями.
Задача — построить систему, которая паузирует контракт при реальных аномалиях с минимумом ложных срабатываний, и при этом сама не становится вектором атаки.
Pausable контракт: базовая реализация
OpenZeppelin Pausable — стандартный starting point:
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract ProtectedVault is Pausable, AccessControl {
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
// Разные роли: автоматический guardian может паузировать,
// только governance может разпаузировать
function pause() external onlyRole(GUARDIAN_ROLE) {
_pause();
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
// DEFAULT_ADMIN = timelock / multisig governance
_unpause();
}
function deposit(uint256 amount) external whenNotPaused {
// ...
}
function withdraw(uint256 amount) external whenNotPaused {
// ...
}
}
Ключевое: разные роли для pause и unpause. Автоматический guardian может быть скомпрометирован или ошибиться — но разпаузировать могут только люди через multisig/governance. Это асимметрия умышленная.
Аномалии: что детектируем
TVL anomaly — резкое падение
Если за N блоков из контракта вышло > X% TVL — что-то не так. Может быть легитимным (крупный withdrawal) или эксплойтом.
contract AnomalyDetector {
uint256 public constant MAX_TVL_DROP_BPS = 1000; // 10% за период
uint256 public constant MONITORING_WINDOW = 100; // блоков
uint256 public tvlSnapshot;
uint256 public snapshotBlock;
function checkTVLAnomaly(uint256 currentTVL) internal {
if (block.number >= snapshotBlock + MONITORING_WINDOW) {
// обновляем snapshot
tvlSnapshot = currentTVL;
snapshotBlock = block.number;
return;
}
if (tvlSnapshot == 0) return;
uint256 dropBps = ((tvlSnapshot - currentTVL) * 10000) / tvlSnapshot;
if (currentTVL < tvlSnapshot && dropBps > MAX_TVL_DROP_BPS) {
_triggerPause("TVL anomaly detected");
}
}
}
Проблема: если протокол позволяет легитимные крупные withdrawals, этот детектор будет срабатывать при выходе крупного LP. Нужна калибровка под конкретный протокол.
Необычно крупная единичная транзакция
Если одна транзакция выводит > Y% от TVL:
uint256 public constant SINGLE_TX_THRESHOLD_BPS = 500; // 5% TVL
modifier checkWithdrawAnomaly(uint256 amount) {
uint256 tvl = totalAssets();
if (tvl > 0 && (amount * 10000 / tvl) > SINGLE_TX_THRESHOLD_BPS) {
// Не паузируем автоматически — только логируем для off-chain
emit LargeWithdrawal(msg.sender, amount, tvl);
}
_;
}
Автоматическая пауза на крупные withdrawals рискованна — это легитимная операция для крупных участников. Лучше — emit события для off-chain мониторинга.
Reentrancy detection on-chain
// Детектор для потенциального reentrancy через callback
uint256 private _callDepth;
modifier noDeepCalls() {
_callDepth++;
if (_callDepth > 1) {
_triggerPause("Reentrancy detected");
revert("Reentrancy");
}
_;
_callDepth--;
}
Это дополнение к стандартному nonReentrant. Разница: вместо просто revert — пауза всего контракта при детекции попытки.
Off-chain мониторинг с автопаузой
On-chain детекторы ограничены: они видят только то, что происходит в текущей транзакции. Более мощный паттерн — off-chain мониторинг + privileged pause transaction.
OpenZeppelin Defender
OZ Defender Sentinel + Autotask — стандартный стек для этого:
// Autotask: вызывается Defender при срабатывании Sentinel условия
const { DefenderRelayProvider, DefenderRelaySigner } = require('@openzeppelin/defender-relay-client/lib/ethers');
exports.handler = async function(credentials) {
const provider = new DefenderRelayProvider(credentials);
const signer = new DefenderRelaySigner(credentials, provider, { speed: 'fast' });
const contract = new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, signer);
// Проверяем условие перед паузой (избегаем ложные срабатывания)
const tvl = await contract.totalAssets();
const threshold = await contract.pauseThreshold();
if (tvl < threshold) {
const tx = await contract.pause();
await tx.wait();
console.log(`Paused. TVL: ${tvl}, Threshold: ${threshold}`);
}
};
Sentinel настраивается на события из контракта (Transfer, Withdrawal) или на условия (баланс < X). При срабатывании — автоматически вызывает Autotask.
Forta Network
Forta — децентрализованная сеть detection ботов. Написать detection bot можно на TypeScript/Python:
import { Finding, HandleTransaction, TransactionEvent, FindingSeverity, FindingType } from 'forta-agent';
const handleTransaction: HandleTransaction = async (txEvent: TransactionEvent) => {
const findings: Finding[] = [];
// Проверяем крупные withdrawals из vault
const withdrawalEvents = txEvent.filterLog(
'Withdrawal(address,uint256,uint256)',
VAULT_ADDRESS
);
for (const event of withdrawalEvents) {
const amount = event.args.assets;
if (amount > LARGE_WITHDRAWAL_THRESHOLD) {
findings.push(
Finding.fromObject({
name: 'Large Vault Withdrawal',
description: `${amount} assets withdrawn in single tx`,
alertId: 'VAULT-LARGE-WITHDRAWAL',
severity: FindingSeverity.High,
type: FindingType.Suspicious,
metadata: {
amount: amount.toString(),
recipient: event.args.receiver,
},
})
);
}
}
return findings;
};
Forta алерты можно интегрировать в Defender через webhook → Autotask цепочку.
Circuit breaker паттерн
Более гибкий паттерн: не полная пауза, а circuit breaker — временное ограничение операций при аномалии:
contract CircuitBreaker {
enum Status { Normal, Restricted, Paused }
Status public status;
uint256 public dailyWithdrawLimit;
uint256 public dailyWithdrawn;
uint256 public lastResetDay;
function withdraw(uint256 amount) external {
require(status != Status.Paused, "Paused");
if (status == Status.Restricted) {
// В restricted режиме — пониженный лимит
require(amount <= restrictedWithdrawLimit, "Exceeds restricted limit");
}
// Daily limit check
uint256 today = block.timestamp / 1 days;
if (today > lastResetDay) {
dailyWithdrawn = 0;
lastResetDay = today;
}
dailyWithdrawn += amount;
require(dailyWithdrawn <= dailyWithdrawLimit, "Daily limit exceeded");
// ... withdraw logic
}
function _setRestricted() internal {
status = Status.Restricted;
emit StatusChanged(Status.Restricted);
}
}
Преимущество circuit breaker перед полной паузой: при превышении дневного лимита протокол не паузируется — просто отклоняет транзакции сверх лимита. Пользователи могут продолжать работать в рамках нормального объёма. При активном эксплойте это ограничивает ущерб без полного останова.
MakerDAO, Compound, Aave используют похожие механизмы (supply/borrow caps, pause guardian).
Governance над pause механизмом
Pause функция сама по себе может стать вектором атаки: скомпрометированный guardian паузирует протокол, делая средства недоступными (DOS). Защита:
Time-limited pause: автоматическая распауза через N блоков, если governance не подтвердит продление:
uint256 public pauseExpiry;
uint256 public constant MAX_AUTO_PAUSE_DURATION = 1 days;
function pause() external onlyGuardian {
_pause();
pauseExpiry = block.timestamp + MAX_AUTO_PAUSE_DURATION;
}
function checkAutoUnpause() public {
if (paused() && block.timestamp > pauseExpiry) {
_unpause();
}
}
Multisig pause с threshold: пауза требует 2-of-3 guardians, а не одного. Один скомпрометированный ключ недостаточен.
Сроки разработки: базовый Pausable с OZ Defender monitoring — 2-3 недели. Полная система с circuit breaker, Forta integration и governance механизмами — 5-7 недель.







