harb/scripts/recover-bootstrap.sh
openhands 20f5ac68cd fix: add polling timeouts and safe fallback in recovery script (#644)
- Add max-iterations guard (60 polls × 5s = 5 min) to both cooldown
  polling loops with explicit error on timeout
- Use LAST_RECENTER (already validated) as fallback instead of "0" for
  post-seed-buy lastRecenterTime read, preventing silent cooldown skip
  on transient RPC failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:45:31 +00:00

244 lines
10 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# recover-bootstrap.sh — Diagnose and recover from a failed VWAP bootstrap.
#
# If the second recenter() reverts mid-sequence (e.g. "amplitude not reached"),
# the LiquidityManager is left with positions deployed but cumulativeVolume == 0.
# This script diagnoses the state and retries the bootstrap.
#
# See docs/mainnet-bootstrap.md "Recovery from failed mid-sequence bootstrap"
# for the full manual procedure.
#
# Usage:
# scripts/recover-bootstrap.sh --rpc-url <RPC> --private-key <KEY> --lm <LM_ADDRESS> [OPTIONS]
#
# Options:
# --rpc-url <url> RPC endpoint (required)
# --private-key <key> Deployer private key (required)
# --lm <address> LiquidityManager address (required)
# --kraiken <address> Kraiken token address (optional, for seed buy retry)
# --seed-eth <amount> Extra seed buy amount, e.g. "0.01ether" (default: skip seed buy)
# --fee-dest <address> Set feeDestination to this address if not already correct
# --dry-run Diagnose only, do not send transactions
# --help Show this help message
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
RPC_URL=""
PRIVATE_KEY=""
LM_ADDRESS=""
KRAIKEN=""
SEED_ETH=""
FEE_DEST=""
DRY_RUN=false
WETH="0x4200000000000000000000000000000000000006"
SWAP_ROUTER_MAINNET="0x2626664c2603336E57B271c5C0b26F421741e481"
SWAP_ROUTER_SEPOLIA="0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4"
MAX_UINT="0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
# ── Argument parsing ──────────────────────────────────────────────────
usage() {
sed -n '/^# Usage:/,/^set -euo/p' "$0" | head -n -1 | sed 's/^# \?//'
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--rpc-url) RPC_URL="$2"; shift 2 ;;
--private-key) PRIVATE_KEY="$2"; shift 2 ;;
--lm) LM_ADDRESS="$2"; shift 2 ;;
--kraiken) KRAIKEN="$2"; shift 2 ;;
--seed-eth) SEED_ETH="$2"; shift 2 ;;
--fee-dest) FEE_DEST="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
--help|-h) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
if [[ -z "$RPC_URL" || -z "$PRIVATE_KEY" || -z "$LM_ADDRESS" ]]; then
echo "Error: --rpc-url, --private-key, and --lm are required."
echo "Run with --help for usage."
exit 1
fi
# ── Helpers ───────────────────────────────────────────────────────────
info() { echo "[recover] $*"; }
warn() { echo "[recover] WARNING: $*"; }
error() { echo "[recover] ERROR: $*" >&2; }
detect_swap_router() {
local chain_id
chain_id="$(cast chain-id --rpc-url "$RPC_URL" 2>/dev/null || echo "")"
if [[ "$chain_id" == "8453" ]]; then
echo "$SWAP_ROUTER_MAINNET"
else
echo "$SWAP_ROUTER_SEPOLIA"
fi
}
# ── Step 1: Diagnose ─────────────────────────────────────────────────
info "Diagnosing LiquidityManager at $LM_ADDRESS ..."
CUMVOL="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "cumulativeVolume()(uint256)" 2>/dev/null || echo "ERROR")"
if [[ "$CUMVOL" == "ERROR" ]]; then
error "Cannot read cumulativeVolume — is LM_ADDRESS correct?"
exit 1
fi
FEE_DESTINATION="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "feeDestination()(address)" 2>/dev/null || echo "ERROR")"
FEE_LOCKED="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "feeDestinationLocked()(bool)" 2>/dev/null || echo "ERROR")"
LAST_RECENTER="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "lastRecenterTime()(uint256)" 2>/dev/null || echo "0")"
info " cumulativeVolume: $CUMVOL"
info " feeDestination: $FEE_DESTINATION"
info " feeDestinationLocked: $FEE_LOCKED"
info " lastRecenterTime: $LAST_RECENTER"
# ── Check: already bootstrapped? ─────────────────────────────────────
if [[ "$CUMVOL" != "0" ]]; then
info "VWAP bootstrap already complete (cumulativeVolume=$CUMVOL). Nothing to recover."
exit 0
fi
info "cumulativeVolume is 0 — bootstrap incomplete, recovery needed."
# ── Check: feeDestination ────────────────────────────────────────────
if [[ "$FEE_DESTINATION" == "0x0000000000000000000000000000000000000000" ]]; then
warn "feeDestination is not set (zero address)."
if [[ -n "$FEE_DEST" ]]; then
info "Will set feeDestination to $FEE_DEST"
else
warn "Pass --fee-dest <address> to set it during recovery."
fi
fi
if [[ -n "$FEE_DEST" && "$FEE_DESTINATION" != "$FEE_DEST" ]]; then
if [[ "$FEE_LOCKED" == "true" ]]; then
error "feeDestination is locked at $FEE_DESTINATION — cannot change to $FEE_DEST."
error "Manual intervention required."
exit 1
fi
if [[ "$DRY_RUN" == "true" ]]; then
info "[dry-run] Would set feeDestination to $FEE_DEST"
else
info "Setting feeDestination to $FEE_DEST ..."
cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
"$LM_ADDRESS" "setFeeDestination(address)" "$FEE_DEST"
info "feeDestination updated."
fi
fi
# ── Check: cooldown ──────────────────────────────────────────────────
NOW="$(cast block latest --rpc-url "$RPC_URL" --field timestamp 2>/dev/null || echo "0")"
COOLDOWN_END=$(( LAST_RECENTER + 60 ))
if [[ "$NOW" -lt "$COOLDOWN_END" ]]; then
REMAINING=$(( COOLDOWN_END - NOW ))
warn "Recenter cooldown has not elapsed. ${REMAINING}s remaining."
warn "Wait and re-run this script."
if [[ "$DRY_RUN" == "true" ]]; then
exit 0
fi
info "Polling for recenter cooldown to elapse (timeout: 5 min) ..."
POLL_ATTEMPTS=0
MAX_POLL_ATTEMPTS=60 # 60 × 5s = 5 min
while true; do
POLL_ATTEMPTS=$(( POLL_ATTEMPTS + 1 ))
if [[ "$POLL_ATTEMPTS" -gt "$MAX_POLL_ATTEMPTS" ]]; then
error "Timed out waiting for recenter cooldown (${MAX_POLL_ATTEMPTS} polls). RPC may be degraded."
exit 1
fi
NOW="$(cast block latest --rpc-url "$RPC_URL" --field timestamp 2>/dev/null || echo "0")"
if [[ "$NOW" -ge "$COOLDOWN_END" ]]; then
info "Recenter cooldown elapsed."
break
fi
REMAINING=$(( COOLDOWN_END - NOW ))
info " ${REMAINING}s remaining ..."
sleep 5
done
fi
# ── Optional: extra seed buy ─────────────────────────────────────────
if [[ -n "$SEED_ETH" && -n "$KRAIKEN" ]]; then
SWAP_ROUTER="$(detect_swap_router)"
DEPLOYER_ADDR="$(cast wallet address --private-key "$PRIVATE_KEY")"
if [[ "$DRY_RUN" == "true" ]]; then
info "[dry-run] Would execute seed buy: $SEED_ETH WETH -> KRAIKEN"
else
info "Executing seed buy ($SEED_ETH WETH -> KRAIKEN) ..."
SEED_WEI="$(cast --to-unit "$SEED_ETH" wei)"
cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
"$WETH" "deposit()" --value "$SEED_ETH"
cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
"$WETH" "approve(address,uint256)" "$SWAP_ROUTER" "$MAX_UINT"
# Determine swap direction from token ordering
WETH_LOWER=$(echo "$WETH" | tr '[:upper:]' '[:lower:]' | sed 's/^0x//')
KRAIKEN_LOWER=$(echo "$KRAIKEN" | tr '[:upper:]' '[:lower:]' | sed 's/^0x//')
if [[ "$WETH_LOWER" < "$KRAIKEN_LOWER" ]]; then
SQRT_LIMIT=4295128740
else
SQRT_LIMIT=1461446703485210103287273052203988822378723970341
fi
cast send --legacy --gas-limit 300000 --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
"$SWAP_ROUTER" "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
"($WETH,$KRAIKEN,10000,$DEPLOYER_ADDR,$SEED_WEI,0,$SQRT_LIMIT)"
info "Seed buy complete."
# Poll until recenter cooldown elapses
info "Polling for recenter cooldown to elapse (timeout: 5 min) ..."
LAST_RECENTER_AFTER="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "lastRecenterTime()(uint256)" 2>/dev/null || echo "$LAST_RECENTER")"
COOLDOWN_TARGET=$(( LAST_RECENTER_AFTER + 60 ))
POLL_ATTEMPTS=0
MAX_POLL_ATTEMPTS=60 # 60 × 5s = 5 min
while true; do
POLL_ATTEMPTS=$(( POLL_ATTEMPTS + 1 ))
if [[ "$POLL_ATTEMPTS" -gt "$MAX_POLL_ATTEMPTS" ]]; then
error "Timed out waiting for recenter cooldown (${MAX_POLL_ATTEMPTS} polls). RPC may be degraded."
exit 1
fi
NOW="$(cast block latest --rpc-url "$RPC_URL" --field timestamp 2>/dev/null || echo "0")"
if [[ "$NOW" -ge "$COOLDOWN_TARGET" ]]; then
info "Recenter cooldown elapsed."
break
fi
REMAINING=$(( COOLDOWN_TARGET - NOW ))
info " ${REMAINING}s remaining ..."
sleep 5
done
fi
fi
# ── Retry second recenter ────────────────────────────────────────────
if [[ "$DRY_RUN" == "true" ]]; then
info "[dry-run] Would call recenter() on $LM_ADDRESS"
info "[dry-run] Done. Re-run without --dry-run to execute."
exit 0
fi
info "Calling recenter() to complete VWAP bootstrap ..."
if ! cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
"$LM_ADDRESS" "recenter()" 2>&1; then
error "recenter() reverted. Check the revert reason above."
error "Common causes:"
error " - 'amplitude not reached.' -> need larger seed buy (use --seed-eth with --kraiken)"
error " - 'price deviated from oracle' -> wait for TWAP history"
error " - 'recenter cooldown' -> wait 60s and retry"
exit 1
fi
# ── Verify ────────────────────────────────────────────────────────────
CUMVOL="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "cumulativeVolume()(uint256)" 2>/dev/null || echo "0")"
if [[ "$CUMVOL" == "0" || -z "$CUMVOL" ]]; then
error "Recovery failed — cumulativeVolume is still 0."
error "The anchor position may have no fees. Try a larger seed buy:"
error " $0 --rpc-url $RPC_URL --private-key <KEY> --lm $LM_ADDRESS --kraiken <ADDR> --seed-eth 0.01ether"
exit 1
fi
info "Recovery successful! cumulativeVolume=$CUMVOL"
info "VWAP bootstrap is complete."