ML Model for Wash Trading Detection on Blockchain

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1All 1305 services
ML Model for Wash Trading Detection on Blockchain
Complex
from 2 weeks to 3 months
Frequently Asked Questions

Blockchain Development Services

Blockchain Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1349
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1247
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    949
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1183
  • image_logo-advance_0.webp
    B2B Advance company logo design
    642
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    921

ML Model for Wash Trading Detection on Blockchain

Chainalysis reports that on several NFT marketplaces, the share of fake volume exceeds 50%, and on some up to 80%. Wash trading distorts market data, misleads investors, and attracts regulatory attention. Traditional threshold methods (e.g., detecting repeated trades between the same addresses) miss up to 60% of manipulations. A model based on blockchain graph analysis and Gradient Boosting with SHAP interpretation raises accuracy to 95% (ROC-AUC >0.95). We develop such systems turnkey—from building the transaction graph to deploying the API and training your team. Over the years, we have delivered 20+ on-chain analysis projects for DeFi protocols, NFT marketplaces, and blockchain exchanges.

What Types of Wash Trading Exist in Web3?

Understanding the varieties determines the choice of model features:

  • Self-trading: the same wallet buys and sells to itself, or through a chain of affiliated addresses.
  • Circular trading: A sells to B, B sells to C, C sells to A. The asset returns to the original owner.
  • Layered wash trading: complex chains through 5–10 addresses to conceal links. Used to pump NFTs before selling to real buyers at inflated prices.
  • Airdrop farming: wash trading to accumulate trading volume for a future airdrop. This was widespread on Blur.
  • Fee rebate abuse: obtaining rebates from the exchange through artificial volume.

Building an ML Model for Wash Trading: Step-by-Step Plan

  1. Collect on-chain data: via The Graph, Dune Analytics, or a custom indexer. For real-time monitoring, we use WebSocket RPC (Infura, Alchemy). Data includes: hash, sender, recipient, amount, timestamp, token_id.
  2. Build the transaction graph: using NetworkX, create a directed weighted graph. Edge weight is the cumulative volume. Search for cycles of length up to 6 nodes—a simple sign of wash trading.
  3. Cluster affiliated addresses: group addresses with a common funding source and synchronous activity (correlation >0.85). Use Union-Find.
  4. Extract features: temporal (regularity, night activity), economic (PNL, counterparty concentration), NFT-specific (ownership change frequency).
  5. Train Gradient Boosting: 200 trees, max_depth=5, learning_rate=0.05. Optimize for ROC-AUC with class imbalance (class weights).
  6. Interpret with SHAP: for each prediction, get the top-5 features and their contributions. The analyst sees why an address is flagged as suspicious.
  7. Deploy API: FastAPI endpoint /assess?address=0x... returns probability, risk level, and contributing factors.

Why Graph Analysis Is the Primary Tool?

Graph analysis allows visualizing fund flows and detecting cyclic patterns invisible when analyzing individual transactions. We build a directed graph where edges are weighted by volume and apply cycle detection and address clustering algorithms. This yields interpretable results and feeds into the ML model. Graph analysis with NetworkX processes up to 100k nodes per second—twice as fast as manual analysis.

Building the Transaction Graph and Cycle Detection

Graph construction code
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  # for 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

Clustering Affiliated Addresses

Addresses belonging to the same cluster (controlled by one entity) are identified through:

  • Same funding source (received ETH from one address)
  • Time-synchronized activity patterns
  • Common gas price strategies
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]

Features for the ML Model: What Distinguishes a Wash Trader

In addition to graph analysis, we build a feature vector for each trading pair or address. Features fall into three groups: temporal, economic, and NFT-specific.

Temporal, Economic, and NFT-Specific Features

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

Example Key Features and Their SHAP Influence

Feature Typical Value for Wash Trader Impact (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

Classification Model: Gradient Boosting with SHAP

We aggregate features and train the model. Our implementation uses Gradient Boosting optimized for imbalanced data.

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

Why Gradient Boosting with SHAP?

Gradient Boosting delivers high accuracy on tabular data, while SHAP provides interpretability. Unlike neural networks, we explain each prediction: which features and how they contributed. This is critical for compliance and decision-making. Comparison with rules: manual thresholds detect only 40% of wash trading; our model achieves 95% (ROC-AUC >0.95).

Confidence Assessment and Interpretation

The model does not output a binary result, but a score with explanation. This allows the analyst to make informed decisions.

@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=[])

Data Source Comparison

