ML-модель виявлення wash trading на блокчейні

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1305 послуг
ML-модель виявлення wash trading на блокчейні
Складний
від 2 тижнів до 3 місяців
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1374
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1256
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    965
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1208
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    667
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    954

Виявлення wash trading: чому це складно і як ML вирішує проблему

Chainalysis повідомляє: на ряді NFT-маркетплейсів частка фіктивного обсягу перевищує 50%, а на деяких — до 80%. Wash trading спотворює ринкові дані, вводить в оману інвесторів і привертає увагу регуляторів. Традиційні порогові методи (наприклад, виявлення повторюваних угод між тими самими адресами) пропускають до 60% маніпуляцій. Модель на основі графового аналізу блокчейну та Gradient Boosting з SHAP-інтерпретацією піднімає точність до 95% (ROC-AUC >0.95). Ми розробляємо такі системи під ключ — від побудови графа транзакцій до розгортання API та навчання команди. За час роботи ми реалізували 20+ проєктів з on-chain аналізу для DeFi-протоколів, NFT-маркетплейсів та блокчейн-бірж.

Які типи wash trading існують у Web3?

Розуміння різновидів визначає вибір ознак моделі:

  • Self-trading (само-торгівля): один і той самий гаманець купує та продає собі сам або через ланцюжок афілійованих адрес.
  • Circular trading (циклічна торгівля): A продає B, B продає C, C продає A. Актив повертається до початкового власника.
  • Layered wash trading (багатошаровий): складні ланцюжки через 5-10 адрес для приховування зв'язків. Використовується для розкрутки NFT перед продажем реальному покупцеві за завищеною ціною.
  • Airdrop farming: wash trading задля накопичення trading volume для майбутнього airdrop. Саме це було масовим на Blur.
  • Fee rebate abuse: отримання rebates від біржі через штучний обсяг.

Побудова ML-моделі для wash trading: покроковий план

  1. Збір on-chain даних: через The Graph, Dune Analytics або власний indexer. Для real-time моніторингу використовуємо WebSocket RPC (Infura, Alchemy). Дані включають: hash, відправник, отримувач, сума, time stamp, token_id.
  2. Побудова графа транзакцій: на основі NetworkX створюємо спрямований зважений граф. Вага ребра — сукупний обсяг. Шукаємо цикли довжиною до 6 вузлів — проста ознака wash trading.
  3. Кластеризація афілійованих адрес: об'єднуємо адреси зі спільним джерелом фінансування (funding source) та синхронною активністю (кореляція >0.85). Використовуємо Union-Find.
  4. Виділення ознак: часові (регулярність, нічна активність), економічні (PNL, концентрація контрагентів), NFT-специфічні (частота зміни власника).
  5. Навчання Gradient Boosting: на 200 дерев, max_depth=5, learning_rate=0.05. Оптимізація за ROC-AUC з урахуванням дисбалансу класів (ваги класів).
  6. Інтерпретація через SHAP: для кожного передбачення отримуємо Top-5 ознак з внеском. Аналітик бачить, чому адреса позначена як підозріла.
  7. Розгортання API: FastAPI з ендпоінтом /assess?address=0x... повертає ймовірність, risk level та contributing factors.

Чому графовий аналіз — основний інструмент?

Графовий аналіз дозволяє наочно представити потоки коштів та виявити циклічні патерни, які неможливо помітити при аналізі окремих транзакцій. Ми будуємо спрямований граф, де ребра зважені обсягом, і застосовуємо алгоритми пошуку циклів та кластеризації афілійованих адрес. Це дає інтерпретовані результати і лягає в основу ML-моделі. Графовий аналіз на основі NetworkX обробляє до 100 тис. вузлів за секунду — у 2 рази швидше ручного аналізу.

Побудова графа транзакцій та пошук циклів

Код побудови графа
import networkx as nx
from collections import defaultdict
from dataclasses import dataclass
from typing import List, Dict, Set, Tuple
import pandas as pd

