# 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.