Merge pull request 'fix: bootstrap + red-team on forked networks' (#684) from fix/fitness-factory-address into master

This commit is contained in:
johba 2026-03-14 16:39:59 +01:00
commit 969c910b04
5 changed files with 115 additions and 45 deletions

View file

@ -130,7 +130,33 @@ main() {
bootstrap_log "Waiting for Anvil"
wait_for_rpc
# Idempotency: if deployments-local.json exists and contracts have code,
# bootstrap already ran against this Anvil instance — skip.
local deploy_file="$ONCHAIN_DIR/deployments-local.json"
if [[ -f "$deploy_file" ]]; then
local krk_addr
krk_addr=$(jq -r '.contracts.Kraiken // empty' "$deploy_file" 2>/dev/null || true)
if [[ -n "$krk_addr" ]]; then
local code
code=$(cast call --rpc-url "$ANVIL_RPC" "$krk_addr" "decimals()(uint8)" 2>/dev/null || true)
if [[ -n "$code" && "$code" != "0x" ]]; then
bootstrap_log "Already bootstrapped (Kraiken at $krk_addr responds) — skipping"
return 0
fi
fi
fi
maybe_set_deployer_from_mnemonic
# On forked networks, well-known addresses (Anvil mnemonic accounts) may
# have code (e.g. ERC-4337 Account Abstraction proxies on Base Sepolia).
# The feeDestination lock in LiquidityManager treats any address with code
# as a contract and locks permanently. Strip code so they behave as EOAs.
bootstrap_log "Clearing code from deployer + feeDest (fork safety)"
cast rpc --rpc-url "$ANVIL_RPC" anvil_setCode "$DEPLOYER_ADDR" "0x" 2>/dev/null || true
# feeDest = address(uint160(uint256(keccak256("harb.local.feeDest"))))
cast rpc --rpc-url "$ANVIL_RPC" anvil_setCode "0x8A9145E1Ea4C4d7FB08cF1011c8ac1F0e10F9383" "0x" 2>/dev/null || true
derive_txnbot_wallet
run_forge_script
extract_addresses

View file

@ -151,11 +151,9 @@ contract AttackRunner is Script {
uint24 internal constant POOL_FEE = 10_000;
address internal constant WETH = 0x4200000000000000000000000000000000000006;
address internal constant SWAP_ROUTER = 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4;
// Base mainnet NonfungiblePositionManager https://basescan.org/address/0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3
address internal constant NPM_ADDR = 0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3;
// Base mainnet Uniswap V3 Factory https://basescan.org/address/0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24
address internal constant V3_FACTORY = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
address internal constant SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;
address internal constant NPM_ADDR = 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1;
address internal constant V3_FACTORY = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD; // Base mainnet
// Anvil test accounts

View file

@ -0,0 +1,3 @@
{"op":"buy","amount":"31900000000000000000"}
{"op":"recenter"}
{"op":"sell","amount":"all","token":"KRK"}

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 (recenter is public, any account can call)
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,11 @@ 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. recenter() is now public (no recenterAccess needed) ──
# Any address can call recenter() — TWAP oracle enforces safety.
log "recenter() is public — no access grant needed"
# ── 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 +112,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 +142,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) ─────
log "Taking Anvil snapshot..."
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
log " Snapshot ID: $SNAP"
@ -190,9 +168,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 +387,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)
- Can call recenter() (public, TWAP-enforced)
---
@ -436,7 +414,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).
Any account can call it (public). TWAP oracle enforces safety.
### Staking
\`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\`