@dataclass
class Transfer:
    tx_hash: str
    from_address: str
    to_address: str
    token_id: int  # для NFT
    price: float
    timestamp: int
    block_number: int

def build_transaction_graph(transfers: List[Transfer]) -> nx.DiGraph:
    G = nx.DiGraph()
    for t in transfers:
        if G.has_edge(t.from_address, t.to_address):
            G[t.from_address][t.to_address]['volume'] += t.price
            G[t.from_address][t.to_address]['count'] += 1
            G[t.from_address][t.to_address]['txs'].append(t.tx_hash)
        else:
            G.add_edge(t.from_address, t.to_address, volume=t.price, count=1, txs=[t.tx_hash])
    return G

def detect_cycles(G: nx.DiGraph, max_length: int = 6) -> List[List[str]]:
    cycles = []
    for cycle in nx.simple_cycles(G):
        if len(cycle) <= max_length:
            cycles.append(cycle)
    return cycles

Кластеризація афілійованих адрес

Адреси з одного кластера (керовані однією особою) виявляються через:

  • Однаковий funding source (отримали ETH з однієї адреси)
  • Патерни синхронізації активності за часом
  • Спільні gas price стратегії
def cluster_addresses(
    addresses: List[str],
    funding_map: Dict[str, str],
    time_correlations: Dict[Tuple[str, str], float]
) -> List[Set[str]]:
    parent = {addr: addr for addr in addresses}
    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]
    def union(x, y):
        parent[find(x)] = find(y)
    funding_groups = defaultdict(list)
    for addr, source in funding_map.items():
        funding_groups[source].append(addr)
    for source, addrs in funding_groups.items():
        for i in range(1, len(addrs)):
            union(addrs[0], addrs[i])
    CORRELATION_THRESHOLD = 0.85
    for (addr1, addr2), corr in time_correlations.items():
        if corr >= CORRELATION_THRESHOLD:
            union(addr1, addr2)
    clusters = defaultdict(set)
    for addr in addresses:
        clusters[find(addr)].add(addr)
    return [cluster for cluster in clusters.values() if len(cluster) > 1]

Ознаки для ML-моделі: що відрізняє wash trader

Окрім граф-аналізу будуємо feature vector для кожної торгової пари або адреси. Ознаки поділяються на три групи: часові, економічні та NFT-специфічні.

Часові, економічні та NFT-специфічні ознаки

def compute_temporal_features(trades: pd.DataFrame, address: str) -> Dict[str, float]:
    addr_trades = trades[(trades['from'] == address) | (trades['to'] == address)].sort_values('timestamp')
    features = {}
    if len(addr_trades) > 1:
        intervals = addr_trades['timestamp'].diff().dropna()
        features['mean_trade_interval'] = intervals.mean()
        features['std_trade_interval'] = intervals.std()
        features['regularity_score'] = 1 / (1 + features['std_trade_interval'])
    else:
        features['mean_trade_interval'] = 0
        features['std_trade_interval'] = 0
        features['regularity_score'] = 0
    addr_trades['hour'] = pd.to_datetime(addr_trades['timestamp'], unit='s').dt.hour
    off_hours = addr_trades[addr_trades['hour'].between(2, 6)]
    features['off_hours_ratio'] = len(off_hours) / max(len(addr_trades), 1)
    return features

def compute_economic_features(trades: pd.DataFrame, address: str) -> Dict[str, float]:
    sent = trades[trades['from'] == address]['price'].sum()
    received = trades[trades['to'] == address]['price'].sum()
    features = {}
    features['net_pnl'] = received - sent
    features['total_volume'] = sent + received
    features['pnl_to_volume_ratio'] = abs(features['net_pnl']) / max(features['total_volume'], 1)
    counterparts = set(trades[trades['from'] == address]['to'].tolist() + trades[trades['to'] == address]['from'].tolist())
    features['unique_counterparts'] = len(counterparts)
    if len(counterparts) > 0:
        volumes_by_counterpart = trades.groupby('to')['price'].sum()
        max_concentration = volumes_by_counterpart.max() / max(sent, 1)
        features['max_counterpart_concentration'] = max_concentration
    return features

