Розробка системи policy engine для управління транзакціями
Класичний сценарій: кастодіальний гаманець або корпоративний мультисиг. Сотрудник ініціює вивід $500K на зовнішню адресу. Контракт не заперечує — підписи зібрані. Але адреса потрапила під санкції OFAC дві години тому назад. Або ліміт на денний вивід перевищений, тому що паралельно пройшло ще три транзакції через API. Або це просто адреса з risk score 95 по Chainalysis.
Policy engine — це шар між ініціатором транзакції та її виконанням, який застосовує набір правил до підписання. Не після, не через моніторинг — саме до. На цьому рівні можна зупинити транзакцію, потребувати додаткові підписи, залоггувати для compliance або автоматично відклонити.
Архітектура policy engine
Рівні застосування політик
Policy engine може існувати на кількох рівнях — їх часто змішують, що породжує проблеми:
Off-chain pre-execution — найпоширеніший та практичний. Правила перевіряються в сервісі до того, як транзакція уходить на підпис та в мемпул. Гнучкий, дешевий, підтримує будь-яку логіку. Недолік: потребує довіри до цього сервісу; при компрометації його — політики обходяться.
On-chain enforcement — смарт-контракт, який є точкою входу для всіх транзакцій та містить логіку політик. Safe{Core} Protocol Hooks — приклад такого механізму. Гарантії сильніше, але логіка обмежена тим, що можна висловити on-chain: нема доступу до зовнішніх даних без оракулів, кожна перевірка коштує газ.
Hybrid — політики верифікуються off-chain, але контракт приймає доказ того, що перевірка пройшла (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. Перевірка адрес (risk-скор) та транзакцій (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-аудитів.







