fix: bootstrap + red-team on forked networks

Bootstrap fixes:
- Idempotency check: skip if Kraiken already deployed on Anvil
- anvil_setCode to strip ERC-4337 code from deployer + feeDest
- DeployLocal.sol: feeDest derived from keccak256('harb.local.feeDest')

Red-team fixes:
- New bootstrap-light.sh: Anvil-only, ~30s deploy
- red-team.sh uses bootstrap-light instead of full docker compose
- anvil_setBalance for feeDest before impersonation
- forge --color never, path resolution, docker chown

Address fixes (all Base mainnet, in both FitnessEvaluator + AttackRunner):
- V3_FACTORY: 0x33128a8fC17869897dcE68Ed026d694621f6FDfD
- SWAP_ROUTER: 0x2626664c2603336E57B271c5C0b26F421741e481
- NPM_ADDR: 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1
This commit is contained in:
openhands 2026-03-13 11:55:22 +00:00
parent 8607c097eb
commit dbf78de793
5 changed files with 208 additions and 61 deletions

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Lightweight bootstrap for red-team / evaluator use.
# Starts only Anvil + deploys contracts. No ponder, no webapp, no txnbot.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
ONCHAIN_DIR="$REPO_ROOT/onchain"
RPC_URL="http://localhost:8545"
CAST="$HOME/.foundry/bin/cast"
FORGE="$HOME/.foundry/bin/forge"
log() { echo "[bootstrap-light] $*"; }
die() { log "ERROR: $*" >&2; exit 1; }
# 1. Start Anvil (docker)
log "Starting Anvil..."
cd "$REPO_ROOT"
sudo docker compose down -v 2>/dev/null || true
sudo docker compose up -d anvil
for i in $(seq 1 30); do
$CAST chain-id --rpc-url "$RPC_URL" 2>/dev/null && break
sleep 1
done
$CAST chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 || die "Anvil not responding"
log "Anvil running"
# 2. Clear ERC-4337 code from well-known addresses (fork safety)
DEPLOYER=$($CAST wallet address --mnemonic "test test test test test test test test test test test junk" 2>/dev/null)
log "Clearing code from deployer ($DEPLOYER) + feeDest"
$CAST rpc --rpc-url "$RPC_URL" anvil_setCode "$DEPLOYER" "0x" 2>/dev/null || true
$CAST rpc --rpc-url "$RPC_URL" anvil_setCode "0x8A9145E1Ea4C4d7FB08cF1011c8ac1F0e10F9383" "0x" 2>/dev/null || true
# 3. Deploy contracts — capture output for addresses
log "Deploying contracts..."
cd "$ONCHAIN_DIR"
# Fix ownership of forge artifacts (docker creates root-owned files)
sudo chown -R "$(id -u):$(id -g)" cache out broadcast 2>/dev/null || true
rm -f deployments-local.json # force fresh
DEPLOY_OUT=$($FORGE script script/DeployLocal.sol --rpc-url "$RPC_URL" --broadcast 2>&1)
echo "$DEPLOY_OUT" | grep -E "^\[|deployed|complete|Summary" || true
# 4. Extract addresses from output and write deployments-local.json
KRK=$(echo "$DEPLOY_OUT" | grep -oP 'Kraiken deployed: \K0x[a-fA-F0-9]+')
STAKE=$(echo "$DEPLOY_OUT" | grep -oP 'Stake deployed: \K0x[a-fA-F0-9]+')
OPT=$(echo "$DEPLOY_OUT" | grep -oP 'Optimizer deployed: \K0x[a-fA-F0-9]+')
LM=$(echo "$DEPLOY_OUT" | grep -oP 'LiquidityManager deployed: \K0x[a-fA-F0-9]+')
[[ -n "$LM" ]] || die "Could not extract LiquidityManager address from deploy output"
cat > "$ONCHAIN_DIR/deployments-local.json" << EOF
{
"contracts": {
"Kraiken": "$KRK",
"Stake": "$STAKE",
"LiquidityManager": "$LM",
"OptimizerProxy": "$OPT"
}
}
EOF
# 5. Verify
VWAP=$($CAST call --rpc-url "$RPC_URL" "$LM" "cumulativeVolume()(uint256)" 2>/dev/null || echo "0")
log "LiquidityManager: $LM"
log "cumulativeVolume: $VWAP"
[[ "$VWAP" != "0" ]] && log "✅ Bootstrap complete — VWAP active" || log "⚠️ VWAP not bootstrapped"

View file