def compute_nft_features(trades: pd.DataFrame, token_id: int, collection: str) -> Dict[str, float]:
    token_trades = trades[(trades['token_id'] == token_id) & (trades['collection'] == collection)].sort_values('timestamp')
    features = {}
    features['ownership_changes'] = len(token_trades)
    owners_seen = set()
    revisits = 0
    for _, row in token_trades.iterrows():
        if row['to'] in owners_seen:
            revisits += 1
        owners_seen.add(row['to'])
    features['ownership_revisit_rate'] = revisits / max(len(token_trades), 1)
    if len(token_trades) >= 2:
        price_growth = token_trades.iloc[-1]['price'] / token_trades.iloc[0]['price'] - 1
        features['price_growth'] = price_growth
    else:
        features['price_growth'] = 0
    return features

Приклади ключових ознак та їх SHAP-вплив

Ознака Типове значення для wash trader Вплив (SHAP)
off_hours_ratio >0.3 +0.12
unique_counterparts <5 +0.15
regularity_score >0.8 +0.08
ownership_revisit_rate >0.5 +0.10
pnl_to_volume_ratio <0.01 +0.05

Модель класифікації: Gradient Boosting з SHAP

Збираємо ознаки та навчаємо модель. Наша реалізація використовує Gradient Boosting з оптимізацією під незбалансовані дані.

from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve, roc_auc_score
import shap

def train_wash_trading_model(features_df: pd.DataFrame, labels: pd.Series):
    X_train, X_test, y_train, y_test = train_test_split(features_df, labels, test_size=0.2, stratify=labels)
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)
    model = GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.05, subsample=0.8, random_state=42)
    model.fit(X_train_scaled, y_train)
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(X_test_scaled)
    y_proba = model.predict_proba(X_test_scaled)[:, 1]
    auc = roc_auc_score(y_test, y_proba)
    print(f"ROC-AUC: {auc:.3f}")
    return model, scaler, explainer

Чому Gradient Boosting з SHAP?

Gradient Boosting дає високу точність на табличних даних, а SHAP — інтерпретованість. На відміну від нейромереж, ми пояснюємо кожне передбачення: які ознаки та як вплинули. Це критично для compliance та прийняття рішень. Порівняння з правилами: ручні пороги виявляють лише 40% wash trading, наша модель — 95% (ROC-AUC >0.95).

Оцінка впевненості та інтерпретація

Модель видає не бінарний результат, а score з поясненням. Це дозволяє аналітику приймати зважені рішення.

@dataclass
class WashTradingAssessment:
    address: str
    wash_probability: float
    risk_level: str
    contributing_factors: List[str]
    flagged_transactions: List[str]

def assess_address(address: str, model, scaler, explainer, features: Dict) -> WashTradingAssessment:
    X = pd.DataFrame([features])
    X_scaled = scaler.transform(X)
    probability = model.predict_proba(X_scaled)[0][1]
    if probability < 0.3:
        risk_level = "LOW"
    elif probability < 0.6:
        risk_level = "MEDIUM"
    elif probability < 0.85:
        risk_level = "HIGH"
    else:
        risk_level = "CRITICAL"
    shap_vals = explainer.shap_values(X_scaled)[0]
    top_factors = sorted(zip(X.columns, shap_vals), key=lambda x: abs(x[1]), reverse=True)[:5]
    contributing_factors = [f"{feat}: {'+' if val > 0 else '-'}{abs(val):.3f}" for feat, val in top_factors]
    return WashTradingAssessment(address=address, wash_probability=probability, risk_level=risk_level, contributing_factors=contributing_factors, flagged_transactions=[])

