harb/docs/mainnet-bootstrap.md

260 lines
9.4 KiB
Markdown
Raw Normal View History

# 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<your-private-key>"
export BASESCAN_API_KEY="<your-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 101145, 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 36 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 56.