diff --git a/containers/bootstrap.sh b/containers/bootstrap.sh index 0160018..4130b59 100755 --- a/containers/bootstrap.sh +++ b/containers/bootstrap.sh @@ -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 diff --git a/onchain/script/backtesting/AttackRunner.s.sol b/onchain/script/backtesting/AttackRunner.s.sol index 88d5b92..c5640f4 100644 --- a/onchain/script/backtesting/AttackRunner.s.sol +++ b/onchain/script/backtesting/AttackRunner.s.sol @@ -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 ────────────────────────────────────────────────── diff --git a/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl b/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl new file mode 100644 index 0000000..0f0660b --- /dev/null +++ b/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl @@ -0,0 +1,3 @@ +{"op":"buy","amount":"31900000000000000000"} +{"op":"recenter"} +{"op":"sell","amount":"all","token":"KRK"} diff --git a/scripts/harb-evaluator/bootstrap-light.sh b/scripts/harb-evaluator/bootstrap-light.sh new file mode 100755 index 0000000..425ef24 --- /dev/null +++ b/scripts/harb-evaluator/bootstrap-light.sh @@ -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" diff --git a/scripts/harb-evaluator/red-team.sh b/scripts/harb-evaluator/red-team.sh index 2c0eeff..8aae354 100755 --- a/scripts/harb-evaluator/red-team.sh +++ b/scripts/harb-evaluator/red-team.sh @@ -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 " " — 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)\`