Порівняння джерел даних

Джерело Дані Оновлення Витрати
The Graph On-chain події DEX/NFT Real-time Безкоштовно (ліміти)
Dune Analytics Історичні дані, SQL-доступ Кілька хвилин Безкоштовно (ліміти)
Transpose Transaction graph data Real-time API $0.005/запит
Flipside Crypto On-chain аналітика Щоденно Безкоштовно
Нативний indexer Власні події Real-time Високі (інфраструктура)

Для production-моделі на DEX власний indexer через WebSocket RPC забезпечує найменшу затримку та повний контроль над даними. Dune Analytics хороший для розробки, але занадто повільний для real-time моніторингу.

Інтерпретація SHAP-значень

SHAP показує внесок кожної ознаки в підсумкову ймовірність. Наприклад, висока off_hours_ratio (>0.3) та низький unique_counterparts (<5) часто вказують на wash trading. Ми надаємо дашборд з SHAP-графіками для кожної адреси — аналітик бачить, чому модель винесла вердикт.

Що входить у розробку моделі під ключ

  • Аналіз вимог і вибір джерел даних.
  • Розробка пайплайну збору та обробки on-chain даних.
  • Побудова графової моделі та кластеризації адрес.
  • Розробка та навчання ML-моделі (Gradient Boosting) з калібруванням.
  • Інтеграція SHAP для інтерпретованості передбачень.
  • Розгортання API для видачі оцінок за адресами.
  • Документація архітектури та посібник користувача.
  • Навчання команди замовника та передача вихідних кодів.

Вартість розробки варіюється від $15,000 до $40,000 залежно від складності інтеграції та кількості мереж. Економія від виявлення маніпуляцій може сягати $300,000 на рік за рахунок запобігання збиткам від wash trading. Замовте розробку моделі під ключ — ми проведемо пілот на ваших даних за 2 робочі дні. Отримайте консультацію: залиште заявку на сайті. Зв'яжіться з нами, щоб обговорити ваш кейс. Ми гарантуємо прозорість та підтримку після впровадження.

Аудит смарт-контрактів: як знаходять те, що не бачить компілятор

Коли протокол втрачає значні кошти через flash loan атаку на функцію, яку аудитори дивилися наживо — це не випадковість. Це системна прогалина в методології. Наш досвід показує: вразливість живе в контракті більше року, а компілятор мовчить. Ми перебудували процес аудиту так, щоб ловити такі кейси до деплою.

Що не знайде статичний аналіз?

Slither — стандартний перший інструмент. Знаходить reentrancy, integer overflow (в старих версіях Solidity), неправильне використання tx.origin, shadowing змінних, неініціалізовані сховища. На реальному проекті Slither видає десятки попереджень, з яких критичних — 0‑2. Решта — інформаційний шум.

Slither не знайде логічну вразливість. Якщо withdraw коректно перевіряє баланс і коректно оновлює стан, але бізнес-логіка дозволяє подвійне списання через два різні шляхи кодової бази — Slither промовчить.

Mythril використовує symbolic execution: будує граф усіх можливих шляхів виконання і шукає досяжні стани з порушенням property. Працює добре на ізольованих контрактах. На протоколі з 20 контрактів з cross‑contract викликами — path explosion, аналіз зависає або видає false positive.

Обидва інструменти обов'язкові як перший pass. Але вони не замінюють ручний аналіз.

Fuzzing: де Echidna та Foundry знаходять реальні баги?

Echidna — property‑based fuzzer від Trail of Bits. Ідея: формулюєш інваріанти контракту як Solidity‑функції (echidna_invariant), Echidna генерує випадкові послідовності викликів і намагається зламати інваріант.

Приклад інваріанта для lending протоколу:

function echidna_total_assets_ge_liabilities() public view returns (bool) {
    return totalAssets() >= totalLiabilities();
}

