From b61695331397563128078df369ac93bae90ee499 Mon Sep 17 00:00:00 2001 From: johba Date: Sat, 21 Mar 2026 21:00:14 +0000 Subject: [PATCH] fix: add missing shell scripts and fix contract interface in run-protocol - Add scripts/harb-evaluator/run-resources.sh: collects disk, RAM, Anthropic API usage, and Woodpecker CI queue metrics - Add scripts/harb-evaluator/run-protocol.sh: collects TVL, fees, position data, and rebalance events from LiquidityManager - Fix run-protocol.toml: positions accessed via positions(uint8) not named getters (floorPosition/anchorPosition/discoveryPosition) - Fix event signature: Recentered(int24,bool) not Recenter(int24,int24,int24) Addresses review findings: missing implementation files and contract interface mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) --- formulas/run-protocol.toml | 14 +- scripts/harb-evaluator/run-protocol.sh | 186 ++++++++++++++++++++++++ scripts/harb-evaluator/run-resources.sh | 169 +++++++++++++++++++++ 3 files changed, 362 insertions(+), 7 deletions(-) create mode 100755 scripts/harb-evaluator/run-protocol.sh create mode 100755 scripts/harb-evaluator/run-resources.sh diff --git a/formulas/run-protocol.toml b/formulas/run-protocol.toml index 7888b1a..923289b 100644 --- a/formulas/run-protocol.toml +++ b/formulas/run-protocol.toml @@ -88,20 +88,20 @@ forge_script = "onchain/script/LmTotalEth.s.sol" id = "collect-fees" description = """ Query accumulated protocol fees from the LiquidityManager via cast call: - LiquidityManager.accumulatedFees() → uint256 + cast call $LM "accumulatedFees()(uint256)" Records accumulated_fees_eth (wei string) and accumulated_fees_eth_formatted (ETH, 3 dp). -Falls back to 0 gracefully if the function is not present on the deployed -contract (older deployment without fee tracking). +Falls back to 0 gracefully if the function reverts or is not present on +the deployed contract (older deployment without fee tracking). """ [[steps]] id = "collect-positions" description = """ Query the three Uniswap V3 positions held by the LiquidityManager: - LiquidityManager.floorPosition() → (tickLower, tickUpper, liquidity) - LiquidityManager.anchorPosition() → (tickLower, tickUpper, liquidity) - LiquidityManager.discoveryPosition() → (tickLower, tickUpper, liquidity) + LiquidityManager.positions(0) → (liquidity, tickLower, tickUpper) # FLOOR + LiquidityManager.positions(1) → (liquidity, tickLower, tickUpper) # ANCHOR + LiquidityManager.positions(2) → (liquidity, tickLower, tickUpper) # DISCOVERY Records position_count (number of positions with liquidity > 0) and the positions array. """ @@ -116,7 +116,7 @@ Records: - last_rebalance_block: block number of the most recent Recenter event (0 if none found in the window). """ -event_signature = "Recenter(int24,int24,int24)" +event_signature = "Recentered(int24,bool)" [[steps]] id = "collect" diff --git a/scripts/harb-evaluator/run-protocol.sh b/scripts/harb-evaluator/run-protocol.sh new file mode 100755 index 0000000..0e9503f --- /dev/null +++ b/scripts/harb-evaluator/run-protocol.sh @@ -0,0 +1,186 @@ +#!/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" <&2; exit 2; } + +mkdir -p "$OUT_DIR" + +# ── collect-disk ────────────────────────────────────────────────────────────── + +disk_used_bytes=0 +disk_total_bytes=0 +disk_used_pct=0 + +if command -v df >/dev/null 2>&1; then + # -B1 gives bytes; tail -1 skips header; awk grabs used, total, pct + read -r disk_total_bytes disk_used_bytes disk_used_pct < <( + df -B1 "$DISK_PATH" | tail -1 | awk '{gsub(/%/,"",$5); print $2, $3, $5}' + ) +else + echo "WARN: df not available, disk metrics will be 0" >&2 +fi + +# ── collect-ram ─────────────────────────────────────────────────────────────── + +ram_used_bytes=0 +ram_total_bytes=0 +ram_used_pct=0 + +if command -v free >/dev/null 2>&1; then + # free -b: second line (Mem:) has total, used + read -r ram_total_bytes ram_used_bytes < <( + free -b | awk '/^Mem:/ {print $2, $3}' + ) + if [[ "$ram_total_bytes" -gt 0 ]]; then + ram_used_pct=$(awk "BEGIN {printf \"%.1f\", $ram_used_bytes / $ram_total_bytes * 100}") + fi +elif command -v vm_stat >/dev/null 2>&1; then + # macOS fallback + page_size=$(vm_stat | head -1 | grep -o '[0-9]*') + pages_active=$(vm_stat | awk '/Pages active/ {gsub(/\./,"",$3); print $3}') + pages_wired=$(vm_stat | awk '/Pages wired/ {gsub(/\./,"",$4); print $4}') + pages_free=$(vm_stat | awk '/Pages free/ {gsub(/\./,"",$3); print $3}') + pages_inactive=$(vm_stat | awk '/Pages inactive/ {gsub(/\./,"",$3); print $3}') + ram_used_bytes=$(( (pages_active + pages_wired) * page_size )) + ram_total_bytes=$(( (pages_active + pages_wired + pages_free + pages_inactive) * page_size )) + if [[ "$ram_total_bytes" -gt 0 ]]; then + ram_used_pct=$(awk "BEGIN {printf \"%.1f\", $ram_used_bytes / $ram_total_bytes * 100}") + fi +else + echo "WARN: neither free nor vm_stat available, RAM metrics will be 0" >&2 +fi + +# ── collect-api ─────────────────────────────────────────────────────────────── + +anthropic_calls_24h=0 +anthropic_budget_usd_used=0 +anthropic_budget_pct=0 + +if [[ -f "$CALL_LOG" ]]; then + cutoff=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null \ + || date -u -v-24H +%Y-%m-%dT%H:%M:%S 2>/dev/null \ + || echo "") + today=$(date -u +%Y-%m-%d) + + if [[ -n "$cutoff" ]]; then + anthropic_calls_24h=$(awk -F'"ts"' -v cutoff="$cutoff" ' + NF>1 { split($2,a,"\""); if (a[2] >= cutoff) count++ } + END { print count+0 } + ' "$CALL_LOG") + fi + + anthropic_budget_usd_used=$(awk -F'"' -v today="$today" ' + /"ts"/ && $0 ~ today { + match($0, /"cost_usd"[[:space:]]*:[[:space:]]*([0-9.]+)/, m) + if (m[1] != "") sum += m[1] + } + END { printf "%.2f", sum+0 } + ' "$CALL_LOG") +fi + +if [[ "$ANTHROPIC_BUDGET_USD_LIMIT" != "0" ]]; then + anthropic_budget_pct=$(awk "BEGIN {printf \"%.1f\", $anthropic_budget_usd_used / $ANTHROPIC_BUDGET_USD_LIMIT * 100}") +fi + +# ── collect-ci ──────────────────────────────────────────────────────────────── + +woodpecker_queue_depth="null" +woodpecker_running="null" + +if [[ -n "$WOODPECKER_API_URL" ]]; then + if ci_json=$(curl -sf --max-time 5 "$WOODPECKER_API_URL/api/queue/info" 2>/dev/null); then + woodpecker_queue_depth=$(echo "$ci_json" | jq '.pending // .waiting // 0') + woodpecker_running=$(echo "$ci_json" | jq '.running // 0') + else + echo "WARN: Woodpecker CI unreachable at $WOODPECKER_API_URL, CI metrics will be null" >&2 + fi +fi + +# ── verdict ─────────────────────────────────────────────────────────────────── + +verdict="ok" + +for pct in "$disk_used_pct" "$ram_used_pct" "$anthropic_budget_pct"; do + # Strip trailing % if present + pct_num="${pct//%/}" + if awk "BEGIN {exit !($pct_num >= 95)}"; then + verdict="critical" + break + fi + if awk "BEGIN {exit !($pct_num >= 80)}"; then + verdict="warn" + fi +done + +# ── write JSON ──────────────────────────────────────────────────────────────── + +cat > "$OUT_FILE" <