harb/tools/push3-evolution/fitness.sh
openhands a8db761de8 fix: Push3 evolution: fitness scoring wrapper (transpile → deploy → attack → score) (#545)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:02:00 +00:00

280 lines
10 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# =============================================================================
# fitness.sh — Push3 optimizer fitness scoring wrapper
#
# Pipeline: Push3 candidate → transpile → compile → deploy/upgrade → attack ×N → score
#
# Usage:
# ./tools/push3-evolution/fitness.sh <candidate.push3>
#
# Output:
# Single integer on stdout — total lm_eth_total across all attacks (wei).
#
# Exit codes:
# 0 Success — score printed to stdout.
# 1 Invalid candidate — Push3 program won't transpile, compile, or deploy.
# 2 Infra error — Anvil unavailable, missing tool, bootstrap failure.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ONCHAIN_DIR="$REPO_ROOT/onchain"
ATTACKS_DIR="$ONCHAIN_DIR/script/backtesting/attacks"
RPC_URL="http://localhost:8545"
# Standard Anvil test accounts (deterministic mnemonic)
MNEMONIC="test test test test test test test test test test test junk"
# Account 2 — recenter caller
RECENTER_PK="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
# Account 8 — adversary (used to fund LM with WETH)
ADV_PK="0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97"
# WETH address on the local Anvil Base fork
WETH="0x4200000000000000000000000000000000000006"
# =============================================================================
# Argument parsing
# =============================================================================
if [ $# -ne 1 ]; then
echo "Usage: $0 <candidate.push3>" >&2
exit 2
fi
PUSH3_FILE="$1"
if [ ! -f "$PUSH3_FILE" ]; then
echo "Error: File not found: $PUSH3_FILE" >&2
exit 1
fi
# Canonicalise so relative paths work after cwd changes.
PUSH3_FILE="$(cd "$(dirname "$PUSH3_FILE")" && pwd)/$(basename "$PUSH3_FILE")"
# =============================================================================
# Helpers
# =============================================================================
log() { echo " [fitness] $*" >&2; }
fail1() { echo " [invalid] $*" >&2; exit 1; }
fail2() { echo " [infra] $*" >&2; exit 2; }
# =============================================================================
# Tool check
# =============================================================================
for _tool in forge cast anvil python3; do
command -v "$_tool" &>/dev/null || fail2 "$_tool not found in PATH"
done
# =============================================================================
# Cleanup
# =============================================================================
ANVIL_PID=""
WORK_DIR="$(mktemp -d)"
cleanup() {
[ -n "$ANVIL_PID" ] && kill "$ANVIL_PID" 2>/dev/null || true
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
# =============================================================================
# Step 0 — Start Anvil (if not already running)
# =============================================================================
if cast chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1; then
log "Anvil already running at $RPC_URL"
else
anvil --silent \
--mnemonic "$MNEMONIC" \
--port 8545 &
ANVIL_PID=$!
TRIES=0
until cast chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1; do
TRIES=$((TRIES + 1))
[ $TRIES -gt 50 ] && fail2 "Anvil did not start within 50 attempts"
sleep 0.2
done
log "Anvil started (PID $ANVIL_PID)"
fi
# =============================================================================
# Steps 13 — Transpile → compile → deploy fresh stack → UUPS upgrade
#
# deploy-optimizer.sh handles the full pipeline. With no OPTIMIZER_PROXY set it
# also runs DeployLocal.sol to produce the initial stack.
#
# Exit codes from deploy-optimizer.sh all map to exit 1 (invalid candidate)
# because transpile / compile / round-trip failures are candidate issues.
# =============================================================================
log "Running deploy-optimizer.sh (transpile → compile → deploy → upgrade)…"
DEPLOY_LOG="$WORK_DIR/deploy.log"
DEPLOY_EC=0
"$REPO_ROOT/tools/deploy-optimizer.sh" "$PUSH3_FILE" >"$DEPLOY_LOG" 2>&1 || DEPLOY_EC=$?
if [ "$DEPLOY_EC" -ne 0 ]; then
# Surface the deploy log so operators can diagnose candidate failures.
cat "$DEPLOY_LOG" >&2
fail1 "deploy-optimizer.sh failed (exit $DEPLOY_EC)"
fi
log "Optimizer deployed and upgraded"
# =============================================================================
# Step 4 — Read deployment addresses
#
# DeployLocal.sol writes to onchain/deployments-local.json; addresses are
# deterministic for a fresh Anvil + standard mnemonic.
# =============================================================================
DEPLOYMENTS="$ONCHAIN_DIR/deployments-local.json"
[ -f "$DEPLOYMENTS" ] || fail2 "deployments-local.json not found — did DeployLocal.sol run?"
LM_ADDR=$(python3 -c "
import json
d = json.load(open('$DEPLOYMENTS'))
print(d['contracts']['LiquidityManager'])
" 2>/dev/null) || fail2 "Failed to read LiquidityManager from deployments-local.json"
[ -n "$LM_ADDR" ] || fail2 "LiquidityManager address is empty in deployments-local.json"
log "LiquidityManager: $LM_ADDR"
# =============================================================================
# Step 5 — Bootstrap LM state
#
# a. Grant recenterAccess to the standard Anvil account 2 (impersonate feeDestination).
# b. Fund LM with 1000 WETH from the adversary account (account 8).
# c. Call recenter() to deploy the capital into Uniswap positions so attacks
# have something meaningful to work against. The LM needs at least some
# TWAP history; mine blocks and retry until recenter succeeds.
# =============================================================================
RECENTER_ADDR=$(cast wallet address --private-key "$RECENTER_PK")
FEE_DEST=$(cast call "$LM_ADDR" "feeDestination()(address)" \
--rpc-url "$RPC_URL" 2>/dev/null | sed 's/\[.*//;s/[[:space:]]//g') \
|| fail2 "Failed to read feeDestination() from LM"
log "Granting recenterAccess to $RECENTER_ADDR (via feeDestination $FEE_DEST)"
cast rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" >/dev/null
cast send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \
"$LM_ADDR" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \
|| fail2 "setRecenterAccess failed"
cast rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" >/dev/null
log "Funding LM with 1000 WETH"
cast send "$WETH" "deposit()" --value 1000ether \
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| fail2 "Failed to wrap ETH to WETH"
cast send "$WETH" "transfer(address,uint256)" "$LM_ADDR" 1000000000000000000000 \
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| fail2 "Failed to transfer WETH to LM"
log "Initial recenter — deploying capital into positions"
RECENTERED=false
for _attempt in 1 2 3 4; do
# Mine 50 blocks each attempt to accumulate TWAP history.
for _b in $(seq 1 50); do
cast rpc evm_mine --rpc-url "$RPC_URL" >/dev/null 2>&1
done
if cast send "$LM_ADDR" "recenter()" \
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1; then
RECENTERED=true
break
fi
done
if ! $RECENTERED; then
log "WARNING: initial recenter did not succeed — attack scores may be lower than expected"
fi
# =============================================================================
# Step 6 — Take base Anvil snapshot
#
# All attacks revert to this snapshot so they each start from the same state.
# =============================================================================
BASE_SNAP=$(cast rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
log "Base snapshot: $BASE_SNAP"
# =============================================================================
# Steps 78 — Run each attack and accumulate lm_eth_total
# =============================================================================
TOTAL_ETH=0
ATTACK_COUNT=0
for ATTACK_JSONL in "$ATTACKS_DIR"/*.jsonl; do
[ -f "$ATTACK_JSONL" ] || continue
ATTACK_NAME="$(basename "$ATTACK_JSONL" .jsonl)"
log "Running attack: $ATTACK_NAME"
# a. Take per-attack snapshot (identical to base on first iteration;
# on subsequent iterations the state is already back at base from the
# previous revert).
ATK_SNAP=$(cast rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
# b. Run AttackRunner, capturing all output (console.log snapshots come via
# stdout when using --broadcast; stderr carries compilation noise).
ATK_OUT="$WORK_DIR/atk-${ATTACK_NAME}.txt"
ATK_EC=0
(
cd "$ONCHAIN_DIR"
ATTACK_FILE="$ATTACK_JSONL" \
forge script script/backtesting/AttackRunner.s.sol \
--rpc-url "$RPC_URL" --broadcast --no-color 2>&1
) >"$ATK_OUT" || ATK_EC=$?
if [ "$ATK_EC" -ne 0 ]; then
log " WARNING: AttackRunner failed for $ATTACK_NAME (exit $ATK_EC) — skipping"
cast rpc anvil_revert "$ATK_SNAP" --rpc-url "$RPC_URL" >/dev/null 2>&1 || true
continue
fi
# c. Extract lm_eth_total from the final JSON snapshot.
# Snapshot lines are emitted by console.log and start with '{'.
ETH_RETAINED=$(python3 - "$ATK_OUT" <<'PYEOF'
import sys, json
snapshots = []
with open(sys.argv[1]) as f:
for line in f:
line = line.strip()
if line.startswith('{') and '"lm_eth_total"' in line:
try:
snapshots.append(json.loads(line))
except json.JSONDecodeError:
pass
if snapshots:
# lm_eth_total is a quoted integer string in the snapshot JSON.
val = snapshots[-1]['lm_eth_total']
print(int(val) if isinstance(val, str) else val)
else:
print(0)
PYEOF
)
log " $ATTACK_NAME: lm_eth_total=$ETH_RETAINED"
TOTAL_ETH=$(python3 -c "print(int('$TOTAL_ETH') + int('$ETH_RETAINED'))")
ATTACK_COUNT=$((ATTACK_COUNT + 1))
# d. Revert to per-attack snapshot — resets Anvil state to post-bootstrap
# baseline so the next attack starts from the same conditions.
cast rpc anvil_revert "$ATK_SNAP" --rpc-url "$RPC_URL" >/dev/null 2>&1 || true
done
# =============================================================================
# Output
# =============================================================================
if [ "$ATTACK_COUNT" -eq 0 ]; then
fail2 "No attacks ran — check $ATTACKS_DIR for *.jsonl files"
fi
log "Score: $TOTAL_ETH wei (sum of lm_eth_total across $ATTACK_COUNT attacks)"
echo "$TOTAL_ETH"