Разработка системы policy engine для управления транзакциями
Классический сценарий: кастодиальный кошелёк или корпоративный мультисиг. Сотрудник инициирует вывод $500K на внешний адрес. Контракт не возражает — подписи собраны. Но адрес попал под санкции OFAC два часа назад. Или лимит на дневной вывод превышен, потому что параллельно прошло ещё три транзакции через API. Или это просто адрес с risk score 95 по Chainalysis.
Policy engine — это слой между инициатором транзакции и её исполнением, который применяет набор правил до подписания. Не после, не through monitoring — именно до. На этом уровне можно остановить транзакцию, потребовать дополнительные подписи, залоггировать для compliance или автоматически отклонить.
Архитектура policy engine
Уровни применения политик
Policy engine может существовать на нескольких уровнях — их часто смешивают, что порождает проблемы:
Off-chain pre-execution — самый распространённый и практичный. Правила проверяются в сервисе до того, как транзакция уходит на подпись и в мемпул. Гибкий, дешёвый, поддерживает любую логику. Недостаток: требует доверия к этому сервису; при компрометации его — политики обходятся.
On-chain enforcement — смарт-контракт, который является точкой входа для всех транзакций и содержит логику политик. Safe{Core} Protocol Hooks — пример такого механизма. Гарантии сильнее, но логика ограничена тем, что можно выразить on-chain: нет доступа к внешним данным без оракулов, каждая проверка стоит gas.
Hybrid — политики верифицируются off-chain, но контракт принимает proof того, что проверка прошла (commitment scheme или trusted signer attestation).
Модель правил
Правило состоит из условия и действия. Условия могут быть:
- Параметрические:
amount > threshold,recipient in whitelist,token == USDC - Контекстуальные:
sender.role == OPERATOR,time_of_day in 09:00-18:00,daily_volume + amount <= limit - Внешние:
chainalysis_risk_score(recipient) < 70,ofac_check(recipient) == CLEAR - On-chain:
recipient.is_contract == false,token.paused == false
Действия: ALLOW, DENY, REQUIRE_APPROVAL(n_signers), DELAY(duration), NOTIFY(channels).
Правила имеют приоритеты и могут конфликтовать. Нужна чёткая семантика: first-match, или all-must-pass, или whitelist-overrides-blacklist. Это проектное решение, которое закладывается в движок.
interface PolicyRule {
id: string;
priority: number;
conditions: Condition[];
conditionLogic: 'AND' | 'OR';
action: PolicyAction;
metadata: { name: string; owner: string; updatedAt: number };
}
interface PolicyAction {
type: 'ALLOW' | 'DENY' | 'REQUIRE_APPROVAL' | 'DELAY';
params?: {
requiredApprovers?: string[]; // адреса или роли
minApprovals?: number;
delaySeconds?: number;
notifyChannels?: string[];
};
}
Evaluator: порядок вычисления
Движок должен обрабатывать правила эффективно — особенно когда правил сотни, а часть требует внешних вызовов (API compliance провайдера).
Оптимальная стратегия: short-circuit evaluation с кэшированием. Сначала проверяются дешёвые локальные условия (параметры транзакции, роли), потом — кэшированные внешние данные, последними — свежие API-вызовы с таймаутом.
class PolicyEvaluator:
def evaluate(self, tx: Transaction, context: EvalContext) -> PolicyDecision:
sorted_rules = sorted(self.rules, key=lambda r: r.priority, reverse=True)
for rule in sorted_rules:
# Сначала дешёвые условия
cheap_conditions = [c for c in rule.conditions if c.type == 'PARAMETRIC']
if not self._eval_conditions(cheap_conditions, tx, context):
continue
# Потом дорогие (с кэшем)
expensive_conditions = [c for c in rule.conditions if c.type == 'EXTERNAL']
cache_key = self._cache_key(expensive_conditions, tx)
cached = self.cache.get(cache_key)
results = cached if cached else self._eval_external(expensive_conditions, tx)
self.cache.set(cache_key, results, ttl=300)
if self._eval_conditions_with_results(rule.conditions, results, rule.conditionLogic):
return PolicyDecision(action=rule.action, rule_id=rule.id)
return PolicyDecision(action=DEFAULT_ACTION)
On-chain реализация: Safe Hooks
Safe{Core} Protocol (EIP-7579 совместимый) предоставляет механизм хуков:
interface ISafeProtocolHooks {
function preCheck(
Safe safe,
SafeTransaction calldata tx,
uint256 executionType,
bytes calldata executionMeta
) external returns (bytes memory preCheckData);
function postCheck(
Safe safe,
bool success,
bytes calldata preCheckData
) external;
}
preCheck вызывается до исполнения. Если revert — транзакция не проходит. Здесь можно: проверить whitelist/blacklist (хранится в storage хука), проверить лимиты (через аккумуляторы по адресам/токенам), требовать дополнительный approval через timelock.
Пример лимитного хука:
contract DailyLimitHook is ISafeProtocolHooks {
mapping(address => mapping(address => uint256)) public dailyVolume; // safe => token => amount
mapping(address => mapping(address => uint256)) public lastResetDay;
mapping(address => mapping(address => uint256)) public dailyLimit;
function preCheck(Safe safe, SafeTransaction calldata tx, uint256, bytes calldata)
external returns (bytes memory)
{
address token = _extractToken(tx.data);
uint256 amount = _extractAmount(tx.data);
uint256 today = block.timestamp / 1 days;
address safeAddr = address(safe);
if (lastResetDay[safeAddr][token] < today) {
dailyVolume[safeAddr][token] = 0;
lastResetDay[safeAddr][token] = today;
}
require(
dailyVolume[safeAddr][token] + amount <= dailyLimit[safeAddr][token],
"DailyLimitExceeded"
);
return abi.encode(token, amount); // передаём в postCheck
}
function postCheck(Safe safe, bool success, bytes calldata preCheckData) external {
if (success) {
(address token, uint256 amount) = abi.decode(preCheckData, (address, uint256));
dailyVolume[address(safe)][token] += amount;
}
}
}
Compliance интеграции
Для финансовых продуктов policy engine неизбежно включает интеграцию с compliance провайдерами. Основные:
Chainalysis — KYT API. Проверка адресов (риск-скор) и транзакций (exposure к известным кластерам). Latency: 200–800ms, нужен кэш и graceful degradation.
Elliptic — аналогичный функционал, немного другая модель оценки риска. Используется в Fireblocks.
TRM Labs — специализируется на cross-chain анализе, хорошо покрывает Solana и Tron.
OFAC screening — можно через те же провайдеры или через самостоятельно поддерживаемый snapshot SDN списка (обновляется нечасто, можно хранить локально и обновлять через webhook).
Важный момент: compliance API имеют SLA и могут быть недоступны. Policy engine должен иметь явную политику для случая EXTERNAL_CHECK_TIMEOUT: fail-open (разрешить с логом) vs. fail-closed (заблокировать). Это бизнес-решение, но его нужно зафиксировать.
Мониторинг и аудит
Policy engine без полного audit log бесполезен для compliance. Каждое решение должно содержать:
- Transaction hash или pre-tx ID
- Список применённых правил и их результаты
- Значения всех условий в момент оценки
- Финальное решение и исполнитель
- Timestamp с точностью до миллисекунды
Это неизменяемый лог. Хранение: PostgreSQL с append-only таблицей + репликация в S3/Arweave для long-term retention. Для регуляторных требований — минимум 5 лет.
| Компонент | Технология |
|---|---|
| Rule storage | PostgreSQL + Redis cache |
| Evaluator | Go / Python service |
| On-chain hooks | Solidity (Safe Protocol) |
| Compliance API | Chainalysis / Elliptic / TRM |
| Audit log | PostgreSQL → S3 |
| Admin UI | React + role-based access |
Разработка policy engine — это не просто набор if-else. Это система с формальной семантикой правил, гарантиями атомарности, аудитируемостью и операционной надёжностью. Сделанная правильно, она становится фундаментом для получения лицензий и прохождения compliance-аудитов.







