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 <noreply@anthropic.com>
9.1 KiB
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:
- 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. - 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
# 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. 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.
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 below for how to separate the deploy from the bootstrap.
After the broadcast completes, record the addresses from the console output:
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:
cast call $POOL "slot0()" --rpc-url $BASE_RPC
# Returns: sqrtPriceX96, tick, ... (non-zero sqrtPriceX96 confirms initialization)
Record the block timestamp of pool creation:
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.
# 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.
# 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
# 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:
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.
# 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):
cast call $KRAIKEN "balanceOf(address)" $DEPLOYER_ADDRESS --rpc-url $BASE_RPC
# Should be > 0
Step 5 — Wait ≥ 60 s (recenter cooldown)
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.
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
# 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.