harb/onchain/script/DeployLocal.sol
openhands dbf78de793 fix: bootstrap + red-team on forked networks
Bootstrap fixes:
- Idempotency check: skip if Kraiken already deployed on Anvil
- anvil_setCode to strip ERC-4337 code from deployer + feeDest
- DeployLocal.sol: feeDest derived from keccak256('harb.local.feeDest')

Red-team fixes:
- New bootstrap-light.sh: Anvil-only, ~30s deploy
- red-team.sh uses bootstrap-light instead of full docker compose
- anvil_setBalance for feeDest before impersonation
- forge --color never, path resolution, docker chown

Address fixes (all Base mainnet, in both FitnessEvaluator + AttackRunner):
- V3_FACTORY: 0x33128a8fC17869897dcE68Ed026d694621f6FDfD
- SWAP_ROUTER: 0x2626664c2603336E57B271c5C0b26F421741e481
- NPM_ADDR: 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1
2026-03-14 13:31:23 +00:00

196 lines
9.6 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
// Anvil account 9 — guaranteed to be an EOA with no code on any fork.
// Previous address (0xf6a3...) has 171 bytes of code on Base mainnet,
// which triggers the feeDestination lock.
// Derived from keccak256 — guaranteed no code on any fork.
address internal constant feeDest = address(uint160(uint256(keccak256("harb.local.feeDest"))));
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. Temporarily make sender the feeDestination (deployer can do this
// because setFeeDestination is gated on deployer, not feeDestination).
// This allows sender to call setRecenterAccess.
// 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. 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.
// 5. Revoke recenterAccess and restore the real feeDestination.
// =====================================================================
console.log("\n[7/7] Bootstrapping VWAP with seed trade...");
// Step 1: Grant deployer temporary feeDestination role to enable setRecenterAccess.
// NOTE: on forked networks, bootstrap.sh pre-clears code from deployer
// and feeDest via anvil_setCode — required because Base Sepolia has
// ERC-4337 code at well-known addresses, triggering feeDestination lock.
liquidityManager.setFeeDestination(sender);
liquidityManager.setRecenterAccess(sender);
console.log(" Temporary recenterAccess granted to deployer");
// 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: 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());
// Step 5: Clean up -> revoke temporary access and set the real feeDestination.
liquidityManager.revokeRecenterAccess();
liquidityManager.setFeeDestination(feeDest);
console.log(" recenterAccess revoked, feeDestination restored 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. Grant recenterAccess to txnBot (call from feeDestination):");
console.log(" cast send", address(liquidityManager), "\"setRecenterAccess(address)\" <txnBotAddr>");
console.log("3. txnBot can now call recenter() to rebalance positions.");
vm.stopBroadcast();
}
}