2026-03-09 03:28:10 +00:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# red-team.sh — Adversarial floor-attack agent runner.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Spawns a Claude sub-agent with tools and a goal: make ethPerToken() decrease.
|
|
|
|
|
|
# The agent iterates freely — snapshot → strategy → check floor → revert → repeat.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Usage: red-team.sh
|
|
|
|
|
|
#
|
|
|
|
|
|
# Exit codes:
|
|
|
|
|
|
# 0 floor held (no confirmed decrease)
|
|
|
|
|
|
# 1 floor broken (agent found a strategy that decreased ethPerToken)
|
|
|
|
|
|
# 2 infra error (stack not running, missing dependency, etc.)
|
|
|
|
|
|
#
|
|
|
|
|
|
# Environment overrides:
|
|
|
|
|
|
# CLAUDE_TIMEOUT seconds for the agent run (default: 7200)
|
|
|
|
|
|
# RPC_URL Anvil RPC endpoint (default: http://localhost:8545)
|
|
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
CAST=/home/debian/.foundry/bin/cast
|
2026-03-11 02:08:06 +00:00
|
|
|
|
FORGE=/home/debian/.foundry/bin/forge
|
2026-03-09 03:28:10 +00:00
|
|
|
|
RPC_URL="${RPC_URL:-http://localhost:8545}"
|
|
|
|
|
|
CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"
|
|
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
|
|
|
|
REPORT_DIR="$REPO_ROOT/tmp"
|
|
|
|
|
|
REPORT="$REPORT_DIR/red-team-report.txt"
|
2026-03-09 09:23:37 +00:00
|
|
|
|
STREAM_LOG="$REPORT_DIR/red-team-stream.jsonl"
|
2026-03-11 02:08:06 +00:00
|
|
|
|
MEMORY_FILE="$REPO_ROOT/tmp/red-team-memory.jsonl"
|
|
|
|
|
|
ATTACK_EXPORT="$REPORT_DIR/red-team-attacks.jsonl"
|
|
|
|
|
|
ATTACK_SNAPSHOTS="$REPORT_DIR/red-team-snapshots.jsonl"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json"
|
|
|
|
|
|
|
2026-03-15 15:23:43 +00:00
|
|
|
|
# ── Candidate metadata (set by red-team-sweep.sh; defaults to unknown for standalone runs) ─
|
|
|
|
|
|
CANDIDATE_NAME="${CANDIDATE_NAME:-unknown}"
|
|
|
|
|
|
OPTIMIZER_PROFILE="${OPTIMIZER_PROFILE:-unknown}"
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
# ── Anvil accounts ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Account 8 — adversary (10k ETH, 0 KRK)
|
|
|
|
|
|
ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
|
2026-03-14 15:10:59 +00:00
|
|
|
|
# Account 2 — recenter caller (recenter is public, any account can call)
|
2026-03-09 03:28:10 +00:00
|
|
|
|
RECENTER_PK=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
|
|
|
|
|
|
|
|
|
|
|
|
# ── Infrastructure constants ───────────────────────────────────────────────────
|
|
|
|
|
|
WETH=0x4200000000000000000000000000000000000006
|
2026-03-15 06:48:16 +00:00
|
|
|
|
# Base mainnet SwapRouter02 — https://basescan.org/address/0x2626664c2603336E57B271c5C0b26F421741e481
|
|
|
|
|
|
SWAP_ROUTER=0x2626664c2603336E57B271c5C0b26F421741e481
|
|
|
|
|
|
# Base mainnet Uniswap V3 Factory — https://basescan.org/address/0x33128a8fC17869897dcE68Ed026d694621f6FDfD
|
|
|
|
|
|
V3_FACTORY=0x33128a8fC17869897dcE68Ed026d694621f6FDfD
|
2026-03-14 02:22:51 +00:00
|
|
|
|
# Base mainnet NonfungiblePositionManager — https://basescan.org/address/0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3
|
|
|
|
|
|
NPM=0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3
|
2026-03-09 03:28:10 +00:00
|
|
|
|
POOL_FEE=10000
|
|
|
|
|
|
|
|
|
|
|
|
# ── Logging helpers ────────────────────────────────────────────────────────────
|
|
|
|
|
|
log() { echo "[red-team] $*"; }
|
|
|
|
|
|
die() { echo "[red-team] ERROR: $*" >&2; exit 2; }
|
|
|
|
|
|
|
|
|
|
|
|
# ── Prerequisites ──────────────────────────────────────────────────────────────
|
2026-03-11 02:08:06 +00:00
|
|
|
|
command -v "$CAST" &>/dev/null || die "cast not found at $CAST"
|
|
|
|
|
|
command -v "$FORGE" &>/dev/null || die "forge not found at $FORGE"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
command -v claude &>/dev/null || die "claude CLI not found (install: npm i -g @anthropic-ai/claude-code)"
|
|
|
|
|
|
command -v python3 &>/dev/null || die "python3 not found"
|
|
|
|
|
|
command -v jq &>/dev/null || die "jq not found"
|
|
|
|
|
|
|
2026-03-13 11:55:22 +00:00
|
|
|
|
# ── 1. Fresh stack via bootstrap-light ─────────────────────────────────────────
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
|
log "Running bootstrap-light ..."
|
|
|
|
|
|
bash "$SCRIPT_DIR/bootstrap-light.sh" || die "bootstrap-light failed"
|
2026-03-10 19:11:11 +00:00
|
|
|
|
|
|
|
|
|
|
# Verify Anvil responds
|
2026-03-09 03:28:10 +00:00
|
|
|
|
"$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
2026-03-13 11:55:22 +00:00
|
|
|
|
|| die "Anvil not accessible at $RPC_URL after bootstrap-light"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
# ── 2. Read contract addresses ─────────────────────────────────────────────────
|
|
|
|
|
|
[[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)"
|
|
|
|
|
|
|
|
|
|
|
|
KRK=$(jq -r '.contracts.Kraiken' "$DEPLOYMENTS")
|
|
|
|
|
|
STAKE=$(jq -r '.contracts.Stake' "$DEPLOYMENTS")
|
|
|
|
|
|
LM=$(jq -r '.contracts.LiquidityManager' "$DEPLOYMENTS")
|
|
|
|
|
|
OPT=$(jq -r '.contracts.OptimizerProxy' "$DEPLOYMENTS")
|
|
|
|
|
|
|
|
|
|
|
|
for var in KRK STAKE LM OPT; do
|
|
|
|
|
|
val="${!var}"
|
|
|
|
|
|
[[ -n "$val" && "$val" != "null" ]] \
|
|
|
|
|
|
|| die "$var address missing from deployments-local.json — was bootstrap successful?"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
log " KRK: $KRK"
|
|
|
|
|
|
log " STAKE: $STAKE"
|
|
|
|
|
|
log " LM: $LM"
|
|
|
|
|
|
log " OPT: $OPT"
|
|
|
|
|
|
|
|
|
|
|
|
# Derive Anvil account addresses from their private keys
|
|
|
|
|
|
ADV_ADDR=$("$CAST" wallet address --private-key "$ADV_PK")
|
|
|
|
|
|
RECENTER_ADDR=$("$CAST" wallet address --private-key "$RECENTER_PK")
|
|
|
|
|
|
log " Adversary: $ADV_ADDR (account 8)"
|
|
|
|
|
|
log " Recenter: $RECENTER_ADDR (account 2)"
|
|
|
|
|
|
|
|
|
|
|
|
# Get Uniswap V3 Pool address
|
|
|
|
|
|
POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \
|
2026-03-11 10:19:14 +00:00
|
|
|
|
"$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log " Pool: $POOL"
|
|
|
|
|
|
|
2026-03-14 15:10:59 +00:00
|
|
|
|
# ── 3a. recenter() is now public (no recenterAccess needed) ──
|
|
|
|
|
|
# Any address can call recenter() — TWAP oracle enforces safety.
|
|
|
|
|
|
log "recenter() is public — no access grant needed"
|
2026-03-13 11:55:22 +00:00
|
|
|
|
|
|
|
|
|
|
# ── 3b. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
|
2026-03-12 16:13:44 +00:00
|
|
|
|
# setFeeDestination allows repeated EOA sets; setting to a contract locks it permanently.
|
|
|
|
|
|
# The deployer (Anvil account 0) deployed LiquidityManager and may call setFeeDestination again.
|
2026-03-12 17:13:50 +00:00
|
|
|
|
# DEPLOYER_PK is Anvil's deterministic account-0 key — valid ONLY against a local ephemeral
|
|
|
|
|
|
# Anvil instance. Never run this script against a non-ephemeral or shared-state chain.
|
2026-03-12 16:13:44 +00:00
|
|
|
|
DEPLOYER_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
2026-03-10 19:11:11 +00:00
|
|
|
|
log "Setting feeDestination to LM ($LM) ..."
|
2026-03-12 16:13:44 +00:00
|
|
|
|
"$CAST" send --rpc-url "$RPC_URL" --private-key "$DEPLOYER_PK" \
|
|
|
|
|
|
"$LM" "setFeeDestination(address)" "$LM" >/dev/null 2>&1 \
|
|
|
|
|
|
|| die "setFeeDestination($LM) failed"
|
2026-03-11 10:19:14 +00:00
|
|
|
|
VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
2026-03-10 19:11:11 +00:00
|
|
|
|
log " feeDestination set to: $VERIFY"
|
|
|
|
|
|
[[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY"
|
|
|
|
|
|
|
2026-03-13 11:55:22 +00:00
|
|
|
|
# ── 3c. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# Send ETH as WETH (LM uses WETH internally), then recenter to deploy into positions.
|
|
|
|
|
|
# Without recenter, the ETH sits idle and the first recenter mints massive KRK.
|
|
|
|
|
|
log "Funding LM with 1000 ETH ..."
|
|
|
|
|
|
# Wrap to WETH and transfer to LM
|
|
|
|
|
|
"$CAST" send "$WETH" "deposit()" --value 1000ether \
|
|
|
|
|
|
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
|
|
|
|
|
|| die "Failed to wrap ETH"
|
|
|
|
|
|
"$CAST" send "$WETH" "transfer(address,uint256)" "$LM" 1000000000000000000000 \
|
|
|
|
|
|
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
|
|
|
|
|
|| die "Failed to transfer WETH to LM"
|
|
|
|
|
|
|
|
|
|
|
|
# Recenter to deploy the new WETH into positions (establishes realistic baseline)
|
|
|
|
|
|
log "Recentering to deploy funded WETH into positions ..."
|
|
|
|
|
|
"$CAST" send "$LM" "recenter()" \
|
|
|
|
|
|
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
|
|
|
|
|
|| log " WARNING: initial recenter failed (may need amplitude — mining blocks)"
|
2026-03-15 10:47:36 +00:00
|
|
|
|
# Advance time and mine blocks, then retry recenter
|
2026-03-10 19:11:11 +00:00
|
|
|
|
for _i in $(seq 1 3); do
|
2026-03-15 10:47:36 +00:00
|
|
|
|
"$CAST" rpc evm_increaseTime 600 --rpc-url "$RPC_URL" >/dev/null 2>&1
|
2026-03-10 19:11:11 +00:00
|
|
|
|
for _b in $(seq 1 50); do
|
|
|
|
|
|
"$CAST" rpc evm_mine --rpc-url "$RPC_URL" >/dev/null 2>&1
|
|
|
|
|
|
done
|
|
|
|
|
|
"$CAST" send "$LM" "recenter()" \
|
|
|
|
|
|
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 && break
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
LM_WETH=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
log " LM after recenter: ETH=$LM_ETH WETH=$LM_WETH"
|
|
|
|
|
|
|
2026-03-14 15:10:59 +00:00
|
|
|
|
# ── 4. Take Anvil snapshot (clean baseline) ─────
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log "Taking Anvil snapshot..."
|
|
|
|
|
|
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
|
|
|
|
|
|
log " Snapshot ID: $SNAP"
|
|
|
|
|
|
|
2026-03-09 03:59:12 +00:00
|
|
|
|
# Revert to the baseline snapshot on exit so subsequent runs start clean.
|
2026-03-13 09:48:34 +00:00
|
|
|
|
CLAUDE_PID=""
|
2026-03-09 03:59:12 +00:00
|
|
|
|
cleanup() {
|
|
|
|
|
|
local rc=$?
|
2026-03-13 09:48:34 +00:00
|
|
|
|
if [[ -n "${CLAUDE_PID:-}" ]]; then
|
|
|
|
|
|
kill "$CLAUDE_PID" 2>/dev/null || true
|
|
|
|
|
|
fi
|
2026-03-09 03:59:12 +00:00
|
|
|
|
if [[ -n "${SNAP:-}" ]]; then
|
|
|
|
|
|
"$CAST" rpc anvil_revert "$SNAP" --rpc-url "$RPC_URL" >/dev/null 2>&1 || true
|
|
|
|
|
|
fi
|
|
|
|
|
|
exit $rc
|
|
|
|
|
|
}
|
|
|
|
|
|
trap cleanup EXIT INT TERM
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# ── Helper: compute total ETH controlled by LM ────────────────────────────────
|
|
|
|
|
|
# Total = free ETH + free WETH + ETH locked in all 3 Uni V3 positions
|
|
|
|
|
|
# This is the real metric: "can the adversary extract ETH from the protocol?"
|
|
|
|
|
|
# Uses a forge script with exact Uni V3 integer math (LiquidityAmounts + TickMath)
|
|
|
|
|
|
# instead of multiple cast calls + Python float approximation.
|
|
|
|
|
|
compute_lm_total_eth() {
|
2026-03-10 20:01:12 +00:00
|
|
|
|
local output result
|
2026-03-13 11:55:22 +00:00
|
|
|
|
output=$(cd "$REPO_ROOT" && LM="$LM" WETH="$WETH" POOL="$POOL" \
|
|
|
|
|
|
"$FORGE" script onchain/script/LmTotalEth.s.sol \
|
|
|
|
|
|
--rpc-url "$RPC_URL" --root onchain 2>&1)
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# forge script prints "== Logs ==" then " <value>" — extract the number
|
2026-03-10 20:01:12 +00:00
|
|
|
|
result=$(echo "$output" | awk '/^== Logs ==/{getline; gsub(/^[[:space:]]+/,""); print; exit}')
|
|
|
|
|
|
[[ -n "$result" && "$result" =~ ^[0-9]+$ ]] || die "Failed to read LM total ETH (forge output: $output)"
|
|
|
|
|
|
echo "$result"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 09:23:37 +00:00
|
|
|
|
# ── Helper: extract strategy findings from stream-json and append to memory ────
|
|
|
|
|
|
extract_memory() {
|
|
|
|
|
|
local stream_file="$1"
|
|
|
|
|
|
local run_num memory_file="$MEMORY_FILE"
|
|
|
|
|
|
|
2026-03-09 10:00:56 +00:00
|
|
|
|
# Determine run number: one entry per line in JSONL, so next run = line_count + 1
|
2026-03-09 09:23:37 +00:00
|
|
|
|
if [[ -f "$memory_file" ]]; then
|
|
|
|
|
|
run_num=$(wc -l < "$memory_file")
|
2026-03-09 10:00:56 +00:00
|
|
|
|
run_num=$((run_num + 1))
|
2026-03-09 09:23:37 +00:00
|
|
|
|
else
|
|
|
|
|
|
run_num=1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-03-15 15:23:43 +00:00
|
|
|
|
python3 - "$stream_file" "$memory_file" "$run_num" "$LM_ETH_BEFORE" "$CANDIDATE_NAME" "$OPTIMIZER_PROFILE" <<'PYEOF'
|
2026-03-09 09:23:37 +00:00
|
|
|
|
import json, sys, re
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
|
|
stream_file = sys.argv[1]
|
|
|
|
|
|
memory_file = sys.argv[2]
|
|
|
|
|
|
run_num = int(sys.argv[3])
|
2026-03-09 10:00:56 +00:00
|
|
|
|
try:
|
2026-03-10 19:11:11 +00:00
|
|
|
|
lm_eth_before = int(sys.argv[4])
|
2026-03-09 10:00:56 +00:00
|
|
|
|
except (ValueError, IndexError):
|
2026-03-10 19:11:11 +00:00
|
|
|
|
print(" extract_memory: invalid lm_eth_before value, skipping", file=sys.stderr)
|
2026-03-09 10:00:56 +00:00
|
|
|
|
sys.exit(0)
|
2026-03-15 15:23:43 +00:00
|
|
|
|
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: buy → stake → recenter → sell."""
|
|
|
|
|
|
text = (strategy_name + " " + steps_text).lower()
|
|
|
|
|
|
ops = []
|
|
|
|
|
|
if "wrap" in text:
|
|
|
|
|
|
ops.append("wrap")
|
|
|
|
|
|
if "buy" in text:
|
|
|
|
|
|
ops.append("buy")
|
|
|
|
|
|
stake_pos = text.find("stake")
|
|
|
|
|
|
unstake_pos = text.find("unstake")
|
|
|
|
|
|
if stake_pos >= 0 and (unstake_pos < 0 or stake_pos < unstake_pos):
|
|
|
|
|
|
ops.append("stake_all" if "all" in text[max(0, stake_pos-10):stake_pos+20] else "stake")
|
|
|
|
|
|
recenters = len(re.findall(r"\brecenter\b", text))
|
|
|
|
|
|
if recenters == 1:
|
|
|
|
|
|
ops.append("recenter")
|
|
|
|
|
|
elif recenters > 1:
|
|
|
|
|
|
ops.append("recenter_multi")
|
|
|
|
|
|
if unstake_pos >= 0:
|
|
|
|
|
|
ops.append("unstake")
|
|
|
|
|
|
if "sell" in text:
|
|
|
|
|
|
ops.append("sell")
|
|
|
|
|
|
if "add_lp" in text or ("mint" in text and ("lp" in text or "liquidity" in text)):
|
|
|
|
|
|
ops.append("add_lp")
|
|
|
|
|
|
if "remove_lp" in text or "decreaseliquidity" in text:
|
|
|
|
|
|
ops.append("remove_lp")
|
|
|
|
|
|
return " → ".join(ops) if ops else strategy_name[:60]
|
2026-03-09 09:23:37 +00:00
|
|
|
|
|
|
|
|
|
|
texts = []
|
|
|
|
|
|
with open(stream_file) as f:
|
|
|
|
|
|
for line in f:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if not line:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
obj = json.loads(line)
|
|
|
|
|
|
if obj.get("type") == "assistant":
|
|
|
|
|
|
for block in obj.get("message", {}).get("content", []):
|
|
|
|
|
|
if block.get("type") == "text":
|
|
|
|
|
|
texts.append(block["text"])
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# Parse strategies from agent text
|
|
|
|
|
|
strategies = []
|
|
|
|
|
|
current = None
|
|
|
|
|
|
for text in texts:
|
2026-03-09 10:00:56 +00:00
|
|
|
|
# Detect strategy headers: matches "## Strategy 1: name" and "STRATEGY 1: name"
|
|
|
|
|
|
strat_match = re.search(r"(?:##\s*)?[Ss][Tt][Rr][Aa][Tt][Ee][Gg][Yy]\s*\d+[^:]*:\s*(.+)", text)
|
2026-03-09 09:23:37 +00:00
|
|
|
|
if strat_match:
|
|
|
|
|
|
if current:
|
|
|
|
|
|
strategies.append(current)
|
|
|
|
|
|
current = {
|
|
|
|
|
|
"strategy": strat_match.group(1).strip(),
|
|
|
|
|
|
"steps": "",
|
2026-03-10 19:11:11 +00:00
|
|
|
|
"lm_eth_after": None,
|
2026-03-09 09:23:37 +00:00
|
|
|
|
"insight": ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if current:
|
2026-03-09 10:00:56 +00:00
|
|
|
|
# Capture floor readings — take the last match in the block (most recent value)
|
2026-03-10 20:01:12 +00:00
|
|
|
|
floor_matches = list(re.finditer(r"(?:floor|ethPerToken|lm.?eth)[^\d]*?(\d{4,})\s*(?:wei)?", text, re.IGNORECASE))
|
2026-03-09 10:00:56 +00:00
|
|
|
|
if floor_matches:
|
2026-03-10 19:11:11 +00:00
|
|
|
|
current["lm_eth_after"] = int(floor_matches[-1].group(1))
|
2026-03-09 09:23:37 +00:00
|
|
|
|
|
2026-03-15 15:23:43 +00:00
|
|
|
|
# Capture insights — prefer explicit labels, then WHY explanations
|
|
|
|
|
|
for ins_pat in [
|
|
|
|
|
|
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+)?(.+)"
|
|
|
|
|
|
]:
|
|
|
|
|
|
insight_match = re.search(ins_pat, text)
|
2026-03-09 09:23:37 +00:00
|
|
|
|
if insight_match and len(insight_match.group(1)) > 20:
|
|
|
|
|
|
current["insight"] = insight_match.group(1).strip()[:300]
|
2026-03-15 15:23:43 +00:00
|
|
|
|
break
|
2026-03-09 09:23:37 +00:00
|
|
|
|
|
|
|
|
|
|
# Capture step summaries
|
|
|
|
|
|
if any(word in text.lower() for word in ["wrap", "buy", "sell", "stake", "recenter", "mint", "approve"]):
|
|
|
|
|
|
if len(text) < 200:
|
|
|
|
|
|
current["steps"] += text.strip() + "; "
|
|
|
|
|
|
|
|
|
|
|
|
if current:
|
|
|
|
|
|
strategies.append(current)
|
|
|
|
|
|
|
|
|
|
|
|
# Write to memory file
|
|
|
|
|
|
ts = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
with open(memory_file, "a") as f:
|
|
|
|
|
|
for s in strategies:
|
2026-03-10 19:11:11 +00:00
|
|
|
|
fa = s["lm_eth_after"] if s.get("lm_eth_after") is not None else lm_eth_before
|
|
|
|
|
|
delta_bps = round((fa - lm_eth_before) * 10000 / lm_eth_before) if lm_eth_before else 0
|
|
|
|
|
|
if fa < lm_eth_before:
|
2026-03-09 09:23:37 +00:00
|
|
|
|
result = "DECREASED"
|
2026-03-10 19:11:11 +00:00
|
|
|
|
elif fa > lm_eth_before:
|
2026-03-09 09:23:37 +00:00
|
|
|
|
result = "INCREASED"
|
|
|
|
|
|
else:
|
|
|
|
|
|
result = "HELD"
|
|
|
|
|
|
|
2026-03-15 15:23:43 +00:00
|
|
|
|
pattern = make_pattern(s["strategy"], s["steps"])
|
2026-03-09 09:23:37 +00:00
|
|
|
|
entry = {
|
|
|
|
|
|
"run": run_num,
|
|
|
|
|
|
"ts": ts,
|
2026-03-15 15:23:43 +00:00
|
|
|
|
"candidate": candidate,
|
|
|
|
|
|
"optimizer_profile": optimizer_profile,
|
2026-03-09 09:23:37 +00:00
|
|
|
|
"strategy": s["strategy"][:100],
|
2026-03-15 15:23:43 +00:00
|
|
|
|
"pattern": pattern[:150],
|
2026-03-09 09:23:37 +00:00
|
|
|
|
"steps": s["steps"][:300].rstrip("; "),
|
2026-03-10 19:11:11 +00:00
|
|
|
|
"lm_eth_before": lm_eth_before,
|
|
|
|
|
|
"lm_eth_after": fa,
|
2026-03-09 09:23:37 +00:00
|
|
|
|
"delta_bps": delta_bps,
|
|
|
|
|
|
"result": result,
|
|
|
|
|
|
"insight": s["insight"][:300]
|
|
|
|
|
|
}
|
|
|
|
|
|
f.write(json.dumps(entry) + "\n")
|
2026-03-15 15:23:43 +00:00
|
|
|
|
print(f" Recorded: {entry['strategy']} [{entry['candidate']}] → {result} ({delta_bps:+d} bps)")
|
2026-03-09 09:23:37 +00:00
|
|
|
|
|
|
|
|
|
|
if not strategies:
|
|
|
|
|
|
print(" No strategies detected in stream output")
|
|
|
|
|
|
|
|
|
|
|
|
# Trim memory file: keep 10 most recent + all DECREASED entries (cap at 50)
|
|
|
|
|
|
with open(memory_file) as f:
|
|
|
|
|
|
all_entries = [json.loads(l) for l in f if l.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
if len(all_entries) > 50:
|
2026-03-09 10:00:56 +00:00
|
|
|
|
# Keep all DECREASED entries + 10 most recent; deduplicate preserving order
|
2026-03-09 09:23:37 +00:00
|
|
|
|
trimmed = [e for e in all_entries if e.get("result") == "DECREASED"] + all_entries[-10:]
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
deduped = []
|
|
|
|
|
|
for e in trimmed:
|
|
|
|
|
|
key = (e.get("run"), e.get("ts"), e.get("strategy"))
|
|
|
|
|
|
if key not in seen:
|
|
|
|
|
|
seen.add(key)
|
|
|
|
|
|
deduped.append(e)
|
|
|
|
|
|
with open(memory_file, "w") as f:
|
|
|
|
|
|
for e in deduped:
|
|
|
|
|
|
f.write(json.dumps(e) + "\n")
|
|
|
|
|
|
print(f" Trimmed memory to {len(deduped)} entries")
|
|
|
|
|
|
PYEOF
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# ── 5. Read lm_eth_before ───────────────────────────────────────────────────────
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log "Reading floor before agent run..."
|
2026-03-10 19:11:11 +00:00
|
|
|
|
LM_ETH_BEFORE=$(compute_lm_total_eth)
|
|
|
|
|
|
log " lm_eth_before = $LM_ETH_BEFORE wei"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
# ── 6. Build agent prompt ──────────────────────────────────────────────────────
|
2026-03-09 09:23:37 +00:00
|
|
|
|
|
2026-03-15 14:18:10 +00:00
|
|
|
|
# ── 6a. Read Solidity source files (reflect the current candidate after inject) ─
|
|
|
|
|
|
ONCHAIN_SRC="$REPO_ROOT/onchain/src"
|
|
|
|
|
|
SOL_LM=$(< "$ONCHAIN_SRC/LiquidityManager.sol")
|
|
|
|
|
|
SOL_THREE_POS=$(< "$ONCHAIN_SRC/abstracts/ThreePositionStrategy.sol")
|
|
|
|
|
|
SOL_OPTIMIZER=$(< "$ONCHAIN_SRC/Optimizer.sol")
|
|
|
|
|
|
SOL_OPTIMIZERV3=$(< "$ONCHAIN_SRC/OptimizerV3.sol")
|
|
|
|
|
|
SOL_VWAP=$(< "$ONCHAIN_SRC/VWAPTracker.sol")
|
|
|
|
|
|
SOL_PRICE_ORACLE=$(< "$ONCHAIN_SRC/abstracts/PriceOracle.sol")
|
|
|
|
|
|
|
2026-03-09 09:23:37 +00:00
|
|
|
|
# Build Previous Findings section from memory file
|
|
|
|
|
|
MEMORY_SECTION=""
|
|
|
|
|
|
if [[ -f "$MEMORY_FILE" && -s "$MEMORY_FILE" ]]; then
|
2026-03-09 10:00:56 +00:00
|
|
|
|
MEMORY_SECTION=$(python3 - "$MEMORY_FILE" <<'PYEOF'
|
2026-03-09 09:23:37 +00:00
|
|
|
|
import json, sys
|
2026-03-15 15:23:43 +00:00
|
|
|
|
from collections import defaultdict
|
2026-03-09 09:23:37 +00:00
|
|
|
|
entries = []
|
2026-03-09 10:00:56 +00:00
|
|
|
|
with open(sys.argv[1]) as f:
|
2026-03-09 09:23:37 +00:00
|
|
|
|
for line in f:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if line:
|
|
|
|
|
|
entries.append(json.loads(line))
|
|
|
|
|
|
if not entries:
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
print('## Previous Findings (from earlier runs)')
|
|
|
|
|
|
print()
|
|
|
|
|
|
print('DO NOT repeat strategies marked HELD or INCREASED. Build on the insights.')
|
2026-03-15 15:23:43 +00:00
|
|
|
|
print('Distinguish optimizer-specific vulnerabilities from universal patterns.')
|
2026-03-09 09:23:37 +00:00
|
|
|
|
print('Try NEW combinations not yet attempted. Combine tools creatively.')
|
|
|
|
|
|
print()
|
2026-03-15 15:23:43 +00:00
|
|
|
|
|
|
|
|
|
|
# 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)
|
2026-03-09 09:23:37 +00:00
|
|
|
|
for e in entries:
|
2026-03-15 15:23:43 +00:00
|
|
|
|
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}")
|
2026-03-09 09:23:37 +00:00
|
|
|
|
print()
|
2026-03-15 15:23:43 +00:00
|
|
|
|
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()
|
2026-03-09 10:00:56 +00:00
|
|
|
|
PYEOF
|
|
|
|
|
|
)
|
2026-03-09 09:23:37 +00:00
|
|
|
|
fi
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
PROMPT=$(cat <<PROMPT_EOF
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# Red-team mission: extract ETH from the KRAIKEN LiquidityManager
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
## Goal
|
2026-03-10 19:11:11 +00:00
|
|
|
|
**Extract ETH from the LiquidityManager.** Make the LM end up with less total ETH than it started with.
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
Total LM ETH before: ${LM_ETH_BEFORE} wei
|
|
|
|
|
|
(free ETH + free WETH + ETH locked in all 3 Uni V3 positions)
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
The protocol's ETH is like gold in a cold-backed currency — it's "put to work" in Uniswap positions,
|
|
|
|
|
|
but it should NEVER be extractable by an adversary. Your job is to prove otherwise.
|
|
|
|
|
|
|
|
|
|
|
|
The metric is simple: if LM total ETH goes down, you win.
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-15 15:23:43 +00:00
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
## Contract addresses (local Anvil)
|
|
|
|
|
|
|
|
|
|
|
|
| Contract | Address |
|
|
|
|
|
|
|--------------------|---------|
|
|
|
|
|
|
| Kraiken (KRK) | ${KRK} |
|
|
|
|
|
|
| Stake | ${STAKE} |
|
|
|
|
|
|
| LiquidityManager | ${LM} |
|
|
|
|
|
|
| OptimizerProxy | ${OPT} |
|
|
|
|
|
|
| Pool (WETH/KRK 1%) | ${POOL} |
|
|
|
|
|
|
| NonfungiblePosManager (NPM) | ${NPM} |
|
|
|
|
|
|
| WETH | ${WETH} |
|
|
|
|
|
|
| SwapRouter02 | ${SWAP_ROUTER} |
|
|
|
|
|
|
|
|
|
|
|
|
RPC: http://localhost:8545
|
|
|
|
|
|
CAST binary: /home/debian/.foundry/bin/cast
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Your accounts
|
|
|
|
|
|
|
|
|
|
|
|
### Adversary — Anvil account 8 (your main account)
|
|
|
|
|
|
- Address: ${ADV_ADDR}
|
|
|
|
|
|
- Private key: ${ADV_PK}
|
2026-03-10 20:01:12 +00:00
|
|
|
|
- Balance: ~9000 ETH (10k minus 1000 ETH used to fund LM), 0 KRK
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
### Recenter caller — Anvil account 2
|
|
|
|
|
|
- Address: ${RECENTER_ADDR}
|
|
|
|
|
|
- Private key: ${RECENTER_PK}
|
2026-03-14 15:10:59 +00:00
|
|
|
|
- Can call recenter() (public, TWAP-enforced)
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Protocol mechanics
|
|
|
|
|
|
|
|
|
|
|
|
### ethPerToken (the floor)
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
ethPerToken = (LM_native_ETH + LM_WETH) * 1e18 / adjusted_supply
|
2026-03-13 07:47:35 +00:00
|
|
|
|
adjusted_supply = KRK.outstandingSupply() - KRK_at_Stake
|
2026-03-09 03:28:10 +00:00
|
|
|
|
\`\`\`
|
|
|
|
|
|
To DECREASE the floor you must either:
|
|
|
|
|
|
- Reduce LM's ETH/WETH holdings, OR
|
|
|
|
|
|
- Increase the adjusted outstanding supply of KRK
|
|
|
|
|
|
|
|
|
|
|
|
### Three LM positions
|
|
|
|
|
|
The LiquidityManager maintains three Uniswap V3 positions:
|
|
|
|
|
|
1. **ANCHOR** — straddles the current price; provides two-sided liquidity
|
|
|
|
|
|
2. **DISCOVERY** — above current price; captures upside momentum
|
|
|
|
|
|
3. **FLOOR** — a floor bid: ETH in, KRK out. Backing the floor price.
|
|
|
|
|
|
|
|
|
|
|
|
### recenter()
|
|
|
|
|
|
Calling \`LiquidityManager.recenter()\` removes all three positions, mints or burns KRK
|
|
|
|
|
|
to rebalance, then re-deploys positions at the current price. It:
|
|
|
|
|
|
- Can mint NEW KRK (increasing supply → decreasing floor)
|
|
|
|
|
|
- Can burn KRK (decreasing supply → increasing floor)
|
|
|
|
|
|
- Moves ETH between positions
|
2026-03-14 15:10:59 +00:00
|
|
|
|
Any account can call it (public). TWAP oracle enforces safety.
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
### Staking
|
|
|
|
|
|
\`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\`
|
2026-03-09 03:59:12 +00:00
|
|
|
|
- taxRateIndex: 0–29 (index into the 30-element TAX_RATES array — not a raw percentage)
|
2026-03-09 03:28:10 +00:00
|
|
|
|
- KRK staked is held by the Stake contract (excluded from adjusted_supply)
|
|
|
|
|
|
- KRK in Stake does NOT count against the floor denominator
|
|
|
|
|
|
|
|
|
|
|
|
### outstandingSupply() vs totalSupply()
|
2026-03-09 03:59:12 +00:00
|
|
|
|
\`KRK.outstandingSupply() = totalSupply() - balanceOf(liquidityManager)\`
|
|
|
|
|
|
LM-held KRK (in pool positions) is excluded from outstandingSupply.
|
2026-03-13 07:47:35 +00:00
|
|
|
|
The floor formula then additionally subtracts KRK at Stake to get adjusted_supply.
|
|
|
|
|
|
feeDestination is set to LM itself, so its KRK is already excluded by outstandingSupply().
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-15 14:18:10 +00:00
|
|
|
|
## Source Code (read-only reference)
|
|
|
|
|
|
|
|
|
|
|
|
Use the source code below to reason about internal state transitions, edge cases in tick math,
|
|
|
|
|
|
exact mint/burn logic, optimizer parameter effects, and floor formula details.
|
|
|
|
|
|
Do NOT attempt to deploy or modify contracts — these are for reference only.
|
|
|
|
|
|
|
|
|
|
|
|
### LiquidityManager.sol
|
|
|
|
|
|
\`\`\`solidity
|
|
|
|
|
|
${SOL_LM}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### ThreePositionStrategy.sol
|
|
|
|
|
|
\`\`\`solidity
|
|
|
|
|
|
${SOL_THREE_POS}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Optimizer.sol (base)
|
|
|
|
|
|
\`\`\`solidity
|
|
|
|
|
|
${SOL_OPTIMIZER}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### OptimizerV3.sol (current candidate — reflects inject.sh output)
|
|
|
|
|
|
\`\`\`solidity
|
|
|
|
|
|
${SOL_OPTIMIZERV3}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### VWAPTracker.sol
|
|
|
|
|
|
\`\`\`solidity
|
|
|
|
|
|
${SOL_VWAP}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### PriceOracle.sol
|
|
|
|
|
|
\`\`\`solidity
|
|
|
|
|
|
${SOL_PRICE_ORACLE}
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
## Cast command patterns
|
|
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
### Check total LM ETH (run after each strategy)
|
|
|
|
|
|
Measures free ETH + free WETH + ETH locked in all 3 Uni V3 positions.
|
2026-03-09 03:28:10 +00:00
|
|
|
|
\`\`\`bash
|
2026-03-10 19:11:11 +00:00
|
|
|
|
CAST=/home/debian/.foundry/bin/cast
|
|
|
|
|
|
LM_ETH=\$(\$CAST balance ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
LM_WETH=\$(\$CAST call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
SLOT0=\$(\$CAST call ${POOL} "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" --rpc-url http://localhost:8545)
|
|
|
|
|
|
CUR_TICK=\$(echo "\$SLOT0" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
TOKEN0_IS_WETH=\$(python3 -c "print(1 if '${WETH}'.lower() < '${KRK}'.lower() else 0)")
|
|
|
|
|
|
POS_ETH=0
|
|
|
|
|
|
for STAGE in 0 1 2; do
|
|
|
|
|
|
POS=\$(\$CAST call ${LM} "positions(uint8)(uint128,int24,int24)" \$STAGE --rpc-url http://localhost:8545)
|
|
|
|
|
|
LIQ=\$(echo "\$POS" | sed -n '1p' | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
TL=\$(echo "\$POS" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
TU=\$(echo "\$POS" | sed -n '3p' | sed 's/\[.*//;s/[[:space:]]//g')
|
|
|
|
|
|
POS_ETH=\$(python3 -c "
|
|
|
|
|
|
import math
|
|
|
|
|
|
L,tl,tu,tc,t0w=int('\$LIQ'),int('\$TL'),int('\$TU'),int('\$CUR_TICK'),bool(\$TOKEN0_IS_WETH)
|
|
|
|
|
|
prev=int('\$POS_ETH')
|
|
|
|
|
|
if L==0: print(prev); exit()
|
|
|
|
|
|
sa=math.sqrt(1.0001**tl); sb=math.sqrt(1.0001**tu); sc=math.sqrt(1.0001**tc)
|
|
|
|
|
|
if t0w:
|
|
|
|
|
|
e=L*(1/sa-1/sb) if tc<tl else (0 if tc>=tu else L*(1/sc-1/sb))
|
|
|
|
|
|
else:
|
|
|
|
|
|
e=L*(sb-sa) if tc>=tu else (0 if tc<tl else L*(sc-sa))
|
|
|
|
|
|
print(prev+int(e))
|
|
|
|
|
|
")
|
|
|
|
|
|
done
|
|
|
|
|
|
TOTAL=\$(python3 -c "print(int('\$LM_ETH')+int('\$LM_WETH')+int('\$POS_ETH'))")
|
|
|
|
|
|
echo "Total LM ETH: \$TOTAL wei (free: \$LM_ETH + \$LM_WETH, positions: \$POS_ETH)"
|
|
|
|
|
|
echo "Started with: ${LM_ETH_BEFORE} wei"
|
2026-03-14 03:07:55 +00:00
|
|
|
|
python3 -c "b=${LM_ETH_BEFORE:-0}; a=int('\$TOTAL'); d=b-a; print(f'Delta: {d} wei ({d*100//b if b else 0}% extracted)' if d>0 else f'Delta: {d} wei (LM gained ETH)')"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Wrap ETH to WETH
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${WETH} "deposit()" --value 100ether \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Approve token spend
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send <TOKEN> "approve(address,uint256)" <SPENDER> \
|
|
|
|
|
|
115792089237316195423570985008687907853269984665640564039457584007913129639935 \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Buy KRK (WETH → KRK via SwapRouter)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
# Must wrap ETH and approve WETH first
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${SWAP_ROUTER} \
|
|
|
|
|
|
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
|
|
|
|
|
|
"(${WETH},${KRK},${POOL_FEE},${ADV_ADDR},<WETH_AMOUNT>,0,0)" \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Sell KRK (KRK → WETH via SwapRouter)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
# Must approve KRK first
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${SWAP_ROUTER} \
|
|
|
|
|
|
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
|
|
|
|
|
|
"(${KRK},${WETH},${POOL_FEE},${ADV_ADDR},<KRK_AMOUNT>,0,0)" \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Stake KRK (snatch with no snatching)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
# Approve KRK to Stake first
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${STAKE} \
|
|
|
|
|
|
"snatch(uint256,address,uint32,uint256[])" \
|
|
|
|
|
|
<KRK_AMOUNT> ${ADV_ADDR} 0 "[]" \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Unstake KRK
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${STAKE} \
|
|
|
|
|
|
"exitPosition(uint256)" <POSITION_ID> \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
2026-03-15 10:47:36 +00:00
|
|
|
|
### Advance time (REQUIRED before each recenter call)
|
|
|
|
|
|
recenter() has a 60-second cooldown AND requires 300s of TWAP oracle history.
|
|
|
|
|
|
You MUST advance time before calling recenter:
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast rpc evm_increaseTime 600 --rpc-url http://localhost:8545
|
|
|
|
|
|
for i in \$(seq 1 10); do /home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545; done
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
### Trigger recenter (account 2 only)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${LM} "recenter()" \
|
|
|
|
|
|
--private-key ${RECENTER_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Read KRK balance
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${ADV_ADDR} \
|
|
|
|
|
|
--rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Read ETH balance
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast balance ${ADV_ADDR} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Add LP position via NPM (mint)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
# Must approve both tokens to NPM first. tickLower/tickUpper must be multiples of 200 (pool tickSpacing).
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${NPM} \
|
|
|
|
|
|
"mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))" \
|
|
|
|
|
|
"(${WETH},${KRK},${POOL_FEE},<TICK_LOWER>,<TICK_UPPER>,<AMOUNT0>,<AMOUNT1>,0,0,${ADV_ADDR},<DEADLINE>)" \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Remove LP position via NPM (decreaseLiquidity then collect)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${NPM} \
|
|
|
|
|
|
"decreaseLiquidity((uint256,uint128,uint256,uint256,uint256))" \
|
|
|
|
|
|
"(<TOKEN_ID>,<LIQUIDITY>,0,0,<DEADLINE>)" \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
|
|
|
|
|
|
/home/debian/.foundry/bin/cast send ${NPM} \
|
|
|
|
|
|
"collect((uint256,address,uint128,uint128))" \
|
|
|
|
|
|
"(<TOKEN_ID>,${ADV_ADDR},340282366920938463463374607431768211455,340282366920938463463374607431768211455)" \
|
|
|
|
|
|
--private-key ${ADV_PK} --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Mine a block
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
/home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
### Snapshot and revert (for resetting between strategies)
|
|
|
|
|
|
\`\`\`bash
|
|
|
|
|
|
# Take snapshot (returns ID — save it):
|
|
|
|
|
|
SNAP=\$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://localhost:8545 | tr -d '"')
|
|
|
|
|
|
# Revert to snapshot (one-shot — take a new snapshot immediately after):
|
|
|
|
|
|
/home/debian/.foundry/bin/cast rpc anvil_revert \$SNAP --rpc-url http://localhost:8545
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
## Constraints
|
|
|
|
|
|
|
|
|
|
|
|
- **feeDestination = LM itself** — fees are NOT extracted, they accrue as LM liquidity.
|
|
|
|
|
|
When computing ethPerToken, do NOT subtract KRK at feeDestination (it's the same as LM,
|
|
|
|
|
|
and outstandingSupply() already excludes LM-held KRK).
|
|
|
|
|
|
- **LM has ~1000 ETH reserve** — proportional to your 10,000 ETH (10:1 ratio). This is a
|
|
|
|
|
|
realistic attack scenario, not an empty vault.
|
|
|
|
|
|
- **You MUST NOT call anvil_reset, anvil_setCode, or anvil_setStorageAt.** These are infra
|
|
|
|
|
|
cheats that invalidate the test. Use only swap/stake/LP/recenter protocol operations.
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
## Rules
|
|
|
|
|
|
|
2026-03-10 20:01:12 +00:00
|
|
|
|
1. You have ~9000 ETH (after funding LM with 1000 ETH). Start by wrapping some if you need WETH for swaps.
|
2026-03-10 19:11:11 +00:00
|
|
|
|
2. Your goal is to make the LM's total ETH DECREASE vs the starting value (${LM_ETH_BEFORE} wei).
|
2026-03-09 03:28:10 +00:00
|
|
|
|
3. Try at least 3 distinct strategies. After each attempt:
|
2026-03-10 19:11:11 +00:00
|
|
|
|
a. Run the total LM ETH check command above.
|
|
|
|
|
|
b. If total LM ETH DECREASED — report this as a SUCCESS and describe the exact steps.
|
|
|
|
|
|
c. If LM ETH held or INCREASED — revert to the snapshot and try a new strategy.
|
2026-03-09 03:28:10 +00:00
|
|
|
|
Remember: \`anvil_revert\` is one-shot. Take a new snapshot immediately after reverting.
|
|
|
|
|
|
4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell).
|
|
|
|
|
|
5. Be methodical. Report every strategy tried even if it failed.
|
2026-03-09 09:23:37 +00:00
|
|
|
|
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.
|
2026-03-10 19:11:11 +00:00
|
|
|
|
8. Start executing immediately. No lengthy planning — act, measure, iterate.
|
2026-03-15 15:23:43 +00:00
|
|
|
|
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?
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-09 09:23:37 +00:00
|
|
|
|
${MEMORY_SECTION}
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
## Final report format
|
|
|
|
|
|
|
|
|
|
|
|
After trying all strategies, output a clearly structured report:
|
|
|
|
|
|
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
=== RED-TEAM REPORT ===
|
|
|
|
|
|
|
2026-03-15 15:23:43 +00:00
|
|
|
|
Candidate: ${CANDIDATE_NAME}
|
|
|
|
|
|
Optimizer Profile: ${OPTIMIZER_PROFILE}
|
2026-03-10 19:11:11 +00:00
|
|
|
|
lm_eth_before: <value> wei (total: free + positions)
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
STRATEGY 1: <name>
|
2026-03-15 15:23:43 +00:00
|
|
|
|
Pattern: <abstract op sequence e.g. "buy → recenter → sell">
|
2026-03-09 03:28:10 +00:00
|
|
|
|
Steps: <what you did>
|
2026-03-10 19:11:11 +00:00
|
|
|
|
lm_eth_after: <value> wei
|
|
|
|
|
|
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
|
2026-03-15 15:23:43 +00:00
|
|
|
|
Insight: <WHY this worked/failed given the optimizer profile>
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
|
|
|
|
|
STRATEGY 2: ...
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
=== CONCLUSION ===
|
2026-03-10 19:11:11 +00:00
|
|
|
|
ETH extracted: YES / NO
|
2026-03-09 03:28:10 +00:00
|
|
|
|
Winning strategy: <describe if YES, else "None">
|
2026-03-15 15:23:43 +00:00
|
|
|
|
Universal pattern: <would this likely work on other candidates? Why or why not?>
|
2026-03-10 19:11:11 +00:00
|
|
|
|
lm_eth_before: ${LM_ETH_BEFORE} wei
|
|
|
|
|
|
lm_eth_after: <final value> wei
|
2026-03-09 03:28:10 +00:00
|
|
|
|
\`\`\`
|
|
|
|
|
|
PROMPT_EOF
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ── 7. Create output directory and run the agent ───────────────────────────────
|
|
|
|
|
|
mkdir -p "$REPORT_DIR"
|
|
|
|
|
|
|
|
|
|
|
|
log "Spawning Claude red-team agent (timeout: ${CLAUDE_TIMEOUT}s)..."
|
|
|
|
|
|
log " Report will be written to: $REPORT"
|
|
|
|
|
|
|
|
|
|
|
|
set +e
|
2026-03-09 10:00:56 +00:00
|
|
|
|
# Note: --verbose is required by the claude CLI when --output-format stream-json is used;
|
|
|
|
|
|
# omitting it causes the CLI to exit with an error, producing an empty stream log.
|
2026-03-09 03:59:12 +00:00
|
|
|
|
timeout "$CLAUDE_TIMEOUT" claude -p --dangerously-skip-permissions \
|
2026-03-09 09:23:37 +00:00
|
|
|
|
--verbose --output-format stream-json \
|
2026-03-13 09:48:34 +00:00
|
|
|
|
"$PROMPT" >"$STREAM_LOG" 2>&1 &
|
|
|
|
|
|
CLAUDE_PID=$!
|
|
|
|
|
|
wait "$CLAUDE_PID"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
AGENT_EXIT=$?
|
2026-03-13 09:48:34 +00:00
|
|
|
|
CLAUDE_PID=""
|
2026-03-09 03:28:10 +00:00
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
|
|
if [[ $AGENT_EXIT -ne 0 ]]; then
|
2026-03-09 09:23:37 +00:00
|
|
|
|
log "WARNING: claude exited with code $AGENT_EXIT — see $STREAM_LOG for details"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
fi
|
|
|
|
|
|
|
2026-03-09 09:23:37 +00:00
|
|
|
|
# Extract readable text from stream-json for the report
|
|
|
|
|
|
python3 - "$STREAM_LOG" >"$REPORT" <<'PYEOF'
|
|
|
|
|
|
import json, sys
|
|
|
|
|
|
with open(sys.argv[1]) as f:
|
|
|
|
|
|
for line in f:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if not line:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
obj = json.loads(line)
|
|
|
|
|
|
if obj.get("type") == "assistant":
|
|
|
|
|
|
for block in obj.get("message", {}).get("content", []):
|
|
|
|
|
|
if block.get("type") == "text":
|
|
|
|
|
|
print(block["text"], end="")
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
PYEOF
|
|
|
|
|
|
|
2026-03-09 10:00:56 +00:00
|
|
|
|
# If the agent crashed and produced no readable output, treat as an infra error
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# rather than silently reporting ETH SAFE (a false pass).
|
2026-03-09 10:00:56 +00:00
|
|
|
|
if [[ $AGENT_EXIT -ne 0 && ! -s "$REPORT" ]]; then
|
|
|
|
|
|
die "claude agent failed (exit $AGENT_EXIT) with no readable output — see $STREAM_LOG"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-03-10 19:11:11 +00:00
|
|
|
|
# ── 8. Read lm_eth_after ────────────────────────────────────────────────────────
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log "Reading floor after agent run..."
|
2026-03-10 19:11:11 +00:00
|
|
|
|
LM_ETH_AFTER=$(compute_lm_total_eth)
|
2026-03-09 09:23:37 +00:00
|
|
|
|
|
|
|
|
|
|
# ── 8a. Extract and persist strategy findings ──────────────────────────────────
|
|
|
|
|
|
log "Extracting strategy findings from agent output..."
|
|
|
|
|
|
extract_memory "$STREAM_LOG"
|
2026-03-10 19:11:11 +00:00
|
|
|
|
log " lm_eth_after = $LM_ETH_AFTER wei"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
|
2026-03-11 02:08:06 +00:00
|
|
|
|
# ── 8b. Export attack sequence and replay with AttackRunner ────────────────────
|
|
|
|
|
|
# Converts the agent's cast send commands to structured JSONL and replays them
|
|
|
|
|
|
# via AttackRunner.s.sol to capture full state snapshots for optimizer training.
|
|
|
|
|
|
log "Exporting attack sequence from stream log..."
|
|
|
|
|
|
set +e
|
|
|
|
|
|
python3 "$REPO_ROOT/scripts/harb-evaluator/export-attacks.py" \
|
|
|
|
|
|
"$STREAM_LOG" "$ATTACK_EXPORT" 2>&1 | while IFS= read -r line; do log " $line"; done
|
|
|
|
|
|
EXPORT_EXIT=${PIPESTATUS[0]}
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
|
|
if [[ $EXPORT_EXIT -eq 0 && -f "$ATTACK_EXPORT" && -s "$ATTACK_EXPORT" ]]; then
|
|
|
|
|
|
log " Attack export: $ATTACK_EXPORT"
|
|
|
|
|
|
log " Replaying attack sequence with AttackRunner for state snapshots..."
|
|
|
|
|
|
set +e
|
|
|
|
|
|
(cd "$REPO_ROOT/onchain" && \
|
|
|
|
|
|
ATTACK_FILE="$ATTACK_EXPORT" \
|
|
|
|
|
|
DEPLOYMENTS_FILE="deployments-local.json" \
|
|
|
|
|
|
"$FORGE" script script/backtesting/AttackRunner.s.sol \
|
|
|
|
|
|
--rpc-url "$RPC_URL" --broadcast 2>&1 \
|
|
|
|
|
|
| grep '^{' >"$ATTACK_SNAPSHOTS")
|
|
|
|
|
|
REPLAY_EXIT=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
if [[ $REPLAY_EXIT -eq 0 && -s "$ATTACK_SNAPSHOTS" ]]; then
|
|
|
|
|
|
SNAPSHOT_COUNT=$(wc -l <"$ATTACK_SNAPSHOTS")
|
|
|
|
|
|
log " AttackRunner replay complete: $SNAPSHOT_COUNT snapshots → $ATTACK_SNAPSHOTS"
|
|
|
|
|
|
else
|
|
|
|
|
|
log " WARNING: AttackRunner replay produced no snapshots (exit $REPLAY_EXIT) — non-fatal"
|
|
|
|
|
|
fi
|
|
|
|
|
|
# Revert to the clean baseline after replay so the floor check below is unaffected.
|
|
|
|
|
|
"$CAST" rpc anvil_revert "$SNAP" --rpc-url "$RPC_URL" >/dev/null 2>&1 || true
|
|
|
|
|
|
# Re-take the snapshot so cleanup trap still has a valid ID to revert.
|
|
|
|
|
|
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
|
|
|
|
|
|
else
|
|
|
|
|
|
log " WARNING: No attack operations exported from stream — skipping AttackRunner replay"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-03-09 03:28:10 +00:00
|
|
|
|
# ── 9. Summarise results ───────────────────────────────────────────────────────
|
|
|
|
|
|
log ""
|
|
|
|
|
|
log "=== RED-TEAM SUMMARY ==="
|
|
|
|
|
|
log ""
|
2026-03-10 19:11:11 +00:00
|
|
|
|
log " lm_eth_before : $LM_ETH_BEFORE wei"
|
|
|
|
|
|
log " lm_eth_after : $LM_ETH_AFTER wei"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log ""
|
|
|
|
|
|
|
|
|
|
|
|
BROKE=false
|
2026-03-13 08:28:26 +00:00
|
|
|
|
if python3 -c "import sys; sys.exit(0 if int('${LM_ETH_AFTER:-0}') < int('${LM_ETH_BEFORE:-0}') else 1)"; then
|
2026-03-09 03:28:10 +00:00
|
|
|
|
BROKE=true
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "$BROKE" == "true" ]]; then
|
2026-03-13 08:28:26 +00:00
|
|
|
|
DELTA=$(python3 -c "print(int('${LM_ETH_BEFORE:-0}') - int('${LM_ETH_AFTER:-0}'))")
|
2026-03-10 19:11:11 +00:00
|
|
|
|
log " RESULT: ETH EXTRACTED ❌"
|
|
|
|
|
|
log " Decrease: $DELTA wei"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log ""
|
|
|
|
|
|
log " See $REPORT for the winning strategy."
|
|
|
|
|
|
log ""
|
|
|
|
|
|
# Append a machine-readable summary to the report
|
|
|
|
|
|
cat >>"$REPORT" <<SUMMARY_EOF
|
|
|
|
|
|
|
|
|
|
|
|
=== RUNNER SUMMARY ===
|
2026-03-10 19:11:11 +00:00
|
|
|
|
lm_eth_before : $LM_ETH_BEFORE
|
|
|
|
|
|
lm_eth_after : $LM_ETH_AFTER
|
2026-03-09 03:28:10 +00:00
|
|
|
|
delta : -$DELTA
|
2026-03-10 19:11:11 +00:00
|
|
|
|
verdict : ETH_EXTRACTED
|
2026-03-09 03:28:10 +00:00
|
|
|
|
SUMMARY_EOF
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
else
|
2026-03-10 19:11:11 +00:00
|
|
|
|
log " RESULT: ETH SAFE ✅"
|
2026-03-09 03:28:10 +00:00
|
|
|
|
log ""
|
|
|
|
|
|
log " See $REPORT for strategies attempted."
|
|
|
|
|
|
log ""
|
|
|
|
|
|
cat >>"$REPORT" <<SUMMARY_EOF
|
|
|
|
|
|
|
|
|
|
|
|
=== RUNNER SUMMARY ===
|
2026-03-10 19:11:11 +00:00
|
|
|
|
lm_eth_before : $LM_ETH_BEFORE
|
|
|
|
|
|
lm_eth_after : $LM_ETH_AFTER
|
2026-03-09 03:28:10 +00:00
|
|
|
|
delta : 0 (or increase)
|
2026-03-10 19:11:11 +00:00
|
|
|
|
verdict : ETH_SAFE
|
2026-03-09 03:28:10 +00:00
|
|
|
|
SUMMARY_EOF
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|