Разработка системы детекции атак на governance
Атаки на DeFi governance — класс угроз, который недооценивают. DeFi-разработчики традиционно фокусируются на безопасности core контрактов: reentrancy, overflow, oracle manipulation. Но governance — это backdoor в любой протокол: успешная атака на governance даёт атакующему право менять любые параметры, дренировать treasury, апгрейдить контракты к malicious версиям.
Beanstalk Farms ($182M, апрель 2022) — атака была выполнена через легитимный governance процесс. Атакующий использовал flash loan для получения большинства votes и моментально прошёл злонамеренный пропозал. Не было взломано ни одного смарт-контракта — только воспользовались отсутствием защит в governance механизме.
Система детекции атак — это on-chain мониторинг + off-chain аналитика, которые идентифицируют подозрительные паттерны в governance активности до того как атака успевает причинить ущерб.
Классификация атак на governance
Класс 1: Flash loan governance attack
Атакующий одалживает governance токены на одну транзакцию, голосует, возвращает. Работает только если:
- Нет voting delay (пропозал можно исполнить сразу)
- Snapshot голосования = текущий блок
- Нет требования длительного holding period
Mitigation уже в контракте: ERC-20Votes checkpoints + voting delay. Детекция: мониторинг транзакций где flash loan и governance vote происходят в одном tx.
Класс 2: Whale accumulation attack
Атакующий постепенно накапливает governing tokens через OTC, DEX покупки, заёмные позиции — не раскрывая намерения. Когда достигает достаточного порога — создаёт и исполняет злонамеренный пропозал.
Это легальная активность до момента атаки. Детекция: паттерны аномально быстрого накопления tokens на неизвестных адресах.
Класс 3: Bribe и voting coercion
Атакующий подкупает делегатов через bribing протоколы (Hidden Hand, Votium) или напрямую, чтобы те проголосовали за злонамеренный пропозал. Делегаты могут действовать добросовестно, не понимая последствий сложного технического пропозала.
Детекция: резкий рост bribe activity для конкретного пропозала, несоответствие сложности пропозала и скорости его прохождения.
Класс 4: Long-range накопление и координированная атака
Несколько адресов, которые кажутся независимыми, накапливают tokens на протяжении месяцев. Координированно голосуют. On-chain они не связаны — только общий источник финансирования или общие паттерны активности.
Детекция: graph analysis транзакционных связей, timing correlation между адресами.
Класс 5: Proposal complexity exploit
Легитимный-выглядящий пропозал содержит скрытый malicious эффект. Например, апгрейд контракта где новая реализация имеет backdoor. Или multi-action пропозал где одно действие из пяти — drain treasury.
Детекция: автоматический анализ calldata пропозалов, симуляция исполнения.
On-chain компоненты детекции
Vote Weight Anomaly Detector
contract GovernanceMonitor {
IGovernor public governor;
IVotes public votingToken;
// Threshold: если один адрес голосует > X% от кворума — алерт
uint256 public constant WHALE_THRESHOLD_PERCENT = 20;
// Хранение истории voting power для детекции резкого роста
mapping(address => uint256) public lastKnownVotingPower;
mapping(address => uint256) public lastUpdateBlock;
event WhaleVoteDetected(
uint256 indexed proposalId,
address indexed voter,
uint256 votingPower,
uint256 percentOfQuorum,
uint8 support
);
event RapidPowerAccumulation(
address indexed account,
uint256 previousPower,
uint256 currentPower,
uint256 percentIncrease,
uint256 blocksElapsed
);
function checkVote(
uint256 proposalId,
address voter,
uint8 support,
uint256 weight
) external {
uint256 quorum = governor.quorum(governor.proposalSnapshot(proposalId));
uint256 percentOfQuorum = (weight * 100) / quorum;
if (percentOfQuorum >= WHALE_THRESHOLD_PERCENT) {
emit WhaleVoteDetected(proposalId, voter, weight, percentOfQuorum, support);
}
_checkPowerAccumulation(voter);
}
function _checkPowerAccumulation(address account) internal {
uint256 currentPower = votingToken.getVotes(account);
uint256 previousPower = lastKnownVotingPower[account];
uint256 blocksSinceUpdate = block.number - lastUpdateBlock[account];
if (previousPower > 0 && blocksSinceUpdate < 1000) { // ~3.5 часа
uint256 percentIncrease = ((currentPower - previousPower) * 100) / previousPower;
if (percentIncrease > 50) { // >50% роста за <3.5 часа
emit RapidPowerAccumulation(
account,
previousPower,
currentPower,
percentIncrease,
blocksSinceUpdate
);
}
}
lastKnownVotingPower[account] = currentPower;
lastUpdateBlock[account] = block.number;
}
}
Этот контракт вызывается через Governor hook (если Governor поддерживает) или через off-chain мониторинг с последующим on-chain записью алерта.
Proposal Simulation Engine
Автоматическая симуляция исполнения каждого нового пропозала до его обсуждения сообществом.
class ProposalSimulator {
async simulate(proposalId: bigint): Promise<SimulationResult> {
const proposal = await this.getProposalDetails(proposalId);
// Форк mainnet на текущем блоке
const fork = await this.createFork();
const results: ActionResult[] = [];
for (const action of proposal.actions) {
try {
// Симулируем исполнение через eth_call
const result = await fork.simulate({
from: timelockAddress,
to: action.target,
data: action.calldata,
value: action.value
});
results.push({
success: true,
action,
stateChanges: await this.analyzeStateChanges(fork, result),
tokenTransfers: await this.extractTransfers(result.logs)
});
} catch (error) {
results.push({ success: false, action, error: error.message });
}
}
// Анализ опасных паттернов в результатах
const risks = await this.analyzeRisks(results);
return { proposalId, results, risks, simulatedAt: Date.now() };
}
async analyzeRisks(results: ActionResult[]): Promise<Risk[]> {
const risks: Risk[] = [];
for (const result of results) {
// Трансферы из treasury > $1M
const largeTransfers = result.tokenTransfers.filter(
t => t.from === TREASURY_ADDRESS && t.valueUSD > 1_000_000
);
if (largeTransfers.length > 0) {
risks.push({
level: 'HIGH',
type: 'LARGE_TREASURY_TRANSFER',
details: largeTransfers
});
}
// Изменение owner/admin критических контрактов
const ownerChanges = result.stateChanges.filter(
c => c.slot === OWNER_SLOT && CRITICAL_CONTRACTS.includes(c.address)
);
if (ownerChanges.length > 0) {
risks.push({
level: 'CRITICAL',
type: 'OWNERSHIP_TRANSFER',
details: ownerChanges
});
}
// Upgrade proxy к неизвестной implementation
const upgrades = result.stateChanges.filter(
c => c.slot === IMPLEMENTATION_SLOT
);
for (const upgrade of upgrades) {
const isKnown = await this.isKnownContract(upgrade.newValue);
if (!isKnown) {
risks.push({
level: 'CRITICAL',
type: 'UPGRADE_TO_UNKNOWN_CONTRACT',
details: upgrade
});
}
}
}
return risks;
}
}
Decoding и человекочитаемое описание calldata
Технический пропозал должен быть переведён на человеческий язык для community review.
const FUNCTION_REGISTRY: Record<string, FunctionDescriptor> = {
// Compound Governor
'0x3c3e4f7a': {
name: 'setCollateralFactor',
protocol: 'Compound',
category: 'risk-parameter',
description: (params) =>
`Change collateral factor for ${params.cToken} from current to ${formatPercent(params.newCollateralFactorMantissa)}`
},
// Uniswap Governor
'0x4f1ef286': {
name: 'upgradeToAndCall',
protocol: 'Generic',
category: 'upgrade',
description: (params) =>
`Upgrade proxy implementation to ${params.newImplementation}`
}
};
async function decodeProposalAction(target: string, calldata: string): Promise<string> {
const selector = calldata.slice(0, 10);
const descriptor = FUNCTION_REGISTRY[selector];
if (!descriptor) {
// Неизвестная функция — высокий риск
return `⚠️ Unknown function call to ${target}: selector ${selector}`;
}
const iface = new ethers.Interface([`function ${descriptor.name}(...)`]);
const params = iface.decodeFunctionData(descriptor.name, calldata);
return descriptor.description(params);
}
Off-chain аналитика: graph analysis
Координированные атаки выявляются через анализ транзакционного графа.
Кластеризация адресов
import networkx as nx
from collections import defaultdict
class AddressCluster:
def __init__(self, provider):
self.provider = provider
self.graph = nx.DiGraph()
def build_funding_graph(self, addresses: list[str], lookback_blocks: int):
"""Строит граф финансирования между адресами"""
for addr in addresses:
txs = self.get_outgoing_transfers(addr, lookback_blocks)
for tx in txs:
self.graph.add_edge(
tx['from'],
tx['to'],
weight=tx['value'],
token=tx['token']
)
def find_common_funders(self, voters: list[str]) -> dict:
"""Находит общих финансировщиков для набора voters"""
common_sources = defaultdict(list)
for voter in voters:
# Все адреса, от которых получал funds (глубина 2)
ancestors = nx.ancestors(self.graph, voter)
for ancestor in ancestors:
common_sources[ancestor].append(voter)
# Адреса финансирующие > 2 voters — подозрительны
suspicious = {
source: voters
for source, voters in common_sources.items()
if len(voters) >= 3
}
return suspicious
def detect_timing_correlation(self, voters: list[str], window_blocks: int = 100):
"""Выявляет voters с одинаковым timing активности"""
activity_windows = {}
for voter in voters:
txs = self.get_all_txs(voter, 10000) # последние 10k блоков
window_ids = set(tx['blockNumber'] // window_blocks for tx in txs)
activity_windows[voter] = window_ids
# Jaccard similarity между voters
correlations = []
for i, v1 in enumerate(voters):
for v2 in voters[i+1:]:
intersection = len(activity_windows[v1] & activity_windows[v2])
union = len(activity_windows[v1] | activity_windows[v2])
similarity = intersection / union if union > 0 else 0
if similarity > 0.7: # >70% одинаковых активных окон
correlations.append((v1, v2, similarity))
return correlations
Bribe detection
Votium, Hidden Hand, Paladin — основные bribing протоколы. Мониторинг:
// Отслеживаем bribe events для конкретного proposal
const votiumContract = new ethers.Contract(VOTIUM_ADDRESS, VOTIUM_ABI, provider);
const filter = votiumContract.filters.NewIncentive(proposalId);
votiumContract.on(filter, async (proposalId, token, amount, event) => {
const amountUSD = await priceOracle.getUSD(token, amount);
if (amountUSD > BRIBE_ALERT_THRESHOLD) {
await alertChannel.send({
type: 'LARGE_BRIBE',
proposalId,
amount: amountUSD,
token,
txHash: event.transactionHash
});
}
});
Alerting и response система
Уровни алертов
| Уровень | Триггер | Действие |
|---|---|---|
| INFO | Новый пропозал создан | Публикация в Discord/Telegram |
| MEDIUM | Whale vote (>20% quorum) | Уведомление security multisig |
| HIGH | Rapid accumulation или flash loan в tx | Пуш-уведомление команде |
| CRITICAL | Симуляция выявила treasury drain / unknown upgrade | Автопауза (если контракт позволяет) + экстренный созыв |
Emergency response автоматика
// Guardian контракт: может моментально отменить пропозал при алерте
contract GovernanceGuardian {
IGovernor public governor;
address[] public guardians; // multisig members
uint256 public threshold;
mapping(bytes32 => uint256) public guardianSignatures;
function signCancelProposal(uint256 proposalId) external {
require(isGuardian[msg.sender], "Not guardian");
bytes32 key = keccak256(abi.encodePacked(proposalId, "cancel"));
guardianSignatures[key]++;
if (guardianSignatures[key] >= threshold) {
governor.cancel(proposalId);
emit ProposalCancelled(proposalId, "Guardian action");
}
}
}
Guardian не может принимать положительные действия — только отменять пропозалы. Это сохраняет децентрализацию (guardian не может украсть governance), но даёт экстренный рычаг.
Интеграция с Forta Network
Forta — decentralized network ботов для on-chain мониторинга. Публикация detection бота:
// forta-agent.js
const { Finding, FindingSeverity, FindingType, ethers } = require('forta-agent');
const GOVERNOR_ADDRESS = '0x...';
const GOVERNOR_ABI = [...];
async function handleTransaction(txEvent) {
const findings = [];
// Детектируем Vote events
const voteEvents = txEvent.filterLog(
'event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason)',
GOVERNOR_ADDRESS
);
for (const event of voteEvents) {
const { voter, proposalId, weight } = event.args;
// Проверяем наличие flash loan в той же транзакции
const flashLoanEvents = txEvent.filterLog([
'event FlashLoan(address indexed target, address indexed initiator, address indexed asset, uint256 amount, uint256 premium, uint16 referralCode)',
]);
if (flashLoanEvents.length > 0) {
findings.push(Finding.fromObject({
name: 'Flash Loan Governance Vote',
description: `Address ${voter} voted on proposal ${proposalId} in same tx as flash loan`,
alertId: 'GOVERNANCE-FLASH-LOAN',
severity: FindingSeverity.Critical,
type: FindingType.Exploit,
metadata: { voter, proposalId: proposalId.toString(), weight: weight.toString() }
}));
}
}
return findings;
}
module.exports = { handleTransaction };
Стек и сроки
On-chain: Solidity + Foundry. GovernanceMonitor контракт + Guardian контракт. Off-chain: TypeScript + Node.js + viem. ProposalSimulator + GraphAnalyzer + BribeMonitor. Alerting: Forta Network + PagerDuty / Telegram webhook. Storage: PostgreSQL для истории proposals и аномалий.
| Компонент | Срок |
|---|---|
| On-chain monitor контракт | 1–2 недели |
| Proposal simulation engine | 2–3 недели |
| Graph analysis (кластеризация) | 2 недели |
| Bribe monitoring | 1 неделя |
| Forta bot | 1 неделя |
| Alerting pipeline | 1 неделя |
| Dashboard | 2–3 недели |
Полная система детекции: 2–3 месяца. Базовый мониторинг с алертами (без graph analysis): 4–6 недель.
Стоимость зависит от количества отслеживаемых протоколов и требуемой глубины аналитики. Интеграция с существующим протоколом на базе OpenZeppelin Governor обходится дешевле — Governor уже предоставляет нужные события.







