merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
277
app/bundle_detector.py
Normal file
277
app/bundle_detector.py
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
"""
|
||||
Bundle Detection Engine — Atomic block co-occurrence analysis.
|
||||
Detects Jito bundles, Flashbots bundles, and coordinated launches.
|
||||
Implements: atomic-block grouping, common funder, temporal clustering,
|
||||
distribution anomaly detection, holder concentration scoring.
|
||||
|
||||
References:
|
||||
- Section 2.2, Bundle Detection Heuristics
|
||||
- HNUT: 78% early activity was bundled transactions
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleDetection:
|
||||
"""Result of bundle detection on a token."""
|
||||
|
||||
token_address: str
|
||||
chain: str
|
||||
is_bundled: bool
|
||||
confidence: float # 0-1
|
||||
|
||||
# Individual signals
|
||||
atomic_block_score: float = 0.0
|
||||
common_funder_score: float = 0.0
|
||||
temporal_score: float = 0.0
|
||||
distribution_anomaly_score: float = 0.0
|
||||
concentration_score: float = 0.0
|
||||
|
||||
# Details
|
||||
earliest_block: int | None = None
|
||||
wallets_in_earliest_block: int = 0
|
||||
common_funder_address: str | None = None
|
||||
funded_wallets_count: int = 0
|
||||
time_window_seconds: float = 0
|
||||
identical_amount_count: int = 0
|
||||
round_amount_count: int = 0
|
||||
top10_holder_pct: float = 0.0
|
||||
top3_holder_pct: float = 0.0
|
||||
holder_count: int = 0
|
||||
|
||||
risk_label: str = "unknown" # critical, high, medium, low
|
||||
|
||||
|
||||
class BundleDetector:
|
||||
"""Multi-signal bundle detection for Solana tokens."""
|
||||
|
||||
def __init__(self):
|
||||
self.min_holders_for_analysis = 5
|
||||
|
||||
async def detect(
|
||||
self,
|
||||
token_address: str,
|
||||
chain: str = "solana",
|
||||
holders: list[dict] | None = None,
|
||||
transactions: list[dict] | None = None,
|
||||
) -> BundleDetection:
|
||||
"""Run all bundle detection signals and return combined result."""
|
||||
result = BundleDetection(token_address=token_address, chain=chain, is_bundled=False, confidence=0.0)
|
||||
|
||||
if not holders or len(holders) < self.min_holders_for_analysis:
|
||||
result.risk_label = "unknown"
|
||||
return result
|
||||
|
||||
result.holder_count = len(holders)
|
||||
|
||||
# 1. Atomic block co-occurrence
|
||||
if transactions:
|
||||
self._atomic_block_signal(result, transactions)
|
||||
|
||||
# 2. Common funding source
|
||||
if transactions:
|
||||
self._common_funder_signal(result, transactions)
|
||||
|
||||
# 3. Temporal clustering
|
||||
if transactions:
|
||||
self._temporal_signal(result, transactions)
|
||||
|
||||
# 4. Distribution anomaly detection
|
||||
self._distribution_anomaly_signal(result, holders)
|
||||
|
||||
# 5. Holder concentration
|
||||
self._concentration_signal(result, holders)
|
||||
|
||||
# Combine signals into final score
|
||||
result.confidence = self._combined_score(result)
|
||||
result.is_bundled = result.confidence >= 0.5
|
||||
result.risk_label = self._risk_label(result.confidence)
|
||||
|
||||
return result
|
||||
|
||||
def _atomic_block_signal(self, result: BundleDetection, txs: list[dict]):
|
||||
"""Check if many holders acquired tokens in the same block (atomic bundle)."""
|
||||
block_wallets = defaultdict(set)
|
||||
for tx in txs:
|
||||
block = tx.get("blockNumber") or tx.get("slot")
|
||||
wallet = tx.get("from") or tx.get("signer") or tx.get("wallet")
|
||||
if block and wallet:
|
||||
block_wallets[block].add(wallet)
|
||||
|
||||
if not block_wallets:
|
||||
return
|
||||
|
||||
# Find block with most wallet activity
|
||||
max_block = max(block_wallets, key=lambda b: len(block_wallets[b]))
|
||||
max_wallets = len(block_wallets[max_block])
|
||||
|
||||
result.earliest_block = max_block
|
||||
result.wallets_in_earliest_block = max_wallets
|
||||
|
||||
if max_wallets >= 10:
|
||||
result.atomic_block_score = 0.9
|
||||
elif max_wallets >= 5:
|
||||
result.atomic_block_score = 0.7
|
||||
elif max_wallets >= 3:
|
||||
result.atomic_block_score = 0.4
|
||||
else:
|
||||
result.atomic_block_score = 0.1
|
||||
|
||||
def _common_funder_signal(self, result: BundleDetection, txs: list[dict]):
|
||||
"""Detect if multiple buyers were funded from the same source wallet."""
|
||||
funder_counts = defaultdict(int)
|
||||
for tx in txs:
|
||||
funder = tx.get("from") or tx.get("signer")
|
||||
recipient = tx.get("to") or tx.get("recipient")
|
||||
if funder and recipient and funder != recipient:
|
||||
funder_counts[funder] += 1
|
||||
|
||||
if not funder_counts:
|
||||
return
|
||||
|
||||
top_funder = max(funder_counts, key=funder_counts.get)
|
||||
top_count = funder_counts[top_funder]
|
||||
|
||||
result.common_funder_address = top_funder
|
||||
result.funded_wallets_count = top_count
|
||||
|
||||
if top_count >= 20:
|
||||
result.common_funder_score = 0.9
|
||||
elif top_count >= 10:
|
||||
result.common_funder_score = 0.7
|
||||
elif top_count >= 5:
|
||||
result.common_funder_score = 0.5
|
||||
elif top_count >= 3:
|
||||
result.common_funder_score = 0.3
|
||||
|
||||
def _temporal_signal(self, result: BundleDetection, txs: list[dict]):
|
||||
"""Check if wallets appeared within a narrow time window."""
|
||||
timestamps = []
|
||||
for tx in txs:
|
||||
ts = tx.get("timestamp") or tx.get("blockTime")
|
||||
if ts:
|
||||
timestamps.append(ts)
|
||||
|
||||
if len(timestamps) < 2:
|
||||
return
|
||||
|
||||
timestamps.sort()
|
||||
window = timestamps[-1] - timestamps[0]
|
||||
result.time_window_seconds = window
|
||||
|
||||
if window <= 60: # All within 1 minute
|
||||
result.temporal_score = 0.9
|
||||
elif window <= 300: # 5 minutes
|
||||
result.temporal_score = 0.7
|
||||
elif window <= 900: # 15 minutes
|
||||
result.temporal_score = 0.5
|
||||
elif window <= 3600: # 1 hour
|
||||
result.temporal_score = 0.3
|
||||
|
||||
def _distribution_anomaly_signal(self, result: BundleDetection, holders: list[dict]):
|
||||
"""Check for flat/rounded amounts — hallmark of bundled distribution."""
|
||||
amounts = []
|
||||
for h in holders:
|
||||
amt = h.get("amount", 0)
|
||||
if isinstance(amt, (int, float)) and amt > 0:
|
||||
amounts.append(amt)
|
||||
|
||||
if not amounts:
|
||||
return
|
||||
|
||||
# Identical amounts
|
||||
from collections import Counter
|
||||
|
||||
amount_counts = Counter(amounts)
|
||||
identical = sum(1 for count in amount_counts.values() if count >= 3)
|
||||
result.identical_amount_count = identical
|
||||
|
||||
# Round number amounts (multiples of 100, 1000, 10000)
|
||||
round_count = sum(1 for a in amounts if a >= 100 and (a % 100 == 0 or a % 1000 == 0 or a % 10000 == 0))
|
||||
result.round_amount_count = round_count
|
||||
|
||||
identical_pct = identical / len(amounts) if amounts else 0
|
||||
round_pct = round_count / len(amounts) if amounts else 0
|
||||
|
||||
# Combine
|
||||
anomaly_pct = max(identical_pct, round_pct)
|
||||
if anomaly_pct > 0.5:
|
||||
result.distribution_anomaly_score = 0.9
|
||||
elif anomaly_pct > 0.3:
|
||||
result.distribution_anomaly_score = 0.7
|
||||
elif anomaly_pct > 0.15:
|
||||
result.distribution_anomaly_score = 0.5
|
||||
elif anomaly_pct > 0.05:
|
||||
result.distribution_anomaly_score = 0.3
|
||||
|
||||
def _concentration_signal(self, result: BundleDetection, holders: list[dict]):
|
||||
"""Check top-10 and top-3 holder concentration."""
|
||||
amounts = []
|
||||
for h in holders:
|
||||
amt = h.get("amount", 0)
|
||||
if isinstance(amt, (int, float)) and amt > 0:
|
||||
amounts.append(amt)
|
||||
|
||||
if not amounts:
|
||||
return
|
||||
|
||||
amounts.sort(reverse=True)
|
||||
total = sum(amounts)
|
||||
|
||||
top3 = sum(amounts[:3]) / total if total > 0 else 0
|
||||
top10 = sum(amounts[: min(10, len(amounts))]) / total if total > 0 else 0
|
||||
|
||||
result.top3_holder_pct = round(top3 * 100, 1)
|
||||
result.top10_holder_pct = round(top10 * 100, 1)
|
||||
|
||||
if top3 > 0.5 or top10 > 0.8:
|
||||
result.concentration_score = 0.9
|
||||
elif top3 > 0.3 or top10 > 0.6:
|
||||
result.concentration_score = 0.7
|
||||
elif top3 > 0.15 or top10 > 0.4:
|
||||
result.concentration_score = 0.4
|
||||
elif top3 > 0.05:
|
||||
result.concentration_score = 0.2
|
||||
|
||||
def _combined_score(self, r: BundleDetection) -> float:
|
||||
"""Weighted combination of all signals."""
|
||||
scores = [
|
||||
(r.atomic_block_score, 0.30), # Atomic block is strongest signal
|
||||
(r.common_funder_score, 0.25), # Common funder second strongest
|
||||
(r.temporal_score, 0.15),
|
||||
(r.distribution_anomaly_score, 0.20),
|
||||
(r.concentration_score, 0.10),
|
||||
]
|
||||
weighted = sum(s * w for s, w in scores)
|
||||
# Boost if multiple strong signals
|
||||
strong_signals = sum(1 for s, _ in scores if s >= 0.7)
|
||||
if strong_signals >= 3:
|
||||
weighted = min(1.0, weighted * 1.3)
|
||||
elif strong_signals >= 2:
|
||||
weighted = min(1.0, weighted * 1.15)
|
||||
return round(weighted, 4)
|
||||
|
||||
def _risk_label(self, confidence: float) -> str:
|
||||
if confidence >= 0.8:
|
||||
return "critical"
|
||||
elif confidence >= 0.6:
|
||||
return "high"
|
||||
elif confidence >= 0.4:
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
|
||||
# Singleton
|
||||
_detector: BundleDetector | None = None
|
||||
|
||||
|
||||
def get_bundle_detector() -> BundleDetector:
|
||||
global _detector
|
||||
if _detector is None:
|
||||
_detector = BundleDetector()
|
||||
return _detector
|
||||
Loading…
Add table
Add a link
Reference in a new issue