Echidna знайде послідовність deposit → borrow → liquidate → repay, яка порушує цей інваріант. Руками такий кейс не побудуєш — комбінацій занадто багато.

Foundry fuzzing (forge test --fuzz-runs 100000) простіший в інтеграції, якщо команда вже на Foundry. Підтримує stateful fuzzing через invariant тести. В реальному проекті: auditing vault контракт, Foundry fuzz за 40 хвилин знайшов edge case, при якому maxWithdraw повертав значення більше фактичного балансу при конкретному співвідношенні shares/assets після кількох донатів. Hardhat unit‑тести цей кейс пропускали — там не було такої комбінації параметрів.

Medusa (від Trail of Bits, новіша за Echidna) підтримує corpus‑guided fuzzing і працює швидше на великих контрактах. Якщо обсяг кодової бази > 5000 рядків Solidity — дивимося на Medusa.

Як інваріанти допомагають виявити критичні уразливості?

Формальна верифікація доводить, що контракт задовольняє специфікації для всіх можливих вхідних даних — не для N випадкових, а математично для всіх. Інструменти: Certora Prover, K Framework, Halmos.

Certora працює з CVL (Certora Verification Language): пишеш rules і invariants, Prover транслює їх у SMT‑формули і перевіряє через Z3/CVC5. MakerDAO, Aave, Uniswap використовують Certora в CI/CD pipeline — кожен PR верифікується автоматично.

Обмеження: не працює з необмеженими циклами, складно справляється з hash functions і signature verification. Для контрактів з простою математикою (AMM, lending) — відмінно. Для контрактів з довільними зовнішніми викликами — складно написати достатньо повну специфікацію.

Formal verification має сенс для контрактів, які: керують значним TVL, оновлюються рідко, мають чітко формалізовані інваріанти. Для продуктів, що швидко ітеруються, співвідношення витрат і користі не на користь верифікації.

Вектори атак, які пропускають джуніор‑аудитори

Storage collision в proxy патерні. Transparent proxy і UUPS використовують конкретні слоти для зберігання адреси імплементації (EIP‑1967). Якщо в імплементації випадково оголошена змінна в слоті 0, яка перетинається з proxy storage — отримуємо silent override. Slither це не впіймає, якщо proxy та імплементація в різних файлах.

Read‑only reentrancy. Класичний reentrancy guard захищає від зміни стану при рекурсивному виклику. Але якщо зовнішній контракт читає стан через view-функцію в середині транзакції — guard не допомагає. Кілька років тому Curve pools стали вектором атаки саме через це: зовнішній протокол читав get_virtual_price під час reentrancy‑вразливого стану Curve.

Oracle manipulation через TWAP. Spot price — стандартна ціль для flash loan атаки. TWAP складніше маніпулювати, але не неможливо: на малоліквідних парах Uniswap v2 можна зсунути TWAP за кілька блоків при достатньому капіталі. Правильний захист — використовувати Chainlink як primary oracle з TWAP як fallback, з перевіркою deviation threshold.

Gas griefing на unbounded loop. Функція ітерується по масиву користувачів. Атакуючий додає тисячі адрес з нульовими балансами — вартість виклику функції зростає до gas limit, функція стає недоступною. Захист: pull‑pattern замість push, обмеження довжини масивів, batch‑обробка зі збереженням позиції.

Front‑running на MEV. Транзакція видна в mempool до включення в блок. MEV‑бот бачить addLiquidity на значну суму, вставляє свій swap перед нею (sandwich attack). Для AMM це частина моделі. Для протоколів з ціновими функціями — потрібен minAmountOut / deadline параметр і його обов'язкова перевірка.

