diff --git a/STATE.md b/STATE.md index 242481d..47a1ea5 100644 --- a/STATE.md +++ b/STATE.md @@ -27,3 +27,4 @@ - [2026-03-14] evolve.sh stale tmpdirs break subsequent runs (#750) - [2026-03-14] evolve.sh silences all batch-eval errors with 2>/dev/null (#749) - [2026-03-14] evolution-daemon.sh — perpetual evolution loop on DO box (#748) +- [2026-03-14] No mainnet VWAP bootstrap runbook (#728) diff --git a/docs/mainnet-bootstrap.md b/docs/mainnet-bootstrap.md new file mode 100644 index 0000000..b8fc6f2 --- /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. `DeployBase.sol` wraps the inline `recenter()` call in a try/catch, so if the pool is too fresh for the TWAP oracle the bootstrap is skipped with a warning and the deployment still succeeds. The deploy script then prints instructions directing you to complete the bootstrap manually. + +```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 the script still aborts during simulation (e.g., due to an older version of `DeployBase.sol` without the try/catch), see [Troubleshooting](#troubleshooting) 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 +# SwapRouter02 exactInputSingle struct (7 fields — no deadline): +# tokenIn, tokenOut, fee, recipient, amountIn, amountOutMinimum, sqrtPriceLimitX96 +cast send $SWAP_ROUTER \ + "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))(uint256)" \ + "($WETH,$KRAIKEN,10000,$DEPLOYER_ADDRESS,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 +# LM_ADDRESS must already be set from Step 1. +# BootstrapVWAPPhase2.s.sol reads the broadcaster key from the .secret +# seed-phrase file in onchain/ (same as DeployBase.sol). Ensure that file +# is present; the --private-key CLI flag is NOT used by this script. +forge script script/BootstrapVWAPPhase2.s.sol \ + --tc BootstrapVWAPPhase2 \ + --rpc-url $BASE_RPC \ + --broadcast +``` + +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 in `DeployBase.sol` locally (lines 101–145, from `// =====================================================================` through `seedSwapper.executeSeedBuy{ value: SEED_SWAP_ETH }(sender);`) 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..8f66cdf 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). // ===================================================================== @@ -124,17 +128,24 @@ contract DeployBase is Script { console.log("feeDestination set to", feeDest); // Step 2: Fund LM and place initial bootstrap positions. - // NOTE: recenter() requires TWAP history (>= 300s since pool init). - // On Base mainnet this call will revert if the pool is too fresh. + // recenter() requires TWAP history (>= 300s since pool init). + // On a fresh mainnet pool this will revert; the try/catch allows the + // deploy to succeed and the operator completes the bootstrap manually + // by following docs/mainnet-bootstrap.md. (bool funded,) = address(liquidityManager).call{ value: SEED_LM_ETH }(""); require(funded, "Failed to fund LM for seed bootstrap"); - liquidityManager.recenter(); - console.log("First recenter complete -> positions placed, cumulativeVolume still 0"); + try liquidityManager.recenter() { + console.log("First recenter complete -> positions placed, cumulativeVolume still 0"); - // Step 3: Seed buy -> generates a non-zero fee in the anchor position. - SeedSwapper seedSwapper = new SeedSwapper(weth, address(pool), token0isWeth); - seedSwapper.executeSeedBuy{ value: SEED_SWAP_ETH }(sender); - console.log("Seed buy executed -> fee generated in anchor position"); + // Step 3: Seed buy -> generates a non-zero fee in the anchor position. + SeedSwapper seedSwapper = new SeedSwapper(weth, address(pool), token0isWeth); + seedSwapper.executeSeedBuy{ value: SEED_SWAP_ETH }(sender); + console.log("Seed buy executed -> fee generated in anchor position"); + } catch { + console.log("WARNING: recenter() reverted - inline bootstrap skipped."); + console.log(" Pool likely has < 300 s of TWAP history."); + console.log(" Follow docs/mainnet-bootstrap.md (Phase 2 steps) to complete."); + } // Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0). // Cannot be called in the same Forge broadcast as Step 2 — recenter() enforces a @@ -149,7 +160,7 @@ contract DeployBase is Script { console.log("Optimizer:", optimizerAddress); console.log("\nPost-deploy steps:"); console.log(" 1. Wait >= 60 s after this script finishes."); - console.log(" 2. Run: forge script script/BootstrapVWAPPhase2.s.sol --tc BootstrapVWAPPhase2 --fork-url --broadcast"); + console.log(" 2. Run: forge script script/BootstrapVWAPPhase2.s.sol --tc BootstrapVWAPPhase2 --rpc-url --broadcast"); console.log(" This performs the second recenter that records cumulativeVolume > 0."); console.log(" 3. Fund LiquidityManager with operational ETH."); console.log(" 4. recenter() is permissionless - any address (e.g. txnBot) can call it.");