fix: No recovery path if VWAP bootstrap fails mid-sequence (#644)
Add recovery procedure documentation and automated recovery script for when the VWAP bootstrap fails partway through (e.g. second recenter reverts due to insufficient price movement). - Add "Recovery from failed mid-sequence bootstrap" section to docs/mainnet-bootstrap.md with diagnosis steps and manual recovery - Create scripts/recover-bootstrap.sh to automate diagnosis and retry - Add warning comments in BootstrapVWAPPhase2.s.sol, DeployBase.sol, and bootstrap-common.sh referencing the recovery procedure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9cfffa5cea
commit
fbe8384342
5 changed files with 291 additions and 0 deletions
|
|
@ -121,6 +121,9 @@ fund_liquidity_manager() {
|
|||
|
||||
bootstrap_vwap() {
|
||||
detect_swap_router
|
||||
# WARNING: If the second recenter() below fails mid-sequence, the LM is left
|
||||
# with positions deployed but cumulativeVolume == 0 (partial bootstrap).
|
||||
# For mainnet recovery see docs/mainnet-bootstrap.md or scripts/recover-bootstrap.sh.
|
||||
# Idempotency guard: if a previous run already bootstrapped VWAP, skip.
|
||||
local cumvol
|
||||
cumvol="$(cast call --rpc-url "$ANVIL_RPC" \
|
||||
|
|
|
|||
210
scripts/recover-bootstrap.sh
Executable file
210
scripts/recover-bootstrap.sh
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
#!/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 "Waiting ${REMAINING}s for cooldown ..."
|
||||
sleep "$REMAINING"
|
||||
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."
|
||||
|
||||
# Wait for cooldown after potential recenter trigger
|
||||
info "Waiting 65s for recenter cooldown ..."
|
||||
sleep 65
|
||||
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."
|
||||
Loading…
Add table
Add a link
Reference in a new issue