#!/usr/bin/env bash # run-protocol.sh — On-chain protocol health snapshot. # # Collects TVL, accumulated fees, position count, and rebalance frequency # from the deployed LiquidityManager. Writes evidence/protocol/YYYY-MM-DD.json. # # Exit codes: # 0 snapshot written successfully # 2 infrastructure error (RPC unreachable, missing deployments, forge unavailable) # # Environment: # RPC_URL Base network RPC endpoint (required) # DEPLOYMENTS_FILE path to deployments JSON (default: onchain/deployments-local.json) # LOOKBACK_BLOCKS blocks to scan for Recentered events (default: 7200, ~24 h on Base) set -euo pipefail CAST="${CAST:-/home/debian/.foundry/bin/cast}" FORGE="${FORGE:-/home/debian/.foundry/bin/forge}" REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" DATE="$(date -u +%Y-%m-%d)" OUT_DIR="$REPO_ROOT/evidence/protocol" OUT_FILE="$OUT_DIR/$DATE.json" RPC_URL="${RPC_URL:?RPC_URL is required}" DEPLOYMENTS_FILE="${DEPLOYMENTS_FILE:-onchain/deployments-local.json}" LOOKBACK_BLOCKS="${LOOKBACK_BLOCKS:-7200}" # Resolve relative deployments path against repo root if [[ "$DEPLOYMENTS_FILE" != /* ]]; then DEPLOYMENTS_FILE="$REPO_ROOT/$DEPLOYMENTS_FILE" fi die() { echo "ERROR: $*" >&2; exit 2; } mkdir -p "$OUT_DIR" # ── read-addresses ──────────────────────────────────────────────────────────── [[ -f "$DEPLOYMENTS_FILE" ]] || die "Deployments file not found: $DEPLOYMENTS_FILE" LM=$(jq -r '.contracts.LiquidityManager' "$DEPLOYMENTS_FILE") POOL=$(jq -r '.contracts.Pool' "$DEPLOYMENTS_FILE") WETH=0x4200000000000000000000000000000000000006 [[ -n "$LM" && "$LM" != "null" ]] || die "LiquidityManager address missing from $DEPLOYMENTS_FILE" [[ -n "$POOL" && "$POOL" != "null" ]] || die "Pool address missing from $DEPLOYMENTS_FILE" # ── collect block number ────────────────────────────────────────────────────── block_number=$("$CAST" block-number --rpc-url "$RPC_URL" 2>/dev/null) \ || die "RPC unreachable at $RPC_URL" # ── collect-tvl ─────────────────────────────────────────────────────────────── tvl_eth="0" tvl_eth_formatted="0.00" if tvl_output=$(cd "$REPO_ROOT" && LM="$LM" WETH="$WETH" POOL="$POOL" \ "$FORGE" script onchain/script/LmTotalEth.s.sol \ --rpc-url "$RPC_URL" --silent 2>/dev/null); then # forge script outputs the number via console2.log — extract last number tvl_eth=$(echo "$tvl_output" | grep -oE '[0-9]+' | tail -1) tvl_eth="${tvl_eth:-0}" tvl_eth_formatted=$(awk "BEGIN {printf \"%.2f\", $tvl_eth / 1e18}") else echo "WARN: LmTotalEth forge script failed, TVL will be 0" >&2 fi # ── collect-fees ────────────────────────────────────────────────────────────── # accumulatedFees() may not exist on older deployments — graceful fallback to 0. accumulated_fees_eth="0" accumulated_fees_eth_formatted="0.000" if fees_output=$("$CAST" call "$LM" "accumulatedFees()(uint256)" --rpc-url "$RPC_URL" 2>/dev/null); then accumulated_fees_eth=$(echo "$fees_output" | grep -oE '[0-9]+' | head -1) accumulated_fees_eth="${accumulated_fees_eth:-0}" accumulated_fees_eth_formatted=$(awk "BEGIN {printf \"%.3f\", $accumulated_fees_eth / 1e18}") fi # ── collect-positions ───────────────────────────────────────────────────────── # LiquidityManager.positions(uint8 stage) → (uint128 liquidity, int24 tickLower, int24 tickUpper) # Stage: 0=FLOOR, 1=ANCHOR, 2=DISCOVERY position_count=0 positions_json="[" stage_names=("floor" "anchor" "discovery") for stage in 0 1 2; do name="${stage_names[$stage]}" if pos_output=$("$CAST" call "$LM" "positions(uint8)(uint128,int24,int24)" "$stage" --rpc-url "$RPC_URL" 2>/dev/null); then # cast returns one value per line liquidity=$(echo "$pos_output" | sed -n '1p' | tr -d '[:space:]') tick_lower=$(echo "$pos_output" | sed -n '2p' | tr -d '[:space:]') tick_upper=$(echo "$pos_output" | sed -n '3p' | tr -d '[:space:]') liquidity="${liquidity:-0}" tick_lower="${tick_lower:-0}" tick_upper="${tick_upper:-0}" if [[ "$liquidity" != "0" ]]; then position_count=$((position_count + 1)) fi [[ "$stage" -gt 0 ]] && positions_json+="," positions_json+=" { \"name\": \"$name\", \"tick_lower\": $tick_lower, \"tick_upper\": $tick_upper, \"liquidity\": \"$liquidity\" }" else echo "WARN: Failed to read positions($stage) from LiquidityManager" >&2 [[ "$stage" -gt 0 ]] && positions_json+="," positions_json+=" { \"name\": \"$name\", \"tick_lower\": 0, \"tick_upper\": 0, \"liquidity\": \"0\" }" fi done positions_json+=" ]" # ── collect-rebalances ──────────────────────────────────────────────────────── # Event: Recentered(int24 indexed currentTick, bool indexed isUp) rebalance_count_24h=0 last_rebalance_block=0 from_block=$((block_number - LOOKBACK_BLOCKS)) [[ "$from_block" -lt 0 ]] && from_block=0 # Recentered(int24,bool) topic0 event_topic=$("$CAST" keccak "Recentered(int24,bool)" 2>/dev/null) || event_topic="" if [[ -n "$event_topic" ]]; then if logs=$("$CAST" logs --from-block "$from_block" --to-block "$block_number" \ --address "$LM" "$event_topic" --rpc-url "$RPC_URL" 2>/dev/null); then rebalance_count_24h=$(echo "$logs" | grep -c "blockNumber" 2>/dev/null || echo "0") last_block_hex=$(echo "$logs" | grep "blockNumber" | tail -1 | grep -oE '0x[0-9a-fA-F]+' | head -1) if [[ -n "$last_block_hex" ]]; then last_rebalance_block=$(printf '%d' "$last_block_hex" 2>/dev/null || echo "0") fi else echo "WARN: Failed to fetch Recentered event logs" >&2 fi fi # ── verdict ─────────────────────────────────────────────────────────────────── verdict="healthy" if [[ "$tvl_eth" == "0" ]]; then verdict="offline" elif [[ "$position_count" -lt 3 ]] || [[ "$rebalance_count_24h" -eq 0 ]]; then verdict="degraded" fi # ── write JSON ──────────────────────────────────────────────────────────────── cat > "$OUT_FILE" <