@ -33,7 +33,7 @@ DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json"
# ── Anvil accounts ─────────────────────────────────────────────────────────────
# Account 8 — adversary (10k ETH, 0 KRK)
ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
# Account 2 — recenter caller (recenter() is permissionless; any account can call it)
# Account 2 — recenter caller (granted recenterAccess by bootstrap)
RECENTER_PK=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
# ── Infrastructure constants ───────────────────────────────────────────────────
@ -55,39 +55,14 @@ command -v claude &>/dev/null || die "claude CLI not found (install: npm i -g
command -v python3 &>/dev/null || die "python3 not found"
command -v jq &>/dev/null || die "jq not found"
# ── 1. Fresh stack — tear down, rebuild, wait for bootstrap ────────────────────
log "Rebuilding fresh stack ..."
cd "$REPO_ROOT"
# Free RAM: drop caches
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' 2>/dev/null || true
# Tear down completely (volumes too — clean anvil state)
sudo -E docker compose down -v >/dev/null 2>&1 || true
sleep 3
# Bring up
# -E preserves FORK_URL (and other env vars) across the sudo boundary so that
# anvil-entrypoint.sh honours the caller's FORK_URL override.
sudo -E docker compose up -d >/dev/null 2>&1 \
|| die "docker compose up -d failed"
# Wait for bootstrap to complete (max 120s)
log "Waiting for bootstrap ..."
for i in $(seq 1 40); do
if sudo docker logs harb-bootstrap-1 2>&1 | grep -q "Bootstrap complete"; then
log " Bootstrap complete (${i}x3s)"
break
fi
if [[ $i -eq 40 ]]; then
die "Bootstrap did not complete within 120s"
fi
sleep 3
done
# ── 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"
# Verify Anvil responds
"$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| die "Anvil not accessible at $RPC_URL after stack start"
|| die "Anvil not accessible at $RPC_URL after bootstrap-light"
# ── 2. Read contract addresses ─────────────────────────────────────────────────
[[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)"
@ -119,8 +94,23 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \
"$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
log " Pool: $POOL"
# ── 3a. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
# recenter() is now permissionless — no setRecenterAccess() call needed.
# ── 3a. Grant recenterAccess FIRST (while original feeDestination is still set) ──
FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \
|| die "Failed to read feeDestination() from LM"
FEE_DEST=$(echo "$FEE_DEST" | sed 's/\[.*//;s/[[:space:]]//g')
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) via feeDestination ($FEE_DEST) ..."
# feeDest may be a keccak-derived address with zero balance — fund it for gas
"$CAST" rpc --rpc-url "$RPC_URL" anvil_setBalance "$FEE_DEST" "0xDE0B6B3A7640000" 2>/dev/null || true
"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \
|| die "anvil_impersonateAccount $FEE_DEST failed"
"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \
"$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \
|| die "setRecenterAccess($RECENTER_ADDR) failed"
"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \
|| die "anvil_stopImpersonatingAccount $FEE_DEST failed"
log " recenterAccess granted"
# ── 3b. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
# setFeeDestination allows repeated EOA sets; setting to a contract locks it permanently.
# The deployer (Anvil account 0) deployed LiquidityManager and may call setFeeDestination again.
# DEPLOYER_PK is Anvil's deterministic account-0 key — valid ONLY against a local ephemeral
@ -134,7 +124,7 @@ VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | s
log " feeDestination set to: $VERIFY"
[[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY"
# ── 3b. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
# ── 3c. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
# 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 ..."
@ -164,7 +154,7 @@ LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]
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"
# ── 4. Take Anvil snapshot (clean baseline) ────────────────────────────────────
# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ─────
log "Taking Anvil snapshot..."
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
log " Snapshot ID: $SNAP"
@ -190,9 +180,9 @@ trap cleanup EXIT INT TERM
# instead of multiple cast calls + Python float approximation.
compute_lm_total_eth() {
local output result
output=$(LM="$LM" WETH="$WETH" POOL="$POOL" \
/home/debian/.foundry/bin/forge script script/LmTotalEth.s.sol \
--rpc-url "$RPC_URL" --root "$REPO_ROOT/onchain" --no-color 2>&1)
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)
# forge script prints "== Logs ==" then " <value>" — extract the number
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)"
@ -409,7 +399,7 @@ CAST binary: /home/debian/.foundry/bin/cast
### Recenter caller — Anvil account 2
- Address: ${RECENTER_ADDR}
- Private key: ${RECENTER_PK}
- Can call recenter() (permissionless — 60s cooldown + TWAP check enforced)
- Has recenterAccess on LiquidityManager
---
@ -436,7 +426,7 @@ 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
recenter() is permissionless — any account can call it (subject to 60s cooldown and TWAP check).
Only recenterAccess account can call it.
### Staking
\`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\`