2026-03-19 22:24:05 +00:00
|
|
|
#!/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
|
2026-03-19 22:59:21 +00:00
|
|
|
info "Polling for recenter cooldown to elapse ..."
|
|
|
|
|
while true; do
|
|
|
|
|
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
|
2026-03-19 22:24:05 +00:00
|
|
|
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."
|
|
|
|
|
|
2026-03-19 22:59:21 +00:00
|
|
|
# Poll until recenter cooldown elapses
|
|
|
|
|
info "Polling for recenter cooldown to elapse ..."
|
|
|
|
|
LAST_RECENTER_AFTER="$(cast call --rpc-url "$RPC_URL" "$LM_ADDRESS" "lastRecenterTime()(uint256)" 2>/dev/null || echo "0")"
|
|
|
|
|
COOLDOWN_TARGET=$(( LAST_RECENTER_AFTER + 60 ))
|
|
|
|
|
while true; do
|
|
|
|
|
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
|
2026-03-19 22:24:05 +00:00
|
|
|
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:"
|
2026-03-19 22:59:21 +00:00
|
|
|
error " - 'amplitude not reached.' -> need larger seed buy (use --seed-eth with --kraiken)"
|
2026-03-19 22:24:05 +00:00
|
|
|
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."
|