From 1a410a30b74f2f452688807761e147022d917230 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 13 Mar 2026 22:32:53 +0000 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20Remove=20recenterAccess=20=E2=80=94?= =?UTF-8?q?=20make=20recenter()=20public=20with=20TWAP=20enforcement=20(#7?= =?UTF-8?q?06)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- containers/bootstrap.sh | 1 - onchain/script/DeployBase.sol | 28 ++++++---- onchain/script/DeployLocal.sol | 31 +++++----- .../script/backtesting/BacktestRunner.s.sol | 9 ++- onchain/src/LiquidityManager.sol | 42 ++++---------- onchain/test/FitnessEvaluator.t.sol | 24 ++++---- onchain/test/LiquidityManager.t.sol | 56 +++---------------- onchain/test/SupplyCorruption.t.sol | 4 ++ onchain/test/VWAPFloorProtection.t.sol | 15 +++++ onchain/test/helpers/TestBase.sol | 19 +------ scripts/bootstrap-common.sh | 15 ----- scripts/ci-bootstrap.sh | 3 - scripts/harb-evaluator/red-team.sh | 27 +++------ 13 files changed, 94 insertions(+), 180 deletions(-) diff --git a/containers/bootstrap.sh b/containers/bootstrap.sh index 945df7a..d2246ec 100755 --- a/containers/bootstrap.sh +++ b/containers/bootstrap.sh @@ -136,7 +136,6 @@ main() { extract_addresses write_contracts_env fund_liquidity_manager - grant_recenter_access call_recenter seed_application_state write_deployments_json diff --git a/onchain/script/DeployBase.sol b/onchain/script/DeployBase.sol index de9065c..9565a66 100644 --- a/onchain/script/DeployBase.sol +++ b/onchain/script/DeployBase.sol @@ -108,33 +108,42 @@ contract DeployBase is Script { // Fix: execute a small buy BEFORE handing control to users so that // cumulativeVolume>0 by the time the protocol is live. // + // recenter() is now permissionless and always enforces TWAP stability. + // For a fresh pool on Base mainnet this bootstrap must run at least + // 300 seconds after pool initialisation (so the TWAP oracle has history). + // If the pool was just created in this same script run, the first + // recenter() will revert with "price deviated from oracle" — wait 5 min + // and call the bootstrap as a separate transaction or script. + // // Deployer must have SEED_LM_ETH + SEED_SWAP_ETH available (≈0.015 ETH). // ===================================================================== console.log("\nBootstrapping VWAP with seed trade..."); - // Step 1: Temporarily set deployer as feeDestination to call setRecenterAccess. - liquidityManager.setFeeDestination(sender); - liquidityManager.setRecenterAccess(sender); + // Step 1: Set the real feeDestination before any recenter. + liquidityManager.setFeeDestination(feeDest); + console.log("feeDestination set to", feeDest); // Step 2: Fund LM and place initial bootstrap positions. + // NOTE: recenter() requires TWAP history (>= 300s since pool init). + // On Base mainnet this call will revert if the pool is too fresh. (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). + // NOTE: Must be called >= 60s after the first recenter (cooldown) AND + // >= 300s after pool init so TWAP has settled at post-buy price. + // On Base mainnet, mine/wait ~5 min between Step 2 and Step 4. liquidityManager.recenter(); require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0"); console.log("VWAP bootstrapped -> cumulativeVolume:", liquidityManager.cumulativeVolume()); - // Step 5: Clean up -> revoke temporary access and set the real feeDestination. - liquidityManager.revokeRecenterAccess(); - liquidityManager.setFeeDestination(feeDest); - console.log("recenterAccess revoked, feeDestination set to", feeDest); - console.log("\n=== Deployment Complete ==="); console.log("Kraiken:", address(kraiken)); console.log("Stake:", address(stake)); @@ -143,8 +152,7 @@ contract DeployBase is Script { console.log("Optimizer:", optimizerAddress); console.log("\nPost-deploy steps:"); console.log(" 1. Fund LiquidityManager with operational ETH (VWAP already bootstrapped)"); - console.log(" 2. Set recenterAccess to txnBot: lm.setRecenterAccess(txnBot) from feeDestination"); - console.log(" 3. txnBot can now call recenter()"); + console.log(" 2. recenter() is now permissionless - any address (e.g. txnBot) can call it."); vm.stopBroadcast(); } diff --git a/onchain/script/DeployLocal.sol b/onchain/script/DeployLocal.sol index d294974..bf9e696 100644 --- a/onchain/script/DeployLocal.sol +++ b/onchain/script/DeployLocal.sol @@ -126,24 +126,22 @@ contract DeployLocal is Script { // 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. + // 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. Call recenter() again -> cumulativeVolume==0 triggers the bootstrap + // 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. - // 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. - liquidityManager.setFeeDestination(sender); - liquidityManager.setRecenterAccess(sender); - console.log(" Temporary recenterAccess granted to deployer"); + // Step 1: Advance time so TWAP oracle has sufficient history. + vm.warp(block.timestamp + 301); // Step 2: Fund LM and place initial bootstrap positions. (bool funded,) = address(liquidityManager).call{ value: SEED_LM_ETH }(""); @@ -156,17 +154,19 @@ contract DeployLocal is Script { 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). + // Step 4: Warp forward so TWAP settles at post-buy price and cooldown elapses. + vm.warp(block.timestamp + 301); + + // 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()); - // Step 5: Clean up -> revoke temporary access and set the real feeDestination. - liquidityManager.revokeRecenterAccess(); + // Set the real feeDestination now that bootstrap is complete. liquidityManager.setFeeDestination(feeDest); - console.log(" recenterAccess revoked, feeDestination restored to", feeDest); + console.log(" feeDestination set to", feeDest); // Print deployment summary console.log("\n=== Deployment Summary ==="); @@ -180,9 +180,8 @@ contract DeployLocal is Script { 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)\" "); - console.log("3. txnBot can now call recenter() to rebalance positions."); + 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(); } diff --git a/onchain/script/backtesting/BacktestRunner.s.sol b/onchain/script/backtesting/BacktestRunner.s.sol index dcf00d6..54defcd 100644 --- a/onchain/script/backtesting/BacktestRunner.s.sol +++ b/onchain/script/backtesting/BacktestRunner.s.sol @@ -143,14 +143,13 @@ contract BacktestRunner is Script { // ------------------------------------------------------------------ KrAIkenSystem memory sys = KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital); - // Deploy StrategyExecutor and grant it recenter access on the LM. - // recenterAccess bypasses TWAP stability check and cooldown — correct - // for simulation where vm.warp drives time, not a real oracle. - // sender == feeDestination, so the onlyFeeDestination guard is satisfied. + // Deploy StrategyExecutor — recenter() is now permissionless, so no + // access grant is needed. StrategyExecutor.maybeRecenter() calls + // recenter() via try/catch and logs "SKIP" on cooldown/TWAP failures. + // vm.warp in EventReplayer drives time so TWAP and cooldown pass. bool token0isWeth = sp.token0 == address(mockWeth); StrategyExecutor executor = new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval, sp.pool, token0isWeth); - sys.lm.setRecenterAccess(address(executor)); // Deploy baseline strategies and initialize with the same capital as KrAIken. BaselineStrategies baselines = diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 50649f5..dd4a94c 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -49,16 +49,15 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { /// @notice Access control and fee management address private immutable deployer; - address public recenterAccess; address public feeDestination; bool public feeDestinationLocked; /// @notice Last recenter tick — used to determine net trade direction between recenters int24 public lastRecenterTick; - /// @notice Last recenter timestamp — rate limits open recenters. + /// @notice Last recenter timestamp — rate limits recenters. uint256 public lastRecenterTime; - /// @notice Minimum seconds between open recenters (when recenterAccess is unset) + /// @notice Minimum seconds between recenters uint256 internal constant MIN_RECENTER_INTERVAL = 60; /// @notice Target observation cardinality requested from the pool during construction uint16 internal constant ORACLE_CARDINALITY = 100; @@ -69,12 +68,6 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { /// @notice Custom errors error ZeroAddressInSetter(); - /// @notice Access control modifier - modifier onlyFeeDestination() { - require(msg.sender == address(feeDestination), "only callable by feeDestination"); - _; - } - /// @notice Constructor initializes all contract references and pool configuration /// @param _factory The address of the Uniswap V3 factory /// @param _WETH9 The address of the WETH contract @@ -142,28 +135,17 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { } } - /// @notice Sets recenter access for testing/emergency purposes - /// @param addr Address to grant recenter access - function setRecenterAccess(address addr) external onlyFeeDestination { - recenterAccess = addr; - } - - /// @notice Revokes recenter access - function revokeRecenterAccess() external onlyFeeDestination { - recenterAccess = address(0); - } - /// @notice Adjusts liquidity positions in response to price movements. + /// Callable by anyone. Always enforces cooldown and TWAP price stability. /// This function either completes a full recenter (removing all positions, /// recording VWAP where applicable, and redeploying liquidity) or reverts — /// it never returns silently without acting. /// /// @dev Revert conditions (no silent false return for failure): - /// - "access denied" — recenterAccess is set and caller is not that address - /// - "recenter cooldown" — recenterAccess is unset and MIN_RECENTER_INTERVAL has not elapsed - /// - "price deviated from oracle" — recenterAccess is unset and price is outside TWAP bounds - /// - "amplitude not reached." — anchor position exists but price has not moved far enough - /// from the anchor centre to warrant repositioning + /// - "recenter cooldown" — MIN_RECENTER_INTERVAL has not elapsed since last recenter + /// - "price deviated from oracle" — price is outside TWAP bounds (manipulation guard) + /// - "amplitude not reached." — anchor position exists but price has not moved far enough + /// from the anchor centre to warrant repositioning /// /// @return isUp True if the KRK price in ETH rose since the last recenter /// (buy event / net ETH inflow), regardless of token0/token1 ordering. @@ -173,13 +155,9 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { function recenter() external returns (bool isUp) { (, int24 currentTick,,,,,) = pool.slot0(); - // Validate access and price stability - if (recenterAccess != address(0)) { - require(msg.sender == recenterAccess, "access denied"); - } else { - require(block.timestamp >= lastRecenterTime + MIN_RECENTER_INTERVAL, "recenter cooldown"); - require(_isPriceStable(currentTick), "price deviated from oracle"); - } + // Always enforce cooldown and TWAP price stability — no bypass path + require(block.timestamp >= lastRecenterTime + MIN_RECENTER_INTERVAL, "recenter cooldown"); + require(_isPriceStable(currentTick), "price deviated from oracle"); lastRecenterTime = block.timestamp; // Check if price movement is sufficient for recentering diff --git a/onchain/test/FitnessEvaluator.t.sol b/onchain/test/FitnessEvaluator.t.sol index 2fc5052..f230931 100644 --- a/onchain/test/FitnessEvaluator.t.sol +++ b/onchain/test/FitnessEvaluator.t.sol @@ -163,7 +163,7 @@ contract FitnessEvaluator is Test { /// @dev Account 8 — adversary (10 000 ETH in Anvil; funded via vm.deal here) uint256 internal constant ADV_PK = 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97; - /// @dev Account 2 — recenter caller (granted recenterAccess in bootstrap) + /// @dev Account 2 — recenter caller (recenter() is now permissionless) uint256 internal constant RECENTER_PK = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; // ─── Runtime state ──────────────────────────────────────────────────────── @@ -248,7 +248,7 @@ contract FitnessEvaluator is Test { bytes32 ERC1967_IMPL = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; vm.store(optProxy, ERC1967_IMPL, bytes32(uint256(uint160(IMPL_SLOT)))); - // Bootstrap: fund LM, set recenterAccess, initial recenter. + // Bootstrap: fund LM, initial recenter. if (!_bootstrap()) { console.log(string.concat('{"candidate_id":"', candidateId, '","fitness":0,"error":"bootstrap_failed"}')); continue; @@ -367,18 +367,14 @@ contract FitnessEvaluator is Test { * @notice Bootstrap LM state for a candidate evaluation (mirrors fitness.sh bootstrap). * * Steps (same order as fitness.sh): - * a. Grant recenterAccess to recenterAddr (impersonate feeDestination). - * b. Fund adversary account and wrap ETH → WETH. - * c. Transfer 1000 WETH to LM. - * d. Wrap 9000 WETH for adversary trades + set approvals. - * e. Initial recenter (succeeds immediately: recenterAccess set, no ANCHOR liquidity yet). + * a. Fund adversary account and wrap ETH → WETH. + * b. Transfer 1000 WETH to LM. + * c. Wrap 9000 WETH for adversary trades + set approvals. + * d. Initial recenter (callable by anyone: cooldown passes because block.timestamp on a + * Base fork is a large value >> 60; TWAP passes because the pool has existing history). */ function _bootstrap() internal returns (bool) { - // a. Grant recenterAccess (feeDestination call, no ETH needed with gas_price=0). - vm.prank(FEE_DEST); - LiquidityManager(payable(lmAddr)).setRecenterAccess(recenterAddr); - - // b. Fund adversary with ETH. + // a. Fund adversary with ETH. vm.deal(advAddr, 10_000 ether); // c. Wrap 1000 ETH → WETH and send to LM. @@ -397,8 +393,8 @@ contract FitnessEvaluator is Test { IERC20(krkAddr).approve(NPM_ADDR, type(uint256).max); vm.stopPrank(); - // e. Initial recenter: no ANCHOR position exists yet so amplitude check is skipped; - // recenterAccess is set so TWAP stability check is also skipped. + // d. Initial recenter: no ANCHOR position exists yet so amplitude check is skipped. + // Cooldown passes (Base fork timestamp >> 60). TWAP passes (existing pool history). // If all retries fail, revert with a clear message — silent failure would make every // candidate score identically (all lm_eth_total = free WETH only, no positions). bool recentered = false; diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index dd9d4ad..5997797 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -406,12 +406,6 @@ contract LiquidityManagerTest is UniSwapHelper { _skipAutoSetup = true; } - /// @notice Grant recenter access for testing (commonly needed) - function _grantRecenterAccess() internal { - vm.prank(feeDestination); - lm.setRecenterAccess(RECENTER_CALLER); - } - /// @notice Setup with custom parameters but standard flow function _setupCustom(bool token0IsWeth, uint256 accountBalance) internal { disableAutoSetup(); @@ -450,10 +444,6 @@ contract LiquidityManagerTest is UniSwapHelper { vm.prank(account); weth.deposit{ value: 15_000 ether }(); - // Grant recenter access - vm.prank(feeDestination); - lm.setRecenterAccess(RECENTER_CALLER); - // Setup approvals without creating blocking positions vm.startPrank(account); weth.approve(address(lm), type(uint256).max); @@ -947,58 +937,25 @@ contract LiquidityManagerTest is UniSwapHelper { } // ========================================================= - // COVERAGE TESTS: onlyFeeDestination, revokeRecenterAccess, - // open recenter path, VWAP else branch, + // COVERAGE TESTS: cooldown check, TWAP oracle path, VWAP else branch, // optimizer fallback, _getKraikenToken/_getWethToken // ========================================================= /** - * @notice Calling an onlyFeeDestination function from a non-fee address must revert - */ - function testOnlyFeeDestinationReverts() public { - address nonFee = makeAddr("notFeeDestination"); - vm.expectRevert("only callable by feeDestination"); - vm.prank(nonFee); - lm.setRecenterAccess(nonFee); - } - - /** - * @notice feeDestination can revoke recenter access (covers revokeRecenterAccess body) - */ - function testRevokeRecenterAccess() public { - assertEq(lm.recenterAccess(), RECENTER_CALLER, "precondition: access should be set"); - - vm.prank(feeDestination); - lm.revokeRecenterAccess(); - - assertEq(lm.recenterAccess(), address(0), "recenterAccess should be revoked"); - } - - /** - * @notice Open recenter (no access restriction) must fail with cooldown if called too soon + * @notice recenter() must fail with cooldown if called too soon after the last recenter */ function testOpenRecenterCooldown() public { - vm.prank(feeDestination); - lm.revokeRecenterAccess(); - // Immediately try to recenter without waiting — should hit cooldown check vm.expectRevert("recenter cooldown"); lm.recenter(); } /** - * @notice After cooldown, open recenter calls _isPriceStable (covering _getPool) then - * hits amplitude check (covers the open-recenter else branch, lines 141-142, 265-266) - * @dev PriceOracle._isPriceStable has a 60,000-second fallback interval. - * setUp warps ~18,000s so the pool's history is only ~18,000s. - * We warp an additional 61,000s so pool history > 60,000s for the fallback to succeed. + * @notice After cooldown, recenter() calls _isPriceStable (covering _getPool) then + * hits amplitude check when price has not moved since last recenter */ function testOpenRecenterOracleCheck() public { - vm.prank(feeDestination); - lm.revokeRecenterAccess(); - - // Warp enough seconds so pool.observe([300,0]) and its fallback ([60000,0]) both succeed. - // Pool was initialized at timestamp 1; after setUp + this warp: ~79,001s of history. + // Warp enough seconds for cooldown + TWAP window (300s). vm.warp(block.timestamp + 61_000); // _isPriceStable (→ _getPool) is called; price unchanged → stable. @@ -1133,6 +1090,9 @@ contract LiquidityManagerTest is UniSwapHelper { // Move price up with a buy so the second recenter satisfies amplitude requirement buyRaw(10 ether); + // Warp past cooldown interval; also lets TWAP settle at the post-buy price. + vm.warp(block.timestamp + 301); + // Second recenter: _scrapePositions() burns positions and collects principal KRK // into the LM's balance. _setPositions() then calls _getOutstandingSupply(). // Without the fix: outstandingSupply() already excludes balanceOf(lm), and diff --git a/onchain/test/SupplyCorruption.t.sol b/onchain/test/SupplyCorruption.t.sol index 113ebb2..41ccbaf 100644 --- a/onchain/test/SupplyCorruption.t.sol +++ b/onchain/test/SupplyCorruption.t.sol @@ -83,6 +83,7 @@ contract SupplyCorruptionTest is UniSwapHelper { performSwap(5 ether, true); console.log("Performed 5 ETH swap to move price"); + vm.warp(block.timestamp + 301); // TWAP catches up to post-swap price; cooldown passes // Call recenter vm.prank(RECENTER_CALLER); @@ -126,6 +127,7 @@ contract SupplyCorruptionTest is UniSwapHelper { console.log("Initial supply:", initialTotalSupply); // Perform multiple recenter cycles + uint256 ts = block.timestamp; // track time explicitly to avoid Forge block.timestamp reset for (uint256 i = 0; i < 3; i++) { // Swap to move price vm.deal(account, 2 ether); @@ -133,6 +135,8 @@ contract SupplyCorruptionTest is UniSwapHelper { weth.deposit{ value: 2 ether }(); performSwap(2 ether, true); + ts += 301; // TWAP catches up; cooldown passes + vm.warp(ts); vm.prank(RECENTER_CALLER); lm.recenter(); diff --git a/onchain/test/VWAPFloorProtection.t.sol b/onchain/test/VWAPFloorProtection.t.sol index 95bbc22..f25400f 100644 --- a/onchain/test/VWAPFloorProtection.t.sol +++ b/onchain/test/VWAPFloorProtection.t.sol @@ -69,6 +69,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper { // ---- step 2: first buy + recenter → bootstrap ---- buyRaw(25 ether); // push price up enough to satisfy amplitude check + vm.warp(block.timestamp + 301); // TWAP catches up to post-buy price; cooldown passes vm.prank(RECENTER_CALLER); lm.recenter(); // cumulativeVolume == 0 → shouldRecordVWAP = true (bootstrap path) @@ -78,8 +79,11 @@ contract VWAPFloorProtectionTest is UniSwapHelper { // ---- step 3: continued buy-only cycles ---- uint256 successfulBuyCycles; + uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset for (uint256 i = 0; i < 10; i++) { buyRaw(25 ether); + ts += 301; // TWAP catches up; cooldown passes + vm.warp(ts); vm.prank(RECENTER_CALLER); // Recenter may fail if amplitude isn't reached; that's fine. try lm.recenter() { @@ -114,12 +118,16 @@ contract VWAPFloorProtectionTest is UniSwapHelper { // Bootstrap via first buy-recenter buyRaw(25 ether); + vm.warp(block.timestamp + 301); // TWAP catches up; cooldown passes vm.prank(RECENTER_CALLER); lm.recenter(); // Run several buy cycles + uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset for (uint256 i = 0; i < 6; i++) { buyRaw(25 ether); + ts += 301; // TWAP catches up; cooldown passes + vm.warp(ts); vm.prank(RECENTER_CALLER); try lm.recenter() { } catch { } } @@ -160,6 +168,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper { assertEq(lm.cumulativeVolume(), 0, "no VWAP data before first fees"); buyRaw(25 ether); + vm.warp(block.timestamp + 301); // TWAP catches up to post-buy price; cooldown passes vm.prank(RECENTER_CALLER); lm.recenter(); @@ -188,7 +197,10 @@ contract VWAPFloorProtectionTest is UniSwapHelper { vm.prank(RECENTER_CALLER); lm.recenter(); + uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset buyRaw(25 ether); + ts += 301; // TWAP catches up to post-buy price; cooldown passes + vm.warp(ts); vm.prank(RECENTER_CALLER); lm.recenter(); @@ -199,6 +211,8 @@ contract VWAPFloorProtectionTest is UniSwapHelper { } // Recenter with price now lower (sell direction) — must not revert + ts += 301; // TWAP catches up to post-sell price; cooldown passes + vm.warp(ts); vm.prank(RECENTER_CALLER); try lm.recenter() { // success — sell-direction recenter works @@ -239,6 +253,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper { // 25 ether against a 100 ETH LM pool reliably satisfies the amplitude check // (same amount used across other bootstrap tests in this file). buyRaw(25 ether); + vm.warp(block.timestamp + 301); // TWAP catches up to post-buy price; cooldown passes // Step 3: Second recenter — bootstrap path records VWAP. vm.prank(RECENTER_CALLER); diff --git a/onchain/test/helpers/TestBase.sol b/onchain/test/helpers/TestBase.sol index 52d996b..d3ab9b6 100644 --- a/onchain/test/helpers/TestBase.sol +++ b/onchain/test/helpers/TestBase.sol @@ -132,10 +132,6 @@ contract TestEnvironment is TestConstants { // Configure permissions _configurePermissions(); - // Grant recenter access to specified caller - vm.prank(feeDestination); - lm.setRecenterAccess(recenterCaller); - return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } @@ -172,11 +168,14 @@ contract TestEnvironment is TestConstants { /** * @notice Create and initialize the Uniswap pool + * @dev Warp 301 seconds after pool init so _isPriceStable()'s 300-second TWAP window + * has sufficient history for any subsequent recenter() call. */ function _createAndInitializePool() internal { pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); token0isWeth = address(weth) < address(harberg); pool.initializePoolFor1Cent(token0isWeth); + vm.warp(block.timestamp + 301); } /** @@ -248,10 +247,6 @@ contract TestEnvironment is TestConstants { // Configure permissions _configurePermissions(); - // Grant recenter access to specified caller - vm.prank(feeDestination); - lm.setRecenterAccess(recenterCaller); - return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } @@ -299,10 +294,6 @@ contract TestEnvironment is TestConstants { harberg.setLiquidityManager(address(lm)); vm.deal(address(lm), INITIAL_LM_ETH_BALANCE); - // feeDestination IS address(lm), so prank as lm to grant recenter access - vm.prank(address(lm)); - lm.setRecenterAccess(recenterCaller); - return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } @@ -357,10 +348,6 @@ contract TestEnvironment is TestConstants { // Configure permissions _configurePermissions(); - // Grant recenter access to specified caller - vm.prank(feeDestination); - lm.setRecenterAccess(recenterCaller); - return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } } diff --git a/scripts/bootstrap-common.sh b/scripts/bootstrap-common.sh index a67df08..7e628d2 100755 --- a/scripts/bootstrap-common.sh +++ b/scripts/bootstrap-common.sh @@ -100,21 +100,6 @@ fund_liquidity_manager() { "$LIQUIDITY_MANAGER" --value 10ether >>"$LOG_FILE" 2>&1 } -grant_recenter_access() { - bootstrap_log "Granting recenter access to deployer" - cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1 - cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \ - "$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$DEPLOYER_ADDR" >>"$LOG_FILE" 2>&1 - cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1 - - if [[ -n "${TXNBOT_ADDRESS:-}" ]]; then - bootstrap_log "Granting recenter access to txnBot ($TXNBOT_ADDRESS)" - cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1 - cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \ - "$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$TXNBOT_ADDRESS" >>"$LOG_FILE" 2>&1 - cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1 - fi -} call_recenter() { local recenter_pk="$DEPLOYER_PK" diff --git a/scripts/ci-bootstrap.sh b/scripts/ci-bootstrap.sh index 1ffeac6..ec991ff 100755 --- a/scripts/ci-bootstrap.sh +++ b/scripts/ci-bootstrap.sh @@ -57,9 +57,6 @@ cat "$ONCHAIN_DIR/deployments-local.json" echo "=== Funding LiquidityManager ===" fund_liquidity_manager -echo "=== Granting recenter access ===" -grant_recenter_access - echo "=== Calling recenter() to seed liquidity ===" call_recenter diff --git a/scripts/harb-evaluator/red-team.sh b/scripts/harb-evaluator/red-team.sh index 950c389..427f97e 100755 --- a/scripts/harb-evaluator/red-team.sh +++ b/scripts/harb-evaluator/red-team.sh @@ -33,7 +33,7 @@ DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json" # ── Anvil accounts ───────────────────────────────────────────────────────────── # Account 8 — adversary (10k ETH, 0 KRK) ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 -# Account 2 — recenter caller (granted recenterAccess by bootstrap) +# Account 2 — recenter caller (recenter() is permissionless; any account can call it) RECENTER_PK=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a # ── Infrastructure constants ─────────────────────────────────────────────────── @@ -116,21 +116,8 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \ "$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g') log " Pool: $POOL" -# ── 3a. Grant recenterAccess FIRST (while original feeDestination is still set) ── -FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \ - || die "Failed to read feeDestination() from LM" -FEE_DEST=$(echo "$FEE_DEST" | sed 's/\[.*//;s/[[:space:]]//g') -log "Granting recenterAccess to account 2 ($RECENTER_ADDR) via feeDestination ($FEE_DEST) ..." -"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \ - || die "anvil_impersonateAccount $FEE_DEST failed" -"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \ - "$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \ - || die "setRecenterAccess($RECENTER_ADDR) failed" -"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \ - || die "anvil_stopImpersonatingAccount $FEE_DEST failed" -log " recenterAccess granted" - -# ── 3b. Set feeDestination to LM itself (fees accrue as liquidity) ───────────── +# ── 3a. Set feeDestination to LM itself (fees accrue as liquidity) ───────────── +# recenter() is now permissionless — no setRecenterAccess() call needed. # setFeeDestination allows repeated EOA sets; setting to a contract locks it permanently. # The deployer (Anvil account 0) deployed LiquidityManager and may call setFeeDestination again. # DEPLOYER_PK is Anvil's deterministic account-0 key — valid ONLY against a local ephemeral @@ -144,7 +131,7 @@ VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | s log " feeDestination set to: $VERIFY" [[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY" -# ── 3c. Fund LM with 1000 ETH and deploy into positions via recenter ─────────── +# ── 3b. Fund LM with 1000 ETH and deploy into positions via recenter ─────────── # Send ETH as WETH (LM uses WETH internally), then recenter to deploy into positions. # Without recenter, the ETH sits idle and the first recenter mints massive KRK. log "Funding LM with 1000 ETH ..." @@ -174,7 +161,7 @@ LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:] LM_WETH=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g') log " LM after recenter: ETH=$LM_ETH WETH=$LM_WETH" -# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ───── +# ── 4. Take Anvil snapshot (clean baseline) ──────────────────────────────────── log "Taking Anvil snapshot..." SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"') log " Snapshot ID: $SNAP" @@ -419,7 +406,7 @@ CAST binary: /home/debian/.foundry/bin/cast ### Recenter caller — Anvil account 2 - Address: ${RECENTER_ADDR} - Private key: ${RECENTER_PK} -- Has recenterAccess on LiquidityManager +- Can call recenter() (permissionless — 60s cooldown + TWAP check enforced) --- @@ -446,7 +433,7 @@ to rebalance, then re-deploys positions at the current price. It: - Can mint NEW KRK (increasing supply → decreasing floor) - Can burn KRK (decreasing supply → increasing floor) - Moves ETH between positions -Only recenterAccess account can call it. +recenter() is permissionless — any account can call it (subject to 60s cooldown and TWAP check). ### Staking \`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\` From df2f0a87e5542328037bd9ea48eb75c9abdf7943 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 13 Mar 2026 22:45:22 +0000 Subject: [PATCH 2/5] 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 --- onchain/script/DeployLocal.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/onchain/script/DeployLocal.sol b/onchain/script/DeployLocal.sol index bf9e696..9cfa464 100644 --- a/onchain/script/DeployLocal.sol +++ b/onchain/script/DeployLocal.sol @@ -141,7 +141,10 @@ contract DeployLocal is Script { console.log("\n[7/7] Bootstrapping VWAP with seed trade..."); // Step 1: Advance time so TWAP oracle has sufficient history. - vm.warp(block.timestamp + 301); + // 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 }(""); @@ -155,7 +158,8 @@ contract DeployLocal is Script { console.log(" Seed buy executed -> fee generated in anchor position"); // Step 4: Warp forward so TWAP settles at post-buy price and cooldown elapses. - vm.warp(block.timestamp + 301); + ts += 301; + vm.warp(ts); // Step 5: Second recenter records VWAP (bootstrap path + ethFee > 0). liquidityManager.recenter(); From 02b055ceb9c105f42ccfb3f91cdba3e1b1f55ff7 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 13 Mar 2026 23:28:52 +0000 Subject: [PATCH 3/5] fix: move VWAP bootstrap from forge script to bootstrap-common.sh vm.warp in forge script --broadcast only affects the local simulation phase, not the actual Anvil node. The pool.observe([300,0]) call in recenter() therefore reverted with OLD when Forge pre-flighted the broadcast transactions on Anvil. Fix: - Remove the vm.warp + 2-recenter + SeedSwapper VWAP bootstrap from DeployLocal.sol (only contract deployment now, simpler and reliable). - Add bootstrap_vwap() to bootstrap-common.sh that uses Anvil RPC evm_increaseTime + evm_mine to advance chain time before each recenter, then executes a 0.5 ETH WETH->KRK seed swap between them. - Call bootstrap_vwap() before fund_liquidity_manager() in both containers/bootstrap.sh and ci-bootstrap.sh so the LM is seeded with thin positions (1 ETH) during bootstrap, ensuring the 0.5 ETH swap moves the price >400 ticks (amplitude gate). Co-Authored-By: Claude Sonnet 4.6 --- containers/bootstrap.sh | 2 +- onchain/script/DeployLocal.sol | 90 +++++----------------------------- scripts/bootstrap-common.sh | 75 +++++++++++++++++++++------- scripts/ci-bootstrap.sh | 6 +-- 4 files changed, 73 insertions(+), 100 deletions(-) diff --git a/containers/bootstrap.sh b/containers/bootstrap.sh index d2246ec..0160018 100755 --- a/containers/bootstrap.sh +++ b/containers/bootstrap.sh @@ -135,8 +135,8 @@ main() { run_forge_script extract_addresses write_contracts_env + bootstrap_vwap fund_liquidity_manager - call_recenter seed_application_state write_deployments_json write_ponder_env diff --git a/onchain/script/DeployLocal.sol b/onchain/script/DeployLocal.sol index 9cfa464..ced4bed 100644 --- a/onchain/script/DeployLocal.sol +++ b/onchain/script/DeployLocal.sol @@ -11,7 +11,6 @@ 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 @@ -28,17 +27,6 @@ contract DeployLocal is Script { 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; @@ -60,7 +48,7 @@ contract DeployLocal is Script { // Deploy Kraiken token kraiken = new Kraiken("Kraiken", "KRK"); - console.log("\n[1/7] Kraiken deployed:", address(kraiken)); + console.log("\n[1/6] Kraiken deployed:", address(kraiken)); // Determine token ordering token0isWeth = address(weth) < address(kraiken); @@ -68,7 +56,7 @@ contract DeployLocal is Script { // Deploy Stake contract stake = new Stake(address(kraiken), feeDest); - console.log("\n[2/7] Stake deployed:", address(stake)); + console.log("\n[2/6] Stake deployed:", address(stake)); // Set staking pool in Kraiken kraiken.setStakingPool(address(stake)); @@ -79,9 +67,9 @@ contract DeployLocal is Script { 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); + console.log("\n[3/6] Uniswap pool created:", liquidityPool); } else { - console.log("\n[3/7] Using existing pool:", liquidityPool); + console.log("\n[3/6] Using existing pool:", liquidityPool); } pool = IUniswapV3Pool(liquidityPool); @@ -103,74 +91,22 @@ contract DeployLocal is Script { 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); + console.log("\n[4/6] Optimizer deployed:", optimizerAddress); // Deploy LiquidityManager liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress); - console.log("\n[5/7] LiquidityManager deployed:", address(liquidityManager)); + console.log("\n[5/6] 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. + // Set the real feeDestination. liquidityManager.setFeeDestination(feeDest); + + console.log("\n[6/6] Configuration complete"); console.log(" feeDestination set to", feeDest); + console.log(" VWAP bootstrap will be performed by the bootstrap script"); // Print deployment summary console.log("\n=== Deployment Summary ==="); @@ -181,10 +117,10 @@ contract DeployLocal is Script { 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("1. bootstrap-common.sh bootstrap_vwap() advances chain time and seeds VWAP."); + console.log("2. Fund LiquidityManager with operational ETH:"); 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("3. recenter() is permissionless - any address (e.g. txnBot) can call it."); console.log(" TWAP manipulation protection is always enforced (no bypass path)."); vm.stopBroadcast(); diff --git a/scripts/bootstrap-common.sh b/scripts/bootstrap-common.sh index 7e628d2..3898a2c 100755 --- a/scripts/bootstrap-common.sh +++ b/scripts/bootstrap-common.sh @@ -101,34 +101,71 @@ fund_liquidity_manager() { } -call_recenter() { - local recenter_pk="$DEPLOYER_PK" - local recenter_addr="$DEPLOYER_ADDR" - if [[ -n "${TXNBOT_ADDRESS:-}" ]]; then - recenter_pk="$TXNBOT_PRIVATE_KEY" - recenter_addr="$TXNBOT_ADDRESS" - fi - - # If the deploy script already bootstrapped VWAP (cumulativeVolume > 0), positions - # are in place at the post-seed-buy tick. Calling recenter() now would fail with - # "amplitude not reached" because currentTick == anchorCenterTick. Skip it. +bootstrap_vwap() { + # Idempotency guard: if a previous run already bootstrapped VWAP, skip. local cumvol cumvol="$(cast call --rpc-url "$ANVIL_RPC" \ "$LIQUIDITY_MANAGER" "cumulativeVolume()(uint256)" 2>/dev/null || echo "0")" - # cast call with a typed (uint256) selector returns a plain decimal string for - # non-zero values (e.g. "140734553600000") and "0" for zero. A simple != "0" - # check is sufficient; note that the output may include a scientific-notation - # annotation (e.g. "140734553600000 [1.407e14]") which is also != "0", so we - # do NOT attempt to parse it further with cast to-dec (which would fail on the - # annotation and incorrectly fall back to "0"). if [[ "$cumvol" != "0" && -n "$cumvol" ]]; then - bootstrap_log "VWAP already bootstrapped by deploy script (cumulativeVolume=$cumvol) -- skipping initial recenter" + bootstrap_log "VWAP already bootstrapped (cumulativeVolume=$cumvol) -- skipping" return 0 fi - bootstrap_log "Calling recenter() via $recenter_addr" + local recenter_pk="${TXNBOT_PRIVATE_KEY:-$DEPLOYER_PK}" + + # Fund LM with 1 ETH (thin bootstrap positions; 0.5 ETH seed swap moves >400 ticks) + bootstrap_log "Funding LM with 1 ETH for VWAP bootstrap..." + cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ + "$LIQUIDITY_MANAGER" --value 1ether >>"$LOG_FILE" 2>&1 + + # Advance Anvil time 301s so TWAP oracle has sufficient history for _isPriceStable() + cast rpc --rpc-url "$ANVIL_RPC" evm_increaseTime 301 >>"$LOG_FILE" 2>&1 + cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 + + # First recenter: places initial bootstrap positions; no fees yet, cumulativeVolume stays 0 + bootstrap_log "First recenter (places bootstrap positions)..." cast send --rpc-url "$ANVIL_RPC" --private-key "$recenter_pk" \ "$LIQUIDITY_MANAGER" "recenter()" >>"$LOG_FILE" 2>&1 + + # Seed buy: wrap 0.5 ETH to WETH and swap WETH->KRK + # Generates a non-zero WETH fee in the anchor position and moves price >400 ticks. + # sqrtPriceLimitX96 is direction-dependent: MIN+1 when WETHKRK)..." + cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ + "$WETH" "deposit()" --value 0.5ether >>"$LOG_FILE" 2>&1 + cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ + "$WETH" "approve(address,uint256)" "$SWAP_ROUTER" "$MAX_UINT" >>"$LOG_FILE" 2>&1 + + local weth_addr kraiken_addr sqrt_limit + weth_addr=$(echo "$WETH" | tr '[:upper:]' '[:lower:]' | sed 's/^0x//') + kraiken_addr=$(echo "$KRAIKEN" | tr '[:upper:]' '[:lower:]' | sed 's/^0x//') + if [[ "$weth_addr" < "$kraiken_addr" ]]; then + sqrt_limit=4295128740 # WETH=token0, zeroForOne=true, price decreases + else + sqrt_limit=1461446703485210103287273052203988822378723970341 # WETH=token1, price increases + fi + + cast send --legacy --gas-limit 300000 --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ + "$SWAP_ROUTER" "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \ + "($WETH,$KRAIKEN,10000,$DEPLOYER_ADDR,500000000000000000,0,$sqrt_limit)" >>"$LOG_FILE" 2>&1 + + # Advance time 301s so TWAP settles at post-buy price and cooldown (60s) elapses + cast rpc --rpc-url "$ANVIL_RPC" evm_increaseTime 301 >>"$LOG_FILE" 2>&1 + cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 + + # Second recenter: cumulativeVolume==0 path fires (bootstrap), ethFee>0 -> records VWAP + bootstrap_log "Second recenter (records VWAP)..." + cast send --rpc-url "$ANVIL_RPC" --private-key "$recenter_pk" \ + "$LIQUIDITY_MANAGER" "recenter()" >>"$LOG_FILE" 2>&1 + + # Verify VWAP bootstrap succeeded + cumvol="$(cast call --rpc-url "$ANVIL_RPC" \ + "$LIQUIDITY_MANAGER" "cumulativeVolume()(uint256)" 2>/dev/null || echo "0")" + if [[ "$cumvol" == "0" || -z "$cumvol" ]]; then + bootstrap_log "ERROR: VWAP bootstrap failed -- cumulativeVolume is 0" + return 1 + fi + bootstrap_log "VWAP bootstrapped (cumulativeVolume=$cumvol)" } seed_application_state() { diff --git a/scripts/ci-bootstrap.sh b/scripts/ci-bootstrap.sh index ec991ff..c32e3db 100755 --- a/scripts/ci-bootstrap.sh +++ b/scripts/ci-bootstrap.sh @@ -54,12 +54,12 @@ write_deployments_json "$ONCHAIN_DIR/deployments-local.json" echo "=== deployments-local.json written ===" cat "$ONCHAIN_DIR/deployments-local.json" +echo "=== Bootstrapping VWAP ===" +bootstrap_vwap + echo "=== Funding LiquidityManager ===" fund_liquidity_manager -echo "=== Calling recenter() to seed liquidity ===" -call_recenter - echo "=== Seeding application state (initial swap) ===" seed_application_state From 9b53f409b770510bf55e616b6c9c0ab2ec3e2b73 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 14 Mar 2026 01:44:15 +0000 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20update=20e2e=20tests=20for=20public?= =?UTF-8?q?=20recenter()=20=E2=80=94=20remove=20recenterAccess=20reference?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recenterAccess() was removed from LiquidityManager in this PR. The old tests called recenterAccess() (selector 0xdef51130) which now reverts, causing both recenter tests to fail. Update tests to match the new public recenter() behavior: - Test 1: verify any address may call recenter() without "access denied" - Test 2: same caller pattern, guard errors are still acceptable Co-Authored-By: Claude Sonnet 4.6 --- tests/e2e/04-recenter-positions.spec.ts | 49 ++++++++++--------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/tests/e2e/04-recenter-positions.spec.ts b/tests/e2e/04-recenter-positions.spec.ts index 9b504d6..aa82106 100644 --- a/tests/e2e/04-recenter-positions.spec.ts +++ b/tests/e2e/04-recenter-positions.spec.ts @@ -7,7 +7,6 @@ const STACK_RPC_URL = STACK_CONFIG.rpcUrl; // Solidity function selectors const POSITIONS_SELECTOR = '0xf86aafc0'; // positions(uint8) const RECENTER_SELECTOR = '0xf46e1346'; // recenter() -const RECENTER_ACCESS_SELECTOR = '0xdef51130'; // recenterAccess() // Position stages (matches ThreePositionStrategy.Stage enum) const STAGE_FLOOR = 0; @@ -102,50 +101,42 @@ test.describe('Recenter Positions', () => { console.log('[TEST] All three positions have non-zero liquidity'); }); - test('recenter() enforces access control', async () => { + test('recenter() is public — any address may attempt it', async () => { const lmAddress = STACK_CONFIG.contracts.LiquidityManager; - // Read the recenterAccess address - const recenterAccessResult = (await rpcCall('eth_call', [ - { to: lmAddress, data: RECENTER_ACCESS_SELECTOR }, - 'latest', - ])) as string; - const recenterAddr = '0x' + recenterAccessResult.slice(26); - console.log(`[TEST] recenterAccess: ${recenterAddr}`); - expect(recenterAddr).not.toBe('0x' + '0'.repeat(40)); - console.log('[TEST] recenterAccess is set (not zero address)'); - - // Try calling recenter from an unauthorized address — should revert with "access denied" - const unauthorizedAddr = '0x1111111111111111111111111111111111111111'; + // recenter() is now public: anyone can call it (recenterAccess was removed). + // After bootstrap the cooldown and amplitude guards will typically fire, + // but the revert reason must NOT be "access denied". + const callerAddr = '0x1111111111111111111111111111111111111111'; const callResult = await rpcCallRaw('eth_call', [ - { from: unauthorizedAddr, to: lmAddress, data: RECENTER_SELECTOR }, + { from: callerAddr, to: lmAddress, data: RECENTER_SELECTOR }, 'latest', ]); - expect(callResult.error).toBeDefined(); - expect(callResult.error!.message).toContain('access denied'); - console.log('[TEST] Unauthorized recenter correctly rejected with "access denied"'); + if (callResult.error) { + // Acceptable guard errors: cooldown, amplitude, TWAP — NOT access control + const msg = callResult.error.message ?? ''; + expect(msg).not.toContain('access denied'); + console.log(`[TEST] Recenter guard active (expected): ${msg}`); + console.log('[TEST] No "access denied" — access control correctly removed'); + } else { + console.log('[TEST] Recenter succeeded from arbitrary address — access control is gone'); + } }); test('recenter() enforces amplitude check', async () => { const lmAddress = STACK_CONFIG.contracts.LiquidityManager; - // Read the recenterAccess address - const recenterAccessResult = (await rpcCall('eth_call', [ - { to: lmAddress, data: RECENTER_ACCESS_SELECTOR }, - 'latest', - ])) as string; - const recenterAddr = '0x' + recenterAccessResult.slice(26); - - // Call recenter from the authorized address without moving the price - // Should revert with "amplitude not reached" since price hasn't moved enough + // Call recenter from any address without moving the price. + // Should revert with a guard error (cooldown, amplitude, or TWAP), not crash. + const callerAddr = '0x1111111111111111111111111111111111111111'; const callResult = await rpcCallRaw('eth_call', [ - { from: recenterAddr, to: lmAddress, data: RECENTER_SELECTOR }, + { from: callerAddr, to: lmAddress, data: RECENTER_SELECTOR }, 'latest', ]); // After bootstrap's initial swap + recenter, calling recenter again may either: - // - Fail with "amplitude not reached" if price hasn't moved enough + // - Fail with "amplitude not reached" / "recenter cooldown" / "price deviated from oracle" // - Succeed if contract's amplitude threshold allows it (e.g., after swap moved price) // Both outcomes are valid — the key invariant is that recenter doesn't crash unexpectedly if (callResult.error) { From 0d3aee15b44bc6f7e37224bde445da7375e13fe3 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 14 Mar 2026 09:15:48 +0000 Subject: [PATCH 5/5] fix: address AI review findings for #706 recenterAccess removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DeployBase.sol: remove broken inline second recenter() (would always revert with 'recenter cooldown' in same Forge broadcast); replace with operator instructions to run the new BootstrapVWAPPhase2.s.sol script at least 60 s after deployment - BootstrapVWAPPhase2.s.sol: new script for the second VWAP bootstrap recenter on Base mainnet deployments - StrategyExecutor.sol: update stale docstring that still described the removed recenterAccess bypass; reflect permissionless model with vm.warp - TestBase.sol: remove vestigial recenterCaller parameter from all four setupEnvironment* functions (parameter was silently ignored after setRecenterAccess was removed); update all callers across six test files - bootstrap-common.sh: fix misleading retry recenter in seed_application_state() — add evm_increaseTime 61 before evm_mine so the recenter cooldown actually clears and the retry can succeed All 210 tests pass. Co-Authored-By: Claude Sonnet 4.6 --- onchain/script/BootstrapVWAPPhase2.s.sol | 45 +++++++++++++++++++ onchain/script/DeployBase.sol | 16 +++---- .../script/backtesting/StrategyExecutor.sol | 6 +-- onchain/test/EthScarcityAbundance.t.sol | 6 +-- onchain/test/FuzzingAnalyzerBugs.t.sol | 2 +- onchain/test/LiquidityManager.t.sol | 7 ++- onchain/test/ReplayProfitableScenario.t.sol | 2 +- onchain/test/SupplyCorruption.t.sol | 2 +- onchain/test/VWAPFloorProtection.t.sol | 2 +- onchain/test/helpers/TestBase.sol | 21 ++------- scripts/bootstrap-common.sh | 4 +- 11 files changed, 71 insertions(+), 42 deletions(-) create mode 100644 onchain/script/BootstrapVWAPPhase2.s.sol diff --git a/onchain/script/BootstrapVWAPPhase2.s.sol b/onchain/script/BootstrapVWAPPhase2.s.sol new file mode 100644 index 0000000..dcd7f0d --- /dev/null +++ b/onchain/script/BootstrapVWAPPhase2.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import { LiquidityManager } from "../src/LiquidityManager.sol"; +import "forge-std/Script.sol"; + +/** + * @title BootstrapVWAPPhase2 + * @notice Second phase of the VWAP bootstrap for Base mainnet deployments. + * + * Run this script >= 60 seconds after DeployBase (or DeployBaseMainnet/DeployBaseSepolia) + * finishes. The first recenter() sets lastRecenterTime; the 60-second cooldown must + * elapse before this second recenter() can succeed. + * + * What this does: + * - Calls liquidityManager.recenter() a second time. + * - At this point cumulativeVolume == 0 (bootstrap path) and the seed buy has + * generated ethFee > 0, so recenter() records the VWAP anchor. + * - Asserts cumulativeVolume > 0 to confirm bootstrap success. + * + * Usage: + * export LM_ADDRESS= + * forge script script/BootstrapVWAPPhase2.s.sol --tc BootstrapVWAPPhase2 \ + * --fork-url $BASE_RPC --broadcast + */ +contract BootstrapVWAPPhase2 is Script { + function run() public { + address lmAddress = vm.envAddress("LM_ADDRESS"); + LiquidityManager lm = LiquidityManager(payable(lmAddress)); + + string memory seedPhrase = vm.readFile(".secret"); + uint256 privateKey = vm.deriveKey(seedPhrase, 0); + vm.startBroadcast(privateKey); + + console.log("Running VWAP bootstrap phase 2 on LiquidityManager:", lmAddress); + + lm.recenter(); + + uint256 cumVol = lm.cumulativeVolume(); + require(cumVol > 0, "VWAP bootstrap failed: cumulativeVolume is still 0"); + console.log("VWAP bootstrapped successfully. cumulativeVolume:", cumVol); + + vm.stopBroadcast(); + } +} diff --git a/onchain/script/DeployBase.sol b/onchain/script/DeployBase.sol index 9565a66..5b524ef 100644 --- a/onchain/script/DeployBase.sol +++ b/onchain/script/DeployBase.sol @@ -137,12 +137,9 @@ contract DeployBase is Script { console.log("Seed buy executed -> fee generated in anchor position"); // Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0). - // NOTE: Must be called >= 60s after the first recenter (cooldown) AND - // >= 300s after pool init so TWAP has settled at post-buy price. - // On Base mainnet, mine/wait ~5 min between Step 2 and Step 4. - liquidityManager.recenter(); - require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0"); - console.log("VWAP bootstrapped -> cumulativeVolume:", liquidityManager.cumulativeVolume()); + // Cannot be called in the same Forge broadcast as Step 2 — recenter() enforces a + // 60-second cooldown and there is no time-warp mechanism in a live broadcast. + // Run BootstrapVWAPPhase2.s.sol at least 60 seconds after this script completes. console.log("\n=== Deployment Complete ==="); console.log("Kraiken:", address(kraiken)); @@ -151,8 +148,11 @@ contract DeployBase is Script { console.log("LiquidityManager:", address(liquidityManager)); console.log("Optimizer:", optimizerAddress); console.log("\nPost-deploy steps:"); - console.log(" 1. Fund LiquidityManager with operational ETH (VWAP already bootstrapped)"); - console.log(" 2. recenter() is now permissionless - any address (e.g. txnBot) can call it."); + console.log(" 1. Wait >= 60 s after this script finishes."); + console.log(" 2. Run: forge script script/BootstrapVWAPPhase2.s.sol --tc BootstrapVWAPPhase2 --fork-url --broadcast"); + console.log(" This performs the second recenter that records cumulativeVolume > 0."); + console.log(" 3. Fund LiquidityManager with operational ETH."); + console.log(" 4. recenter() is permissionless - any address (e.g. txnBot) can call it."); vm.stopBroadcast(); } diff --git a/onchain/script/backtesting/StrategyExecutor.sol b/onchain/script/backtesting/StrategyExecutor.sol index 14708f5..9b3dcc6 100644 --- a/onchain/script/backtesting/StrategyExecutor.sol +++ b/onchain/script/backtesting/StrategyExecutor.sol @@ -24,9 +24,9 @@ import { console2 } from "forge-std/console2.sol"; * notified on every block (for time-in-range) and on each successful recenter * (for position lifecycle and fee/IL accounting). * - * Access model: StrategyExecutor must be set as recenterAccess on the LM so that - * the cooldown and TWAP price-stability checks are bypassed in the simulation - * (vm.warp advances simulated time, not real oracle state). + * Access model: recenter() is permissionless — no special access grant is required. + * EventReplayer advances block.timestamp via vm.warp, so the 60-second cooldown and + * the 300-second TWAP window pass normally during simulation. * * TODO(#319): The negligible-impact assumption means we replay historical events * as-is without accounting for KrAIken's own liquidity affecting swap outcomes. diff --git a/onchain/test/EthScarcityAbundance.t.sol b/onchain/test/EthScarcityAbundance.t.sol index e540334..95f126e 100644 --- a/onchain/test/EthScarcityAbundance.t.sol +++ b/onchain/test/EthScarcityAbundance.t.sol @@ -41,7 +41,7 @@ contract EthScarcityAbundance is Test { // Default params: CI=50%, AS=50%, AW=50, DD=50% optimizer = new ConfigurableOptimizer(5e17, 5e17, 50, 5e17); - (factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(optimizer)); + (factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(optimizer)); swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, true); @@ -231,7 +231,7 @@ contract EthScarcityAbundance is Test { // Bull optimizer: high anchorShare, wide anchor, deep discovery ConfigurableOptimizer bullOpt = new ConfigurableOptimizer(3e17, 8e17, 80, 8e17); - (,,,,, LiquidityManager bullLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(bullOpt)); + (,,,,, LiquidityManager bullLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(bullOpt)); vm.deal(address(bullLm), 200 ether); vm.prank(address(bullLm)); @@ -252,7 +252,7 @@ contract EthScarcityAbundance is Test { // Bear optimizer: low anchorShare, moderate anchor, thin discovery ConfigurableOptimizer bearOpt = new ConfigurableOptimizer(8e17, 1e17, 40, 2e17); - (,,,,, LiquidityManager bearLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(bearOpt)); + (,,,,, LiquidityManager bearLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(bearOpt)); vm.deal(address(bearLm), 200 ether); vm.prank(address(bearLm)); diff --git a/onchain/test/FuzzingAnalyzerBugs.t.sol b/onchain/test/FuzzingAnalyzerBugs.t.sol index e60772e..058741b 100644 --- a/onchain/test/FuzzingAnalyzerBugs.t.sol +++ b/onchain/test/FuzzingAnalyzerBugs.t.sol @@ -37,7 +37,7 @@ contract FuzzingAnalyzerBugs is Test { // Bear market params: CI=0.8e18, AS=0.1e18, AW=40, DD=0.2e18 optimizer = new ConfigurableOptimizer(8e17, 1e17, 40, 2e17); - (factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(optimizer)); + (factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(optimizer)); swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, true); diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 5997797..174c472 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -148,7 +148,7 @@ contract LiquidityManagerTest is UniSwapHelper { LiquidityManager _lm, Optimizer _optimizer, bool _token0isWeth - ) = testEnv.setupEnvironment(token0shouldBeWeth, RECENTER_CALLER); + ) = testEnv.setupEnvironment(token0shouldBeWeth); // Assign to state variables factory = _factory; @@ -1029,7 +1029,7 @@ contract LiquidityManagerTest is UniSwapHelper { function testOptimizerFallback() public { RevertingOptimizer revertingOpt = new RevertingOptimizer(); TestEnvironment env = new TestEnvironment(feeDestination); - (,,,,, LiquidityManager _lm,,) = env.setupEnvironmentWithOptimizer(DEFAULT_TOKEN0_IS_WETH, RECENTER_CALLER, address(revertingOpt)); + (,,,,, LiquidityManager _lm,,) = env.setupEnvironmentWithOptimizer(DEFAULT_TOKEN0_IS_WETH, address(revertingOpt)); // Recenter uses the fallback params from the catch block vm.prank(RECENTER_CALLER); @@ -1065,7 +1065,7 @@ contract LiquidityManagerTest is UniSwapHelper { LiquidityManager _lm, Optimizer _optimizer, bool _token0isWeth - ) = selfFeeEnv.setupEnvironmentWithSelfFeeDestination(DEFAULT_TOKEN0_IS_WETH, RECENTER_CALLER); + ) = selfFeeEnv.setupEnvironmentWithSelfFeeDestination(DEFAULT_TOKEN0_IS_WETH); // Wire state variables used by buy/sell/recenter helpers factory = _factory; @@ -1143,7 +1143,6 @@ contract LiquidityManagerTest is UniSwapHelper { , ) = clampTestEnv.setupEnvironmentWithOptimizer( DEFAULT_TOKEN0_IS_WETH, - RECENTER_CALLER, address(highWidthOptimizer) ); diff --git a/onchain/test/ReplayProfitableScenario.t.sol b/onchain/test/ReplayProfitableScenario.t.sol index 1b9c952..add766e 100644 --- a/onchain/test/ReplayProfitableScenario.t.sol +++ b/onchain/test/ReplayProfitableScenario.t.sol @@ -36,7 +36,7 @@ contract ReplayProfitableScenario is Test { BullMarketOptimizer optimizer = new BullMarketOptimizer(); // Use seed 1 setup (odd seed = false for first param) - (, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer)); + (, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(false, address(optimizer)); // Fund exactly as in the recorded scenario vm.deal(address(lm), 200 ether); diff --git a/onchain/test/SupplyCorruption.t.sol b/onchain/test/SupplyCorruption.t.sol index 41ccbaf..3117305 100644 --- a/onchain/test/SupplyCorruption.t.sol +++ b/onchain/test/SupplyCorruption.t.sol @@ -40,7 +40,7 @@ contract SupplyCorruptionTest is UniSwapHelper { LiquidityManager _lm, Optimizer _optimizer, bool _token0isWeth - ) = testEnv.setupEnvironment(false, RECENTER_CALLER); + ) = testEnv.setupEnvironment(false); factory = _factory; pool = _pool; diff --git a/onchain/test/VWAPFloorProtection.t.sol b/onchain/test/VWAPFloorProtection.t.sol index f25400f..f0ca3a5 100644 --- a/onchain/test/VWAPFloorProtection.t.sol +++ b/onchain/test/VWAPFloorProtection.t.sol @@ -34,7 +34,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper { function setUp() public { testEnv = new TestEnvironment(feeDestination); (,pool, weth, harberg, , lm, , token0isWeth) = - testEnv.setupEnvironment(false, RECENTER_CALLER); + testEnv.setupEnvironment(false); vm.deal(address(lm), LM_ETH); diff --git a/onchain/test/helpers/TestBase.sol b/onchain/test/helpers/TestBase.sol index d3ab9b6..d78ec87 100644 --- a/onchain/test/helpers/TestBase.sol +++ b/onchain/test/helpers/TestBase.sol @@ -91,7 +91,6 @@ contract TestEnvironment is TestConstants { /** * @notice Deploy all contracts and set up the environment * @param token0shouldBeWeth Whether WETH should be token0 - * @param recenterCaller Address that will be granted recenter access * @return _factory The deployed Uniswap factory * @return _pool The created Uniswap pool * @return _weth The WETH token contract @@ -101,10 +100,7 @@ contract TestEnvironment is TestConstants { * @return _optimizer The optimizer contract * @return _token0isWeth Whether token0 is WETH */ - function setupEnvironment( - bool token0shouldBeWeth, - address recenterCaller - ) + function setupEnvironment(bool token0shouldBeWeth) external returns ( IUniswapV3Factory _factory, @@ -201,7 +197,6 @@ contract TestEnvironment is TestConstants { /** * @notice Setup environment with specific optimizer * @param token0shouldBeWeth Whether WETH should be token0 - * @param recenterCaller Address that will be granted recenter access * @param optimizerAddress Address of the optimizer to use * @return _factory The deployed Uniswap factory * @return _pool The created Uniswap pool @@ -212,11 +207,7 @@ contract TestEnvironment is TestConstants { * @return _optimizer The optimizer contract * @return _token0isWeth Whether token0 is WETH */ - function setupEnvironmentWithOptimizer( - bool token0shouldBeWeth, - address recenterCaller, - address optimizerAddress - ) + function setupEnvironmentWithOptimizer(bool token0shouldBeWeth, address optimizerAddress) external returns ( IUniswapV3Factory _factory, @@ -257,12 +248,8 @@ contract TestEnvironment is TestConstants { * _getOutstandingSupply skips the feeDestination KRK subtraction (already excluded * by outstandingSupply()). * @param token0shouldBeWeth Whether WETH should be token0 - * @param recenterCaller Address that will be granted recenter access */ - function setupEnvironmentWithSelfFeeDestination( - bool token0shouldBeWeth, - address recenterCaller - ) + function setupEnvironmentWithSelfFeeDestination(bool token0shouldBeWeth) external returns ( IUniswapV3Factory _factory, @@ -301,7 +288,6 @@ contract TestEnvironment is TestConstants { * @notice Setup environment with existing factory and specific optimizer * @param existingFactory The existing Uniswap factory to use * @param token0shouldBeWeth Whether WETH should be token0 - * @param recenterCaller Address that will be granted recenter access * @param optimizerAddress Address of the optimizer to use * @return _factory The existing Uniswap factory * @return _pool The created Uniswap pool @@ -315,7 +301,6 @@ contract TestEnvironment is TestConstants { function setupEnvironmentWithExistingFactory( IUniswapV3Factory existingFactory, bool token0shouldBeWeth, - address recenterCaller, address optimizerAddress ) external diff --git a/scripts/bootstrap-common.sh b/scripts/bootstrap-common.sh index 3898a2c..2ee8bf8 100755 --- a/scripts/bootstrap-common.sh +++ b/scripts/bootstrap-common.sh @@ -196,8 +196,8 @@ seed_application_state() { fi bootstrap_log "Swap returned 0 KRK — recentering and retrying" - # Mine a few blocks to advance time, then recenter - cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 || true + # Advance 61 s to clear the 60-second recenter cooldown, then mine a block. + cast rpc --rpc-url "$ANVIL_RPC" evm_increaseTime 61 >>"$LOG_FILE" 2>&1 || true cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 || true local recenter_pk="${TXNBOT_PRIVATE_KEY:-$DEPLOYER_PK}" cast send --rpc-url "$ANVIL_RPC" --private-key "$recenter_pk" \