# 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 ``` --- ## Recovery from failed mid-sequence bootstrap If the bootstrap fails partway through (e.g., the second `recenter()` in Step 6 reverts due to insufficient price movement / "amplitude not reached"), the LiquidityManager is left in a partially bootstrapped state: - **Positions deployed** — the first `recenter()` placed anchor, floor, and discovery positions - **`cumulativeVolume == 0`** — the VWAP anchor was never recorded - **`feeDestination` set** — `DeployBase.sol` sets this before any recenter attempt - **`recenter()` is permissionless** — no access control to revoke; anyone can call it ### Diagnosing the state ```bash # Check if VWAP bootstrap completed (0 = not yet bootstrapped) cast call $LM_ADDRESS "cumulativeVolume()(uint256)" --rpc-url $BASE_RPC # Check current feeDestination cast call $LM_ADDRESS "feeDestination()(address)" --rpc-url $BASE_RPC # Check if feeDestination is locked (true = cannot be changed) cast call $LM_ADDRESS "feeDestinationLocked()(bool)" --rpc-url $BASE_RPC # Check if positions exist (non-zero liquidity = positions deployed) cast call $LM_ADDRESS "positions(uint8)(int24,int24,uint128)" 1 --rpc-url $BASE_RPC ``` ### Recovery steps 1. **Identify the failure cause** — check the revert reason from Step 6. Common causes: - `"amplitude not reached."` — the seed buy did not move the price enough ticks for `recenter()` to accept the movement as significant - `"price deviated from oracle"` — TWAP history is still insufficient - `"recenter cooldown"` — 60 s has not elapsed since the last recenter 2. **Fix the root cause:** - For amplitude issues: execute a larger seed buy (Step 4 with more ETH) to generate more price movement and anchor fees - For TWAP issues: wait longer for oracle history to accumulate - For cooldown: simply wait 60 s 3. **Retry the second recenter** — re-run Step 6 (`BootstrapVWAPPhase2.s.sol`) or call `recenter()` directly: ```bash cast send $LM_ADDRESS "recenter()" --rpc-url $BASE_RPC --private-key $DEPLOYER_KEY ``` 4. **Verify** — confirm `cumulativeVolume > 0` (Step 7) 5. **If `feeDestination` needs correction** (e.g., was set to the wrong address): ```bash # Only works if feeDestinationLocked is false cast send $LM_ADDRESS \ "setFeeDestination(address)" \ --rpc-url $BASE_RPC \ --private-key $DEPLOYER_KEY ``` ### Automated recovery A helper script automates the diagnosis and retry: ```bash # Diagnose and retry bootstrap scripts/recover-bootstrap.sh --rpc-url $BASE_RPC --private-key $DEPLOYER_KEY --lm $LM_ADDRESS ``` See `scripts/recover-bootstrap.sh --help` for all options. --- ## 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.