Merge pull request 'fix: feat: red-team memory should track candidate + abstract learnings (#820)' (#830) from fix/issue-820 into master

This commit is contained in:
johba 2026-03-15 17:17:33 +01:00
commit bf1a735481
2 changed files with 184 additions and 18 deletions

View file

@ -63,12 +63,46 @@ for seed_file in "${seeds[@]}"; do
fi
log "Injected into OptimizerV3.sol"
# 1b. Extract optimizer profile from transpiler output (CI/AW/AS/DD constants)
TRANSPILER_OUT="$REPO_ROOT/onchain/src/OptimizerV3Push3.sol"
OPTIMIZER_PROFILE=$(python3 - "$TRANSPILER_OUT" <<'PYEOF'
import re, sys
try:
with open(sys.argv[1]) as f:
sol = f.read()
ci_vals = set(re.findall(r'\br40\s*=\s*uint256\((\d+)\)', sol))
aw_vals = set(re.findall(r'\br38\s*=\s*uint256\((\d+)\)', sol))
as_vals = set(re.findall(r'\br39\s*=\s*uint256\((\d+)\)', sol))
dd_vals = set(re.findall(r'\br37\s*=\s*uint256\((\d+)\)', sol))
def fmt_pct(vals):
pcts = sorted(set(round(int(v) * 100 / 1e18) for v in vals))
return '/'.join(str(p) + '%' for p in pcts) if pcts else '?'
def fmt_int(vals):
ints = sorted(set(int(v) for v in vals))
return '/'.join(str(v) for v in ints) if ints else '?'
profile = f"CI={fmt_pct(ci_vals)}, AW={fmt_int(aw_vals)}, AS={fmt_pct(as_vals)}, DD={fmt_pct(dd_vals)}"
# Adaptive: multiple constant branches, OR any register assigned from a variable
has_var_assign = bool(re.search(r'\br(?:37|38|39|40)\s*=\s*uint256\s*\(\s*[a-zA-Z_]\w*\s*\)', sol))
if len(ci_vals) > 1 or len(aw_vals) > 1 or len(as_vals) > 1 or len(dd_vals) > 1 or has_var_assign:
profile += ", adaptive"
print(profile)
except Exception as e:
print(f"unknown (parse error: {e})", file=sys.stderr)
print("unknown")
PYEOF
)
log "Optimizer profile: $OPTIMIZER_PROFILE"
# 2. Clear stale attack file from previous candidate
rm -f "$REPO_ROOT/tmp/red-team-attacks.jsonl"
# 3. Run red-team.sh (handles bootstrap + compile + deploy + attack)
log "Running red-team.sh (timeout: ${TIMEOUT_PER}s)..."
CLAUDE_TIMEOUT="$TIMEOUT_PER" timeout "$((TIMEOUT_PER + 120))" \
CLAUDE_TIMEOUT="$TIMEOUT_PER" CANDIDATE_NAME="$seed_name" OPTIMIZER_PROFILE="$OPTIMIZER_PROFILE" \
timeout "$((TIMEOUT_PER + 120))" \
bash "$SCRIPT_DIR/red-team.sh" 2>&1 | tee "/tmp/red-team-${seed_name}.log" || true
# 4. Collect attacks

View file

@ -30,6 +30,10 @@ ATTACK_EXPORT="$REPORT_DIR/red-team-attacks.jsonl"
ATTACK_SNAPSHOTS="$REPORT_DIR/red-team-snapshots.jsonl"
DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json"
# ── Candidate metadata (set by red-team-sweep.sh; defaults to unknown for standalone runs) ─
CANDIDATE_NAME="${CANDIDATE_NAME:-unknown}"
OPTIMIZER_PROFILE="${OPTIMIZER_PROFILE:-unknown}"
# ── Anvil accounts ─────────────────────────────────────────────────────────────
# Account 8 — adversary (10k ETH, 0 KRK)
ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
@ -185,15 +189,20 @@ extract_memory() {
local stream_file="$1"
local run_num memory_file="$MEMORY_FILE"
# Determine run number: one entry per line in JSONL, so next run = line_count + 1
# Determine run number: use max run in file + 1 so it stays monotonic after trim
if [[ -f "$memory_file" ]]; then
run_num=$(wc -l < "$memory_file")
run_num=$((run_num + 1))
run_num=$(python3 - "$memory_file" <<'EOF'
import json, sys
entries = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
print(max((e.get('run', 0) for e in entries), default=0) + 1)
EOF
)
[[ "$run_num" =~ ^[0-9]+$ ]] || run_num=1
else
run_num=1
fi
python3 - "$stream_file" "$memory_file" "$run_num" "$LM_ETH_BEFORE" <<'PYEOF'
python3 - "$stream_file" "$memory_file" "$run_num" "$LM_ETH_BEFORE" "$CANDIDATE_NAME" "$OPTIMIZER_PROFILE" <<'PYEOF'
import json, sys, re
from datetime import datetime, timezone
@ -205,6 +214,58 @@ try:
except (ValueError, IndexError):
print(" extract_memory: invalid lm_eth_before value, skipping", file=sys.stderr)
sys.exit(0)
candidate = sys.argv[5] if len(sys.argv) > 5 else "unknown"
optimizer_profile = sys.argv[6] if len(sys.argv) > 6 else "unknown"
def make_pattern(strategy_name, steps_text):
"""Extract abstract op sequence preserving execution order."""
text = (strategy_name + " " + steps_text).lower()
op_positions = []
for kw, label in [("wrap", "wrap"), ("buy", "buy"), ("sell", "sell")]:
m = re.search(r'\b' + kw + r'\b', text)
if m:
op_positions.append((m.start(), label))
# Use word boundaries so 'stake' never matches inside 'unstake'
m_stake = re.search(r'\bstake\b', text)
if m_stake:
ctx = text[max(0, m_stake.start() - 10):m_stake.start() + 20]
op_positions.append((m_stake.start(), "stake_all" if "all" in ctx else "stake"))
m_unstake = re.search(r'\bunstake\b', text)
if m_unstake:
op_positions.append((m_unstake.start(), "unstake"))
recenter_matches = list(re.finditer(r'\brecenter\b', text))
if recenter_matches:
label = "recenter" if len(recenter_matches) == 1 else "recenter_multi"
op_positions.append((recenter_matches[0].start(), label))
# add_lp: keyword or mint + LP context
m = re.search(r'\badd_lp\b', text)
if m:
op_positions.append((m.start(), "add_lp"))
elif re.search(r'\bmint\b', text) and ("lp" in text or "liquidity" in text):
m = re.search(r'\bmint\b', text)
op_positions.append((m.start(), "add_lp"))
# remove_lp: keyword or decreaseliquidity
for pat in [r'\bremove_lp\b', r'\bdecreaseliquidity\b']:
m = re.search(pat, text)
if m:
op_positions.append((m.start(), "remove_lp"))
break
# Sort by first occurrence position to reflect actual execution order
op_positions.sort(key=lambda x: x[0])
seen = set()
ops = []
for _, label in op_positions:
if label not in seen:
seen.add(label)
ops.append(label)
return " → ".join(ops) if ops else strategy_name[:60]
texts = []
with open(stream_file) as f:
@ -234,7 +295,8 @@ for text in texts:
"strategy": strat_match.group(1).strip(),
"steps": "",
"lm_eth_after": None,
"insight": ""
"insight": "",
"insight_pri": 999 # tracks priority of stored insight; lower index wins
}
if current:
@ -243,11 +305,21 @@ for text in texts:
if floor_matches:
current["lm_eth_after"] = int(floor_matches[-1].group(1))
# Capture insights
for pattern in [r"[Kk]ey [Ii]nsight:\s*(.+)", r"[Ii]nsight:\s*(.+)", r"(?:discovered|learned|realized)\s+(?:that\s+)?(.+)"]:
insight_match = re.search(pattern, text)
# Capture insights — prefer explicit labels; only overwrite if new match is higher priority
for pri, ins_pat in enumerate([
r"[Kk]ey [Ii]nsight:\s*(.+)",
r"[Ii]nsight:\s*(.+)",
r"[Ww][Hh][Yy][^:]*:\s*(.{30,})",
r"(?:because|since|due to)\s+(.{30,})",
r"(?:discovered|learned|realized)\s+(?:that\s+)?(.+)"
]):
if pri >= current["insight_pri"]:
break # already have a higher-priority insight stored
insight_match = re.search(ins_pat, text)
if insight_match and len(insight_match.group(1)) > 20:
current["insight"] = insight_match.group(1).strip()[:300]
current["insight_pri"] = pri
break
# Capture step summaries
if any(word in text.lower() for word in ["wrap", "buy", "sell", "stake", "recenter", "mint", "approve"]):
@ -270,10 +342,14 @@ with open(memory_file, "a") as f:
else:
result = "HELD"
pattern = make_pattern(s["strategy"], s["steps"])
entry = {
"run": run_num,
"ts": ts,
"candidate": candidate,
"optimizer_profile": optimizer_profile,
"strategy": s["strategy"][:100],
"pattern": pattern[:150],
"steps": s["steps"][:300].rstrip("; "),
"lm_eth_before": lm_eth_before,
"lm_eth_after": fa,
@ -282,7 +358,7 @@ with open(memory_file, "a") as f:
"insight": s["insight"][:300]
}
f.write(json.dumps(entry) + "\n")
print(f" Recorded: {entry['strategy']} → {result} ({delta_bps:+d} bps)")
print(f" Recorded: {entry['strategy']} [{entry['candidate']}] → {result} ({delta_bps:+d} bps)")
if not strategies:
print(" No strategies detected in stream output")
@ -329,6 +405,7 @@ MEMORY_SECTION=""
if [[ -f "$MEMORY_FILE" && -s "$MEMORY_FILE" ]]; then
MEMORY_SECTION=$(python3 - "$MEMORY_FILE" <<'PYEOF'
import json, sys
from collections import defaultdict
entries = []
with open(sys.argv[1]) as f:
for line in f:
@ -340,17 +417,47 @@ if not entries:
print('## Previous Findings (from earlier runs)')
print()
print('DO NOT repeat strategies marked HELD or INCREASED. Build on the insights.')
print('Distinguish optimizer-specific vulnerabilities from universal patterns.')
print('Try NEW combinations not yet attempted. Combine tools creatively.')
print()
for e in entries:
r = e.get('result', '?')
emoji = '❌' if r == 'DECREASED' else '⬆️' if r == 'INCREASED' else '➡️'
print(f"### Run {e.get('run','?')}: {e.get('strategy','?')} {emoji} {r}")
print(f"Steps: {e.get('steps','?')}")
print(f"Delta: {e.get('delta_bps',0)} bps")
if e.get('insight'):
print(f"**Insight:** {e['insight']}")
# Cross-candidate: patterns that DECREASED in multiple distinct candidates
decreased = [e for e in entries if e.get('result') == 'DECREASED']
cross = defaultdict(set)
for e in decreased:
key = e.get('pattern') or e.get('strategy', '')
cross[key].add(e.get('candidate', 'unknown'))
universal = [(p, cands) for p, cands in cross.items() if len(cands) > 1]
if universal:
print('### Universal Patterns (succeeded across multiple candidates)')
for pat, cands in universal:
print(f"- **{pat}** — worked on: {', '.join(sorted(cands))}")
print()
# Group remaining entries by candidate
by_candidate = defaultdict(list)
for e in entries:
by_candidate[e.get('candidate', 'unknown')].append(e)
for cand, cand_entries in sorted(by_candidate.items()):
prof = next((e.get('optimizer_profile', '') for e in cand_entries
if e.get('optimizer_profile', '') not in ('', 'unknown')), '')
print(f"### Candidate: {cand}")
if prof:
print(f"Profile: {prof}")
print()
for e in cand_entries:
r = e.get('result', '?')
emoji = '❌' if r == 'DECREASED' else '⬆️' if r == 'INCREASED' else '➡️'
pat = e.get('pattern', '')
print(f"#### Run {e.get('run','?')}: {e.get('strategy','?')} {emoji} {r}")
if pat:
print(f"Pattern: `{pat}`")
print(f"Steps: {e.get('steps','?')}")
print(f"Delta: {e.get('delta_bps',0)} bps")
if e.get('insight'):
print(f"**Insight:** {e['insight']}")
print()
PYEOF
)
fi
@ -371,6 +478,21 @@ The metric is simple: if LM total ETH goes down, you win.
---
## Current Attack Target
| Field | Value |
|-------|-------|
| Candidate | ${CANDIDATE_NAME} |
| Optimizer Profile | ${OPTIMIZER_PROFILE} |
Use the optimizer profile to reason about this candidate's behavior:
- **CI** (concentration index %): higher → optimizer recenters more aggressively → more KRK minting opportunities
- **AW** (anchorWidth ticks): wider → liquidity spread over larger price range → less ETH per tick
- **AS** (anchorShare %): higher → more ETH locked in anchor position → different rebalancing behavior
- **DD** (discoveryDepth %): higher → more ETH in discovery position (above-price) → price-sensitive exposure
---
## Contract addresses (local Anvil)
| Contract | Address |
@ -649,6 +771,11 @@ SNAP=\$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://local
6. If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches.
7. Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc.
8. Start executing immediately. No lengthy planning — act, measure, iterate.
9. For EVERY strategy attempted, record:
- **Pattern**: abstract op sequence (e.g., "buy → stake_all → recenter_multi → unstake → sell")
- **Insight**: WHY this worked or failed, referencing the optimizer profile (${OPTIMIZER_PROFILE}).
For HELD/INCREASED: which mechanism defended the floor? How did CI/AW/AS/DD cause it?
For DECREASED: which parameter combination created the vulnerability? Is it universal or optimizer-specific?
---
@ -661,12 +788,16 @@ After trying all strategies, output a clearly structured report:
\`\`\`
=== RED-TEAM REPORT ===
Candidate: ${CANDIDATE_NAME}
Optimizer Profile: ${OPTIMIZER_PROFILE}
lm_eth_before: <value> wei (total: free + positions)
STRATEGY 1: <name>
Pattern: <abstract op sequence e.g. "buy → recenter → sell">
Steps: <what you did>
lm_eth_after: <value> wei
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
Insight: <WHY this worked/failed given the optimizer profile>
STRATEGY 2: ...
...
@ -674,6 +805,7 @@ STRATEGY 2: ...
=== CONCLUSION ===
ETH extracted: YES / NO
Winning strategy: <describe if YES, else "None">
Universal pattern: <would this likely work on other candidates? Why or why not?>
lm_eth_before: ${LM_ETH_BEFORE} wei
lm_eth_after: <final value> wei
\`\`\`