Source Data Freshness Cost
The Graph On-chain DEX/NFT events Real-time Free (limits)
Dune Analytics Historical data, SQL access Several minutes Free (limits)
Transpose Transaction graph data Real-time API $0.005/request
Flipside Crypto On-chain analytics Daily Free
Native indexer Custom events Real-time High (infrastructure)

For a production model on DEX, a custom indexer via WebSocket RPC provides the lowest latency and full data control. Dune Analytics is good for development but too slow for real-time monitoring.

Interpretation of SHAP Values

SHAP shows the contribution of each feature to the final probability. For example, high off_hours_ratio (>0.3) and low unique_counterparts (<5) often indicate wash trading. We provide a dashboard with SHAP graphs for each address—the analyst sees why the model made its verdict.

What's Included in Turnkey Model Development

  • Requirements analysis and data source selection.
  • Development of on-chain data collection and processing pipeline.
  • Construction of graph model and address clustering.
  • Development and training of ML model (Gradient Boosting) with calibration.
  • Integration of SHAP for prediction interpretability.
  • Deployment of API for address assessment.
  • Architecture documentation and user guide.
  • Team training and source code handover.

The cost of development varies depending on integration complexity and number of networks. The savings from detecting manipulations can reach $300,000 per year by preventing losses from wash trading. Order a turnkey model development—we'll run a pilot on your data in 2 business days. Get a consultation: leave a request on the website. Contact us to discuss your case. We guarantee transparency and post-deployment support.

How Do We Find What the Compiler Misses?

When a protocol loses $197M through a flash loan attack on a function that auditors reviewed live — it's not an accident. It's a systemic gap in methodology. Our experience shows: vulnerabilities live in a contract for over a year, while the compiler remains silent. We restructured the audit process to catch such cases before deployment.

What Static Analysis Won't Find?

Slither is the standard first tool. It finds reentrancy, integer overflow (in older Solidity versions), improper use of tx.origin, variable shadowing, uninitialized storage. On a real project, Slither produces dozens of warnings, of which critical ones are 0‑2. The rest is informational noise.

Slither won't find logical vulnerabilities. If withdraw correctly checks balance and correctly updates state, but business logic allows double deduction through two different code paths — Slither stays silent.

Mythril uses symbolic execution: builds a graph of all possible execution paths and searches for reachable states violating properties. Works well on isolated contracts. On a protocol of 20 contracts with cross‑contract calls — path explosion, analysis hangs or returns false positives.

Both tools are mandatory as a first pass. But they don't replace manual analysis.

Fuzzing: Where Echidna and Foundry Find Real Bugs

Echidna is a property‑based fuzzer from Trail of Bits. The idea: formulate contract invariants as Solidity functions (echidna_invariant), Echidna generates random call sequences and tries to break the invariant.

Example invariant for a lending protocol:

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

Echidna will find a sequence deposit → borrow → liquidate → repay that violates this invariant. You can't build such a case manually — too many combinations.

Foundry fuzzing (forge test --fuzz-runs 100000) is easier to integrate if the team is already on Foundry. Supports stateful fuzzing via invariant tests. In a real project: auditing a vault contract, Foundry fuzzed for 40 minutes and found an edge case where maxWithdraw returned a value larger than actual balance at a specific shares/assets ratio after several donations. Hardhat unit tests missed it — they didn't have that combination of parameters.

Medusa (from Trail of Bits, newer than Echidna) supports corpus‑guided fuzzing and runs faster on large contracts. If the codebase exceeds 5000 lines of Solidity — we look at Medusa.

How Invariants Help Identify Critical Vulnerabilities

Formal verification proves that the contract satisfies specifications for all possible inputs — not for N random ones, but mathematically for all. Tools: Certora Prover, K Framework, Halmos.

Certora works with CVL (Certora Verification Language): write rules and invariants, the Prover translates them into SMT formulas and checks via Z3/CVC5. MakerDAO, Aave, Uniswap use Certora in CI/CD pipeline — every PR is automatically verified.

Limitations: doesn't work with unbounded loops, struggles with hash functions and signature verification. For contracts with simple math (AMM, lending) — excellent. For contracts with arbitrary external calls — difficult to write sufficiently complete specifications.

Formal verification makes sense for contracts that: manage over $50M, are rarely updated, have clearly formalizable invariants. For fast‑iterating products — the cost‑benefit ratio doesn't favor verification.

What Attack Vectors Do Junior Auditors Miss?

Storage collision in proxy pattern. Transparent proxy and UUPS use specific slots for implementation address (EIP‑1967). If an implementation accidentally declares a variable in slot 0 that overlaps with proxy storage — we get silent override. Slither won't catch this if proxy and implementation are in different files.

