#!/usr/bin/env bash # promote-attacks.sh — Promote red-team attack vectors to onchain/script/backtesting/attacks/ via PR. # # After a red-team run that extracted ETH, this script: # 1. Reads the discovered attack JSONL # 2. Deduplicates against existing files (by op-type fingerprint) # 3. Classifies the attack type from the op sequence # 4. Creates a git branch, commits the file, pushes, and opens a Codeberg PR # # Usage: # promote-attacks.sh [OPTIONS] # # Options: # --attacks FILE Path to attack JSONL (default: /tmp/red-team-attacks.jsonl) # --candidate NAME Optimizer candidate name (default: $CANDIDATE_NAME or "unknown") # --profile PROFILE Optimizer profile string (default: $OPTIMIZER_PROFILE or "unknown") # --eth-extracted DELTA ETH extracted in wei (default: 0) # --eth-before AMOUNT LM ETH before attack in wei (default: 0) # # Env (all optional): # CODEBERG_TOKEN Codeberg API token. If absent, ~/.netrc is tried. # If neither is present, PR creation is skipped with exit 0. # CANDIDATE_NAME Fallback for --candidate # OPTIMIZER_PROFILE Fallback for --profile # # Exit codes: # 0 PR created, or gracefully skipped (no novel attacks, no token, etc.) # 1 Hard failure (git or API error) set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" ATTACKS_DIR="$REPO_ROOT/onchain/script/backtesting/attacks" CODEBERG_REPO="johba/harb" CODEBERG_API="https://codeberg.org/api/v1" log() { echo "[promote-attacks] $*"; } warn() { echo "[promote-attacks] WARNING: $*" >&2; } die() { echo "[promote-attacks] ERROR: $*" >&2; exit 1; } # ── Parse arguments ────────────────────────────────────────────────────────── ATTACKS_FILE="$REPO_ROOT/tmp/red-team-attacks.jsonl" CANDIDATE="${CANDIDATE_NAME:-unknown}" PROFILE="${OPTIMIZER_PROFILE:-unknown}" ETH_EXTRACTED="0" ETH_BEFORE="0" while [[ $# -gt 0 ]]; do case "$1" in --attacks) ATTACKS_FILE="$2"; shift 2 ;; --candidate) CANDIDATE="$2"; shift 2 ;; --profile) PROFILE="$2"; shift 2 ;; --eth-extracted) ETH_EXTRACTED="$2"; shift 2 ;; --eth-before) ETH_BEFORE="$2"; shift 2 ;; *) die "Unknown argument: $1" ;; esac done # ── Guard: file must exist and be non-empty ────────────────────────────────── if [[ ! -f "$ATTACKS_FILE" ]]; then log "Attack file not found: $ATTACKS_FILE — nothing to promote" exit 0 fi if [[ ! -s "$ATTACKS_FILE" ]]; then log "Attack file is empty — nothing to promote" exit 0 fi OP_COUNT=$(wc -l < "$ATTACKS_FILE") log "Processing $OP_COUNT ops from $ATTACKS_FILE" log " candidate : $CANDIDATE" log " profile : $PROFILE" log " extracted : $ETH_EXTRACTED wei" # ── Resolve Codeberg API token ─────────────────────────────────────────────── API_TOKEN="${CODEBERG_TOKEN:-}" if [[ -z "$API_TOKEN" ]] && [[ -f "${HOME:-/home/debian}/.netrc" ]]; then API_TOKEN=$(awk '/codeberg.org/{getline;getline;print $2}' \ "${HOME:-/home/debian}/.netrc" 2>/dev/null || true) fi if [[ -z "$API_TOKEN" ]]; then warn "No Codeberg token found (set CODEBERG_TOKEN or configure ~/.netrc) — skipping PR" exit 0 fi # ── Classify attack type and deduplicate ───────────────────────────────────── CLASSIFY_OUT=$(python3 - "$ATTACKS_FILE" "$ATTACKS_DIR" <<'PYEOF' import json import sys from pathlib import Path def load_ops(path): ops = [] with open(path) as f: for line in f: line = line.strip() if line: try: ops.append(json.loads(line)) except json.JSONDecodeError: pass return ops def fingerprint(ops): """Ordered tuple of op types — used for deduplication (ignores amounts).""" return tuple(op.get("op", "") for op in ops) def classify(ops): """Classify attack type from the operation sequence.""" types = [op.get("op", "") for op in ops] has_stake = "stake" in types has_unstake = "unstake" in types has_mint_lp = "mint_lp" in types has_burn_lp = "burn_lp" in types has_loop = "buy_recenter_loop" in types buys = types.count("buy") sells = types.count("sell") recenters = types.count("recenter") if has_stake and has_unstake: return "staking" if has_mint_lp or has_burn_lp: return "lp-manipulation" # Compound loop op — il-crystallization with large count if has_loop: return "il-crystallization" # Oscillation: many buys and sells with frequent switching if buys >= 3 and sells >= 3: non_rc = [t for t in types if t not in ("recenter", "mine")] alternations = sum( 1 for i in range(len(non_rc) - 1) if non_rc[i] != non_rc[i + 1] ) if alternations >= 4: return "fee-drain-oscillation" # IL crystallisation: multiple buys then one final sell if buys >= 3 and sells == 1: return "il-crystallization" # Floor ratchet: many recenters triggered by buys if recenters >= 5 and buys >= 2: return "floor-ratchet" return "floor-attack" attacks_file = sys.argv[1] attacks_dir = sys.argv[2] new_ops = load_ops(attacks_file) if not new_ops: print("EMPTY") sys.exit(0) new_fp = fingerprint(new_ops) # Deduplication: compare op-type fingerprint against every existing file existing_dir = Path(attacks_dir) if existing_dir.is_dir(): for existing_file in sorted(existing_dir.glob("*.jsonl")): try: existing_ops = load_ops(existing_file) if fingerprint(existing_ops) == new_fp: print(f"DUPLICATE:{existing_file.name}") sys.exit(0) except Exception: pass print(f"NOVEL:{classify(new_ops)}") PYEOF ) log "Classifier: $CLASSIFY_OUT" case "$CLASSIFY_OUT" in EMPTY) log "No ops found in attack file — skipping" exit 0 ;; DUPLICATE:*) log "Op sequence matches existing file: ${CLASSIFY_OUT#DUPLICATE:} — skipping" exit 0 ;; NOVEL:*) ATTACK_TYPE="${CLASSIFY_OUT#NOVEL:}" ;; *) warn "Unexpected classifier output: $CLASSIFY_OUT" exit 0 ;; esac log "Novel attack type: $ATTACK_TYPE" # ── Determine destination filename ─────────────────────────────────────────── # Slug: lowercase, alphanumeric + hyphens, max 30 chars CANDIDATE_SLUG=$(printf '%s' "$CANDIDATE" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9-]/-/g' \ | sed 's/-\+/-/g;s/^-//;s/-$//' \ | cut -c1-30) BASE_NAME="${ATTACK_TYPE}-${CANDIDATE_SLUG}" # Avoid collisions with existing files by appending -v2, -v3, ... SUFFIX="" V=2 while [[ -f "$ATTACKS_DIR/${BASE_NAME}${SUFFIX}.jsonl" ]]; do SUFFIX="-v${V}" (( V++ )) done BASE_NAME="${BASE_NAME}${SUFFIX}" DEST_RELPATH="onchain/script/backtesting/attacks/${BASE_NAME}.jsonl" log "Destination: $DEST_RELPATH" # ── Format ETH values for human-readable output ────────────────────────────── ETH_X=$(python3 -c "print(f'{int(\"$ETH_EXTRACTED\") / 1e18:.4f}')" 2>/dev/null \ || echo "$ETH_EXTRACTED wei") ETH_B=$(python3 -c "print(f'{int(\"$ETH_BEFORE\") / 1e18:.4f}')" 2>/dev/null \ || echo "$ETH_BEFORE wei") # ── Git: create branch + commit in a temporary worktree ────────────────────── DATE_TAG=$(date -u +%Y%m%d-%H%M%S) BRANCH="red-team/${ATTACK_TYPE}-${CANDIDATE_SLUG}-${DATE_TAG}" TMPWT=$(mktemp -d) cleanup_worktree() { local rc=$? cd "$REPO_ROOT" 2>/dev/null || true git worktree remove --force "$TMPWT" 2>/dev/null || true git worktree prune --quiet 2>/dev/null || true rm -rf "$TMPWT" 2>/dev/null || true exit $rc } trap cleanup_worktree EXIT log "Fetching origin/master ..." git -C "$REPO_ROOT" fetch origin master --quiet 2>/dev/null \ || warn "git fetch failed — using local origin/master state" log "Creating worktree branch: $BRANCH ..." git -C "$REPO_ROOT" worktree add -b "$BRANCH" "$TMPWT" "origin/master" --quiet # Copy attack file into the isolated worktree cp "$ATTACKS_FILE" "$TMPWT/$DEST_RELPATH" cd "$TMPWT" git add "$DEST_RELPATH" git commit --quiet -m "$(cat <&1) || die "curl failed when creating PR" PR_NUMBER=$(printf '%s' "$PR_RESPONSE" | jq -r '.number // empty' 2>/dev/null || true) PR_URL=$(printf '%s' "$PR_RESPONSE" | jq -r '.html_url // empty' 2>/dev/null || true) if [[ -n "$PR_NUMBER" && "$PR_NUMBER" != "null" ]]; then log "PR #${PR_NUMBER} created: ${PR_URL}" else warn "PR creation returned unexpected response:" printf '%s' "$PR_RESPONSE" | head -c 400 >&2 die "PR creation failed" fi