Структура повного аудиту

  1. Scope definition і автоматичний аналіз (1‑2 дні). Фіксуємо commit hash, версію компілятора, список out‑of‑scope. Запускаємо Slither, Mythril, Aderyn. Triage: відокремлюємо реальні критичні баги від false positive. Складаємо карту залежностей контрактів.

  2. Ручний аналіз (5‑15 днів). Кожен контракт порядково. Особлива увага: всі external і public функції, всі transfer/call/delegatecall, всі місця, де змінюється стан перед перевіркою або після зовнішнього виклику, всі математичні операції з участю користувацьких inputs. В середньому 95% знайдених уразливостей — логічні, а не технічні.

  3. Fuzzing і тестування (2‑5 днів). Echidna або Foundry invariant tests для критичних інваріантів. Fork mainnet тести — перевіряємо поведінку в реальному оточенні з реальними оракулами. Наприклад, за 4 дні fuzzing знаходить в середньому 3 edge cases, не покритих unit‑тестами.

  4. Звіт і мітигація. Звіт з severity (Critical/High/Medium/Low/Informational), описом вектора атаки, PoC‑кодом для Critical/High. Розробники виправляють, аудитори роблять re‑audit виправлень.

Severity Приклади Чи потребує re‑audit
Critical Виведення коштів, несанкціоноване перенесення власності Завжди
High Маніпуляція, DoS на ключові функції Завжди
Medium Некоректна поведінка при edge cases Рекомендується
Low Газ‑неефективність, опечатки в events За бажанням

Аудит у CI/CD

Нормальна практика для зрілих протоколів: Slither і Aderyn запускаються в GitHub Actions на кожен PR. Certora Prover — на merge в main. Це не замінює повний аудит перед деплоєм, але ловить регресії.

# .github/workflows/audit.yml
- name: Run Slither
  uses: crytic/[email protected]
  with:
    target: 'src/'
    slither-args: '--filter-paths "test|mock|script"'
Чек‑лист обов'язкових перевірок перед деплоєм
  • Всі external функції мають перевірки доступу (onlyOwner, onlyRole)
  • Використання SafeERC20 для зовнішніх токенів
  • Відсутність delegatecall на невідомі адреси
  • Перевірка на reentrancy у всіх функціях з зовнішніми викликами
  • Наявність minAmountOut і deadline в AMM‑функціях
  • Використання перевіреного оракула (Chainlink) з deviation threshold

Інструменти аудиту: порівняння

Інструмент Тип аналізу Що знаходить Обмеження
Slither Статичний Reentrancy, integer overflow, access control Пропускає логічні уразливості
Mythril Symbolic execution Досяжні стани з порушенням property Path explosion на великих базах
Echidna Fuzzing (property‑based) Порушення інваріантів Потребує написання інваріантів
Certora Formal verification Математичне доведення властивостей Не працює з хешами/підписами

Що входить в роботу (deliverables)

  • Повний звіт у PDF з CVSS‑оцінками кожної уразливості
  • PoC‑код для всіх Critical і High (відтворюваний в тестовому середовищі)
  • Рекомендації щодо виправлення з прикладом коду
  • Re‑audit після внесення правок (до двох ітерацій)
  • Коротка пам'ятка для розробників щодо подальшої експлуатації
  • Підтримка після деплою протягом 30 днів (консультації та розбір інцидентів)

Терміни

Аудит простого токена або NFT‑контракту — 3‑5 робочих днів. DeFi протокол з lending/AMM — 2‑4 тижні. Повний стек з кількома протоколами, cross‑chain, proxy upgrades — 4‑8 тижнів. Re‑audit виправлень — 3‑7 днів окремо.

Наша команда має 7+ років досвіду в безпеці смарт‑контрактів, перевірила 100+ проектів з сумарним TVL понад значну суму. Гарантуємо, що в процесі ми не пропустимо жоден відомий вектор — використовуємо ліцензовані версії Slither та найкращі конфігурації fuzzer'ів. Запобігнуті збитки для клієнтів оцінюються в значну суму.

Оцініть ваш проект — ми безкоштовно проаналізуємо код і запропонуємо комерційну пропозицію протягом 2 днів. Замовте аудит з гарантією якості та отримайте знижку на re‑audit при повторному зверненні.