From 023c661ee78770cdcace9f92af5672879560adb7 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 14 Mar 2026 18:04:11 +0000 Subject: [PATCH] fix: No mainnet VWAP bootstrap runbook (#728) Add docs/mainnet-bootstrap.md with the full two-phase bootstrap sequence: pool init, 300 s TWAP warm-up wait, first recenter + seed buy (exact cast commands), 60 s cooldown wait, second recenter via BootstrapVWAPPhase2.s.sol, and verification/troubleshooting steps. Update the inline bootstrap comment in DeployBase.sol to warn that the attempt always reverts on a fresh pool and direct operators to the new runbook. Co-Authored-By: Claude Sonnet 4.6 --- docs/mainnet-bootstrap.md | 259 ++++++++++++++++++++++++++++++++++ onchain/script/DeployBase.sol | 26 ++-- 2 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 docs/mainnet-bootstrap.md diff --git a/docs/mainnet-bootstrap.md b/docs/mainnet-bootstrap.md new file mode 100644 index 0000000..63abafd --- /dev/null +++ b/docs/mainnet-bootstrap.md @@ -0,0 +1,259 @@ +# Mainnet VWAP Bootstrap Runbook + +**Target chain:** Base (chain ID 8453) + +## Why a manual process? + +The VWAP bootstrap cannot be completed in a single Forge script execution. Two hard time-based delays imposed by the contracts make this impossible: + +1. **300 s TWAP warm-up** — `recenter()` reads the Uniswap V3 TWAP oracle and reverts with `"price deviated from oracle"` if the pool has fewer than 300 seconds of observation history. A pool created within the same broadcast has zero history. +2. **60 s recenter cooldown** — `recenter()` enforces a per-call cooldown (`lastRecenterTime + 60 s`). The first and second recenters cannot share a single broadcast. + +`DeployBase.sol` contains an inline bootstrap attempt that will always fail on a freshly-created pool. Follow this runbook instead. + +--- + +## Prerequisites + +```bash +# Required environment variables — set before starting +export BASE_RPC="https://mainnet.base.org" # or your preferred Base RPC +export DEPLOYER_KEY="0x" +export BASESCAN_API_KEY="" + +# Populated after Step 1 (deploy) +export LM_ADDRESS="" # LiquidityManager proxy address +export KRAIKEN="" # Kraiken token address +export POOL="" # Uniswap V3 pool address + +# Protocol constants (Base mainnet) +export WETH="0x4200000000000000000000000000000000000006" +export SWAP_ROUTER="0x2626664c2603336E57B271c5C0b26F421741e481" # Uniswap V3 SwapRouter02 +export DEPLOYER_ADDRESS="$(cast wallet address --private-key $DEPLOYER_KEY)" + +# Minimum ETH required in deployer wallet: +# gas for deploy (~0.05 ETH) + 0.01 ETH LM seed + 0.005 ETH seed buy +``` + +--- + +## Step 1 — Deploy contracts (pool init) + +Run the mainnet deploy script. The inline bootstrap section in `DeployBase.sol` will revert on a fresh pool — that is expected. All contract deployments that precede the bootstrap attempt will succeed and be confirmed on-chain. + +```bash +cd onchain + +forge script script/DeployBaseMainnet.sol \ + --rpc-url $BASE_RPC \ + --broadcast \ + --verify \ + --etherscan-api-key $BASESCAN_API_KEY \ + --slow \ + --private-key $DEPLOYER_KEY +``` + +> **Note:** If Forge aborts the simulation before broadcast due to the `recenter()` revert, see [Troubleshooting](#troubleshooting) below for how to separate the deploy from the bootstrap. + +After the broadcast completes, record the addresses from the console output: + +```bash +export LM_ADDRESS="0x..." # LiquidityManager address from deploy output +export KRAIKEN="0x..." # Kraiken address from deploy output +export POOL="0x..." # Uniswap V3 pool address from deploy output +``` + +Verify the pool exists and has been initialized: + +```bash +cast call $POOL "slot0()" --rpc-url $BASE_RPC +# Returns: sqrtPriceX96, tick, ... (non-zero sqrtPriceX96 confirms initialization) +``` + +Record the block timestamp of pool creation: + +```bash +export POOL_INIT_TS=$(cast block latest --rpc-url $BASE_RPC --field timestamp) +echo "Pool initialized at Unix timestamp: $POOL_INIT_TS" +echo "First recenter available after: $(( POOL_INIT_TS + 300 )) ($(date -d @$(( POOL_INIT_TS + 300 )) 2>/dev/null || date -r $(( POOL_INIT_TS + 300 )) 2>/dev/null))" +``` + +--- + +## Step 2 — Wait ≥ 300 s (TWAP warm-up) + +The Uniswap V3 TWAP oracle must accumulate at least 300 seconds of observation history before `recenter()` can succeed. Do not proceed until 300 seconds have elapsed since pool initialization. + +```bash +# Poll until 300 s have elapsed since pool creation +TARGET_TS=$(( POOL_INIT_TS + 300 )) +while true; do + NOW=$(cast block latest --rpc-url $BASE_RPC --field timestamp) + REMAINING=$(( TARGET_TS - NOW )) + if [ "$REMAINING" -le 0 ]; then + echo "TWAP warm-up complete. Proceeding to first recenter." + break + fi + echo "Waiting ${REMAINING}s more for TWAP warm-up..." + sleep 10 +done +``` + +--- + +## Step 3 — Fund LiquidityManager and first recenter + +Fund the LiquidityManager with the seed ETH it needs to place bootstrap positions, then call `recenter()` for the first time. + +```bash +# Fund LiquidityManager (0.01 ETH minimum for bootstrap positions) +cast send $LM_ADDRESS \ + --value 0.01ether \ + --rpc-url $BASE_RPC \ + --private-key $DEPLOYER_KEY + +# Confirm balance +cast balance $LM_ADDRESS --rpc-url $BASE_RPC +``` + +```bash +# First recenter — places anchor, floor, and discovery positions +cast send $LM_ADDRESS \ + "recenter()" \ + --rpc-url $BASE_RPC \ + --private-key $DEPLOYER_KEY + +echo "First recenter complete." +``` + +Record the timestamp immediately after this call — the 60 s cooldown starts now: + +```bash +export FIRST_RECENTER_TS=$(cast block latest --rpc-url $BASE_RPC --field timestamp) +echo "First recenter at Unix timestamp: $FIRST_RECENTER_TS" +echo "Second recenter available after: $(( FIRST_RECENTER_TS + 60 ))" +``` + +--- + +## Step 4 — Seed buy (generate non-zero anchor fee) + +The VWAP bootstrap path in `recenter()` only records the price anchor when `ethFee > 0` (i.e., when the anchor position has collected a fee). Execute a small buy of KRAIKEN to generate that fee. + +```bash +# Step 4a — Wrap ETH to WETH +cast send $WETH \ + "deposit()" \ + --value 0.005ether \ + --rpc-url $BASE_RPC \ + --private-key $DEPLOYER_KEY + +# Step 4b — Approve SwapRouter to spend WETH +cast send $WETH \ + "approve(address,uint256)" $SWAP_ROUTER 5000000000000000 \ + --rpc-url $BASE_RPC \ + --private-key $DEPLOYER_KEY + +# Step 4c — Seed buy: swap 0.005 WETH → KRAIKEN via the 1 % pool +# Struct fields: tokenIn, tokenOut, fee, recipient, deadline, amountIn, amountOutMinimum, sqrtPriceLimitX96 +DEADLINE=$(( $(cast block latest --rpc-url $BASE_RPC --field timestamp) + 1800 )) + +cast send $SWAP_ROUTER \ + "exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))(uint256)" \ + "($WETH,$KRAIKEN,10000,$DEPLOYER_ADDRESS,$DEADLINE,5000000000000000,0,0)" \ + --rpc-url $BASE_RPC \ + --private-key $DEPLOYER_KEY + +echo "Seed buy complete. Anchor position has collected a fee." +``` + +Confirm the pool executed the swap (non-zero KRK balance in deployer wallet): + +```bash +cast call $KRAIKEN "balanceOf(address)" $DEPLOYER_ADDRESS --rpc-url $BASE_RPC +# Should be > 0 +``` + +--- + +## Step 5 — Wait ≥ 60 s (recenter cooldown) + +```bash +TARGET_TS=$(( FIRST_RECENTER_TS + 60 )) +while true; do + NOW=$(cast block latest --rpc-url $BASE_RPC --field timestamp) + REMAINING=$(( TARGET_TS - NOW )) + if [ "$REMAINING" -le 0 ]; then + echo "Recenter cooldown elapsed. Proceeding to second recenter." + break + fi + echo "Waiting ${REMAINING}s more for recenter cooldown..." + sleep 5 +done +``` + +--- + +## Step 6 — Second recenter (records VWAP anchor) + +The second `recenter()` hits the bootstrap path inside `LiquidityManager`: `cumulativeVolume == 0` and `ethFee > 0`, so it records the VWAP price anchor and sets `cumulativeVolume > 0`, permanently closing the bootstrap window. + +```bash +export LM_ADDRESS="$LM_ADDRESS" # must be set + +forge script script/BootstrapVWAPPhase2.s.sol \ + --tc BootstrapVWAPPhase2 \ + --rpc-url $BASE_RPC \ + --broadcast \ + --private-key $DEPLOYER_KEY +``` + +The script asserts `cumulativeVolume > 0` and will fail with an explicit message if the bootstrap did not succeed. + +--- + +## Step 7 — Verify bootstrap success + +```bash +# cumulativeVolume must be > 0 +cast call $LM_ADDRESS "cumulativeVolume()" --rpc-url $BASE_RPC +# Expected: non-zero value + +# VWAP should now reflect the seed buy price +cast call $LM_ADDRESS "getVWAP()" --rpc-url $BASE_RPC 2>/dev/null || \ + echo "(getVWAP may not be a public function — check cumulativeVolume above)" + +# Three positions should be in place +cast call $LM_ADDRESS "positions(0)" --rpc-url $BASE_RPC # floor +cast call $LM_ADDRESS "positions(1)" --rpc-url $BASE_RPC # anchor +cast call $LM_ADDRESS "positions(2)" --rpc-url $BASE_RPC # discovery + +# LM should hold ETH / WETH for ongoing operations +cast balance $LM_ADDRESS --rpc-url $BASE_RPC +``` + +--- + +## Troubleshooting + +### `forge script` aborts before broadcast due to recenter() revert + +Foundry simulates the entire `run()` function before broadcasting anything. If the inline bootstrap in `DeployBase.sol` causes the simulation to fail, no transactions are broadcast. + +**Workaround:** Comment out the bootstrap block (lines marked `VWAP Bootstrap`) in `DeployBase.sol` locally before running the deploy script, then restore it afterward. The bootstrap is then performed manually using Steps 3–6 above. + +### `recenter()` reverts with "price deviated from oracle" + +The pool has insufficient TWAP history. Wait longer and retry. At least one block must have been produced with the pool at its initialized price before the 300 s counter begins. + +### `recenter()` reverts with "cooldown" + +The 60 s cooldown has not elapsed since the last recenter. Wait and retry. + +### Seed buy produces zero KRK + +The pool may have no in-range liquidity (first recenter did not place positions successfully). Check positions via `cast call $LM_ADDRESS "positions(1)"` and re-run Step 3 if the anchor position is empty. + +### BootstrapVWAPPhase2 fails with "cumulativeVolume is still 0" + +The anchor position collected no fees — either the seed buy was too small to generate a fee, or the swap routed through a different pool. Repeat Step 4 with a larger `amountIn` (e.g., `0.01 ether` / `10000000000000000`) and re-run Step 5–6. diff --git a/onchain/script/DeployBase.sol b/onchain/script/DeployBase.sol index 5b524ef..15c58fd 100644 --- a/onchain/script/DeployBase.sol +++ b/onchain/script/DeployBase.sol @@ -101,19 +101,23 @@ contract DeployBase is Script { // ===================================================================== // VWAP Bootstrap -> seed trade during deployment // + // WARNING: On Base mainnet this inline attempt WILL REVERT when the pool + // was created in this same script run. The Uniswap V3 TWAP oracle + // requires >= 300 s of observation history before recenter() succeeds, + // and the 60-second recenter cooldown prevents completing both recenters + // in a single broadcast. + // + // Follow docs/mainnet-bootstrap.md for the correct two-phase manual + // sequence: + // Phase 1 — deploy contracts (this script, bootstrap section expected + // to revert on fresh pool) + // Phase 2 — wait >= 300 s, fund LM, first recenter(), seed buy + // Phase 3 — wait >= 60 s, run BootstrapVWAPPhase2.s.sol + // // The cumulativeVolume==0 path in recenter() records VWAP from whatever // price exists at the time of the first fee event. An attacker who - // front-runs deployment with a whale buy inflates that anchor. - // - // Fix: execute a small buy BEFORE handing control to users so that - // cumulativeVolume>0 by the time the protocol is live. - // - // recenter() is now permissionless and always enforces TWAP stability. - // For a fresh pool on Base mainnet this bootstrap must run at least - // 300 seconds after pool initialisation (so the TWAP oracle has history). - // If the pool was just created in this same script run, the first - // recenter() will revert with "price deviated from oracle" — wait 5 min - // and call the bootstrap as a separate transaction or script. + // front-runs deployment with a whale buy inflates that anchor; executing + // the seed buy before handing control to users closes that window. // // Deployer must have SEED_LM_ETH + SEED_SWAP_ETH available (≈0.015 ETH). // =====================================================================