Read‑only reentrancy. Classic reentrancy guard protects against state changes during recursive calls. But if an external contract reads state via a view function mid‑transaction — guard doesn't help. Years ago, Curve pools became an attack vector precisely through this: an external protocol read get_virtual_price during a reentrancy‑vulnerable state of Curve.

Oracle manipulation via TWAP. Spot price is a standard target for flash loan attack. TWAP is harder to manipulate, but not impossible: on low‑liquidity Uniswap v2 pairs, TWAP can be shifted over several blocks with enough capital. Proper protection: use Chainlink as primary oracle with TWAP as fallback, with deviation threshold check.

Gas griefing on unbounded loop. A function iterates over an array of users. Attacker adds thousands of addresses with zero balances — the function's gas cost rises to the gas limit, making it inaccessible. Protection: pull pattern instead of push, limit array lengths, batch processing with position tracking.

Front‑running on MEV. Transaction is visible in mempool before inclusion in block. MEV bot sees addLiquidity for a significant amount, inserts its own swap before it (sandwich attack). For AMM this is part of the model. For protocols with price functions — require minAmountOut / deadline parameter and its mandatory verification.

Structure of a Full Audit

  1. Scope definition and automated analysis (1‑2 days). Fix commit hash, compiler version, list of out‑of‑scope items. Run Slither, Mythril, Aderyn. Triage: separate real critical bugs from false positives. Build contract dependency map.

  2. Manual analysis (5‑15 days). Each contract line by line. Special attention: all external and public functions, all transfer/call/delegatecall, all places where state changes before a check or after an external call, all math operations with user inputs. On average, 95% of found vulnerabilities are logical, not technical.

  3. Fuzzing and testing (2‑5 days). Echidna or Foundry invariant tests for critical invariants. Fork mainnet tests — verify behavior in real environment with real oracles. For example, in 4 days fuzzing finds on average 3 edge cases not covered by unit tests.

  4. Report and mitigation. Report with severity (Critical/High/Medium/Low/Informational), attack vector description, PoC code for Critical/High. Developers fix, auditors perform re‑audit of fixes.

Severity Examples Requires re‑audit?
Critical Drain funds, unauthorized ownership transfer Always
High Manipulation, DoS on key functions Always
Medium Incorrect behavior on edge cases Recommended
Low Gas inefficiency, typos in events Optional

Audit in CI/CD

Common practice for mature protocols: Slither and Aderyn run in GitHub Actions on every PR. Certora Prover — on merge to main. This doesn't replace a full audit before deployment, but catches regressions.

# .github/workflows/audit.yml
- name: Run Slither
  uses: crytic/[email protected]
  with:
    target: 'src/'
    slither-args: '--filter-paths "test|mock|script"'
Checklist of mandatory checks before deployment
  • All external functions have access controls (onlyOwner, onlyRole)
  • Use SafeERC20 for external tokens
  • No delegatecall to unknown addresses
  • Reentrancy check in all functions with external calls
  • Presence of minAmountOut and deadline in AMM functions
  • Use of a trusted oracle (Chainlink) with deviation threshold

Audit Tools Comparison

Tool Type of Analysis What It Finds Limitations
Slither Static Reentrancy, integer overflow, access control Misses logical vulnerabilities
Mythril Symbolic execution Reachable states violating properties Path explosion on large codebases
Echidna Fuzzing (property‑based) Invariant violations Requires writing invariants
Certora Formal verification Mathematical proof of properties Doesn't work with hashes/signatures

Deliverables

  • Full report in PDF with CVSS scores for each vulnerability
  • PoC code for all Critical and High (reproducible in test environment)
  • Remediation recommendations with code examples
  • Re‑audit after fixes (up to two iterations)
  • Brief guide for developers on ongoing operation
  • Post‑deployment support for 30 days (consultations and incident analysis)

Timeline

Audit of a simple token or NFT contract — 3‑5 business days. DeFi protocol with lending/AMM — 2‑4 weeks. Full stack with multiple protocols, cross‑chain, proxy upgrades — 4‑8 weeks. Re‑audit of fixes — 3‑7 days separately.

Our team has 7+ years of experience in smart contract security, having audited over 100 projects. We guarantee we won't miss any known attack vectors — we use licensed versions of Slither and best fuzzer configurations. Assess your project — we will analyze your code for free and provide a commercial offer within 2 days. Order an audit with quality guarantee and get a discount on re‑audit for repeat customers.