harb/onchain/script/DeployLocal.sol
openhands df2f0a87e5 fix: track ts explicitly in DeployLocal bootstrap to avoid Forge block.timestamp reset
Forge resets block.timestamp to its pre-warp value after each state-changing
call (e.g. recenter()). The second vm.warp(block.timestamp + 301) in the VWAP
bootstrap was therefore warping to the same timestamp as the first warp, so
lastRecenterTime + 60 > block.timestamp and the second recenter() reverted
with "recenter cooldown".

Fix: store ts = block.timestamp + 301 before the first warp and increment it
explicitly (ts += 301) before the second warp, mirroring the same pattern
applied to VWAPFloorProtection.t.sol and SupplyCorruption.t.sol.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 22:45:22 +00:00

192 lines
9.1 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/Optimizer.sol";
import "../src/Stake.sol";
import "../src/helpers/UniswapHelpers.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "forge-std/Script.sol";
import "./DeployCommon.sol";
/**
* @title DeployLocal
* @notice Deployment script for local Anvil fork
* @dev Run with: forge script script/DeployLocal.sol --rpc-url http://localhost:8545 --broadcast
*/
contract DeployLocal is Script {
using UniswapHelpers for IUniswapV3Pool;
uint24 internal constant FEE = uint24(10_000);
// Configuration
address internal constant feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011;
address internal constant weth = 0x4200000000000000000000000000000000000006;
address internal constant v3Factory = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
// Seed amounts for VWAP bootstrap.
// seedLmEth: initial ETH sent to the LM to create thin bootstrap positions.
// seedSwapEth: ETH used for the seed buy. Must be large enough to move the
// Uniswap tick >400 ticks past the ANCHOR center (minAmplitude = 2*tickSpacing
// = 400 for the 1%-fee pool). The ANCHOR typically holds ~25% of seedLmEth as
// WETH across a ~7200-tick range; consuming half of that WETH (≈0.125 ETH)
// moves the price ~3600 ticks — well above the 400-tick threshold.
// 0.5 ether provides a 4× margin over the minimum needed.
uint256 internal constant SEED_LM_ETH = 1 ether;
uint256 internal constant SEED_SWAP_ETH = 0.5 ether;
// Deployed contracts
Kraiken public kraiken;
Stake public stake;
LiquidityManager public liquidityManager;
IUniswapV3Pool public pool;
bool public token0isWeth;
function run() public {
// Use local mnemonic file for consistent deployment
string memory seedPhrase = vm.readFile(".secret.local");
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
vm.startBroadcast(privateKey);
address sender = vm.addr(privateKey);
console.log("\n=== Starting Local Deployment ===");
console.log("Deployer:", sender);
console.log("Using mnemonic from .secret.local");
console.log("Chain ID: 31337 (Local Anvil)");
// Deploy Kraiken token
kraiken = new Kraiken("Kraiken", "KRK");
console.log("\n[1/7] Kraiken deployed:", address(kraiken));
// Determine token ordering
token0isWeth = address(weth) < address(kraiken);
console.log(" Token ordering - WETH is token0:", token0isWeth);
// Deploy Stake contract
stake = new Stake(address(kraiken), feeDest);
console.log("\n[2/7] Stake deployed:", address(stake));
// Set staking pool in Kraiken
kraiken.setStakingPool(address(stake));
console.log(" Staking pool set in Kraiken");
// Get or create Uniswap V3 pool
IUniswapV3Factory factory = IUniswapV3Factory(v3Factory);
address liquidityPool = factory.getPool(weth, address(kraiken), FEE);
if (liquidityPool == address(0)) {
liquidityPool = factory.createPool(weth, address(kraiken), FEE);
console.log("\n[3/7] Uniswap pool created:", liquidityPool);
} else {
console.log("\n[3/7] Using existing pool:", liquidityPool);
}
pool = IUniswapV3Pool(liquidityPool);
// Initialize pool at 1 cent price if not already initialized
try pool.slot0() returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool) {
if (sqrtPriceX96 == 0) {
pool.initializePoolFor1Cent(token0isWeth);
console.log(" Pool initialized at 1 cent price");
} else {
console.log(" Pool already initialized");
}
} catch {
pool.initializePoolFor1Cent(token0isWeth);
console.log(" Pool initialized at 1 cent price");
}
// Deploy Optimizer
Optimizer optimizerImpl = new Optimizer();
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params);
address optimizerAddress = address(proxy);
console.log("\n[4/7] Optimizer deployed:", optimizerAddress);
// Deploy LiquidityManager
liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress);
console.log("\n[5/7] LiquidityManager deployed:", address(liquidityManager));
// Configure contracts
kraiken.setLiquidityManager(address(liquidityManager));
console.log(" LiquidityManager set in Kraiken");
console.log("\n[6/7] Configuration complete");
// =====================================================================
// [7/7] VWAP Bootstrap -> seed trade during deployment
//
// 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.
//
// Sequence:
// 1. Warp 301 seconds forward so the pool's TWAP oracle has enough history
// for _isPriceStable() to succeed (requires >= 300s of observations).
// 2. Fund LM with SEED_LM_ETH and call recenter() -> places thin initial
// positions; no fees collected yet, so cumulativeVolume stays 0.
// 3. Execute seed buy via SeedSwapper -> generates a non-zero WETH fee
// in the anchor position and moves the tick >400 (minimum amplitude).
// 4. Warp 301 more seconds so TWAP catches up to the post-buy price and
// cooldown (60s) elapses between the two recenters.
// 5. Call recenter() again -> cumulativeVolume==0 triggers the bootstrap
// path (shouldRecordVWAP=true); ethFee>0 → _recordVolumeAndPrice fires
// → cumulativeVolume>0. VWAP is now anchored to the real launch price.
// =====================================================================
console.log("\n[7/7] Bootstrapping VWAP with seed trade...");
// Step 1: Advance time so TWAP oracle has sufficient history.
// Track ts explicitly — Forge resets block.timestamp after state-changing calls,
// so block.timestamp + 301 would warp to the same value if used in Step 4.
uint256 ts = block.timestamp + 301;
vm.warp(ts);
// Step 2: Fund LM and place initial bootstrap positions.
(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");
// 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 4: Warp forward so TWAP settles at post-buy price and cooldown elapses.
ts += 301;
vm.warp(ts);
// Step 5: Second recenter records VWAP (bootstrap path + ethFee > 0).
liquidityManager.recenter();
require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0");
console.log(" Second recenter complete -> VWAP bootstrapped");
console.log(" cumulativeVolume:", liquidityManager.cumulativeVolume());
console.log(" VWAP (X96):", liquidityManager.getVWAP());
// Set the real feeDestination now that bootstrap is complete.
liquidityManager.setFeeDestination(feeDest);
console.log(" feeDestination set to", feeDest);
// Print deployment summary
console.log("\n=== Deployment Summary ===");
console.log("Kraiken (KRK):", address(kraiken));
console.log("Stake:", address(stake));
console.log("Pool:", address(pool));
console.log("LiquidityManager:", address(liquidityManager));
console.log("Optimizer:", optimizerAddress);
console.log("\n=== Next Steps ===");
console.log("VWAP is already bootstrapped. To go live:");
console.log("1. Fund LiquidityManager with operational ETH (current balance includes seed):");
console.log(" cast send", address(liquidityManager), "--value 10ether");
console.log("2. recenter() is now permissionless - any address (e.g. txnBot) can call it.");
console.log(" TWAP manipulation protection is always enforced (no bypass path).");
vm.stopBroadcast();
}
}