diff --git a/onchain/script/backtesting/BacktestKraiken.sol b/onchain/script/backtesting/BacktestKraiken.sol new file mode 100644 index 0000000..2935ce7 --- /dev/null +++ b/onchain/script/backtesting/BacktestKraiken.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import { MockToken } from "./MockToken.sol"; + +/** + * @title BacktestKraiken + * @notice Kraiken-compatible ERC-20 for backtesting purposes only. + * + * Dual mint interface: + * - MockToken.mint(address to, uint256 amount) — public, inherited from MockToken. + * Used by EventReplayer to seed historical pool positions without access checks. + * - mint(uint256 amount) — restricted to liquidityManager. + * Mirrors Kraiken.mint(uint256) so LiquidityManager operates unchanged. + * + * burn(uint256), setPreviousTotalSupply(), outstandingSupply(), and peripheryContracts() + * mirror the real Kraiken interface that LiquidityManager depends on. + * + * Omissions vs real Kraiken (acceptable for backtesting): + * - No staking pool proportional minting (no Stake.sol deployed). + * - No ERC20Permit extension. + * - setLiquidityManager() has no deployer-only guard (script context only). + */ +contract BacktestKraiken is MockToken { + address public liquidityManager; + uint256 public previousTotalSupply; + + constructor() MockToken("Backtesting KRK", "bKRK", 18) { } + + modifier onlyLiquidityManager() { + require(msg.sender == liquidityManager, "only liquidity manager"); + _; + } + + // ------------------------------------------------------------------------- + // Wiring + // ------------------------------------------------------------------------- + + /** + * @notice Wire the LiquidityManager. Can only be called once. + */ + function setLiquidityManager(address lm) external { + require(liquidityManager == address(0), "already set"); + liquidityManager = lm; + } + + // ------------------------------------------------------------------------- + // LM-restricted supply management (mirrors Kraiken interface) + // ------------------------------------------------------------------------- + + /** + * @notice Mint `amount` tokens to the LiquidityManager. + * Overloads MockToken.mint(address,uint256) — the two signatures + * have distinct ABI selectors and coexist without conflict. + */ + function mint(uint256 amount) external onlyLiquidityManager { + if (amount > 0) _mint(liquidityManager, amount); + if (previousTotalSupply == 0) previousTotalSupply = totalSupply(); + } + + /** + * @notice Burn `amount` tokens from the LiquidityManager's balance. + */ + function burn(uint256 amount) external onlyLiquidityManager { + if (amount > 0) _burn(liquidityManager, amount); + } + + /** + * @notice Called by LM when isUp == true during recenter. + */ + function setPreviousTotalSupply(uint256 ts) external onlyLiquidityManager { + previousTotalSupply = ts; + } + + // ------------------------------------------------------------------------- + // View helpers (mirrors Kraiken interface) + // ------------------------------------------------------------------------- + + /** + * @notice Total supply minus the LM's own balance (trader-held supply). + */ + function outstandingSupply() external view returns (uint256) { + return totalSupply() - balanceOf(liquidityManager); + } + + /** + * @notice Returns (liquidityManager, address(0)) — no staking pool in backtests. + * LiquidityManager._getOutstandingSupply() calls this to exclude staked KRK; + * returning address(0) for stakingPool skips that subtraction safely. + */ + function peripheryContracts() external view returns (address, address) { + return (liquidityManager, address(0)); + } +} diff --git a/onchain/script/backtesting/BacktestRunner.s.sol b/onchain/script/backtesting/BacktestRunner.s.sol index 7b48ee1..facca20 100644 --- a/onchain/script/backtesting/BacktestRunner.s.sol +++ b/onchain/script/backtesting/BacktestRunner.s.sol @@ -3,21 +3,42 @@ pragma solidity ^0.8.19; import { Script } from "forge-std/Script.sol"; import { console2 } from "forge-std/console2.sol"; +import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; import { MockToken } from "./MockToken.sol"; +import { BacktestKraiken } from "./BacktestKraiken.sol"; import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol"; import { EventReplayer } from "./EventReplayer.sol"; +import { KrAIkenSystem, KrAIkenDeployer } from "./KrAIkenDeployer.sol"; +import { StrategyExecutor } from "./StrategyExecutor.sol"; /** * @title BacktestRunner * @notice Entry point for backtesting. Deploys a UniswapV3 shadow pool that mirrors the * AERO/WETH 1% pool configuration, initialised at the price from the event cache, - * then replays all Swap/Mint/Burn events from the cache against the shadow pool. + * deploys the KrAIken protocol on top of the shadow pool, then replays all + * Swap/Mint/Burn events from the cache while triggering recenter() at configurable + * block intervals. + * + * Token setup: + * Mock WETH (MockToken) and BacktestKraiken are the shadow pool token pair, sorted by + * address. Both are freely mintable via MockToken.mint(address,uint256), so EventReplayer + * can seed historical LP positions without access-control friction. LiquidityManager + * additionally uses BacktestKraiken.mint(uint256) — the Kraiken-compatible overload + * restricted to the LM address — for its own liquidity minting. + * + * Negligible-impact assumption: + * KrAIken positions sit in the same shadow pool as replayed historical events. + * Events are replayed as-is without adjusting swap amounts for KrAIken's liquidity. + * TODO(#319): Add a feedback loop that accounts for KrAIken's active tick ranges + * when computing the actual swap amounts seen by each LP. * * Usage: * forge script script/backtesting/BacktestRunner.s.sol \ * --rpc-url http://localhost:8545 --broadcast \ * [BACKTEST_EVENTS_FILE=/path/to/events.jsonl] \ - * [INITIAL_SQRT_PRICE_X96=] + * [INITIAL_SQRT_PRICE_X96=] \ + * [RECENTER_INTERVAL=100] \ + * [INITIAL_CAPITAL_WETH=10000000000000000000] * * Price resolution order (first that succeeds wins): * 1. Environment variable INITIAL_SQRT_PRICE_X96 @@ -28,6 +49,9 @@ contract BacktestRunner is Script { /// @dev 2^96 — corresponds to tick 0 (price ratio 1:1). uint160 internal constant DEFAULT_SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336; + /// @dev Default number of blocks between recenter attempts (~3 min on Base at 2s/block). + uint256 internal constant DEFAULT_RECENTER_INTERVAL = 100; + // ------------------------------------------------------------------------- // Price resolution helpers // ------------------------------------------------------------------------- @@ -71,28 +95,73 @@ contract BacktestRunner is Script { // ------------------------------------------------------------------------- function run() external { - // Resolve price before broadcast so the self-call is not recorded. + // Resolve configuration before broadcast so self-calls are not recorded. uint160 sqrtPriceX96 = _resolveSqrtPrice(); + uint256 recenterInterval = DEFAULT_RECENTER_INTERVAL; + try vm.envUint("RECENTER_INTERVAL") returns (uint256 v) { + if (v > 0) recenterInterval = v; + } catch {} + + uint256 initialCapital = KrAIkenDeployer.DEFAULT_INITIAL_CAPITAL; + try vm.envUint("INITIAL_CAPITAL_WETH") returns (uint256 v) { + if (v > 0) initialCapital = v; + } catch {} + vm.startBroadcast(); - // Deploy mock ERC20 tokens (18 decimals, matching AERO and WETH). - MockToken tokenA = new MockToken("Mock AERO", "mAERO", 18); - MockToken tokenB = new MockToken("Mock WETH", "mWETH", 18); - - // Mint an initial supply to the broadcaster for future liquidity seeding. address sender = msg.sender; - tokenA.mint(sender, 1_000_000 ether); - tokenB.mint(sender, 1_000_000 ether); - // Deploy factory + pool and initialise at the resolved price. - ShadowPool memory sp = ShadowPoolDeployer.deploy(address(tokenA), address(tokenB), sqrtPriceX96); + // ------------------------------------------------------------------ + // Token deployment + // + // BacktestKraiken extends MockToken, so EventReplayer can mint it + // freely via MockToken.mint(address,uint256) for historical pool + // positions. LiquidityManager calls the restricted mint(uint256) + // overload (onlyLiquidityManager) wired by KrAIkenDeployer.deploy(). + // ------------------------------------------------------------------ + MockToken mockWeth = new MockToken("Mock WETH", "mWETH", 18); + BacktestKraiken krk = new BacktestKraiken(); + + // Seed the broadcaster with mock WETH for manual interactions. + mockWeth.mint(sender, 1_000_000 ether); + + // Deploy factory + pool, sorted by address (token0 < token1). + ShadowPool memory sp = ShadowPoolDeployer.deploy(address(mockWeth), address(krk), sqrtPriceX96); + + // ------------------------------------------------------------------ + // KrAIken system deployment (follows DeployLocal.sol pattern) + // + // 1. Deploy OptimizerV3Push3 (no proxy — only exposes isBullMarket(), + // causing LM to fall back to safe bear-mode defaults via try/catch). + // 2. Deploy LiquidityManager pointing at the shadow pool. + // 3. Wire BacktestKraiken.setLiquidityManager(lm). + // 4. Set feeDestination = sender. + // 5. Mint initialCapital mock WETH to LM. + // ------------------------------------------------------------------ + 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. + StrategyExecutor executor = + new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval); + sys.lm.setRecenterAccess(address(executor)); vm.stopBroadcast(); - // Instantiate EventReplayer outside the broadcast block: it uses Foundry cheat codes - // (vm.readLine, vm.roll, vm.warp) that only work in the forge simulation context and - // must not be sent as real transactions to the RPC endpoint. + // ------------------------------------------------------------------ + // EventReplayer is instantiated outside the broadcast block because + // it uses Foundry cheat codes (vm.readLine, vm.roll, vm.warp) that + // only work in the forge simulation context and must not be sent as + // real transactions. + // + // sp.token0 / sp.token1 are cast to MockToken — BacktestKraiken + // inherits MockToken so the cast is valid; its mint(address,uint256) + // selector matches, enabling EventReplayer's mint callback. + // ------------------------------------------------------------------ EventReplayer replayer = new EventReplayer(sp.pool, MockToken(sp.token0), MockToken(sp.token1)); // Query pool state (view calls, no broadcast needed). @@ -111,11 +180,21 @@ contract BacktestRunner is Script { console2.log("sqrtPriceX96: ", uint256(slot0SqrtPrice)); console2.log("Initial tick: ", int256(tick)); console2.log("Liquidity: ", uint256(liquidity)); + console2.log("\n=== KrAIken System ==="); + console2.log("BacktestKraiken:", address(krk)); + console2.log("Optimizer: ", address(sys.optimizer)); + console2.log("LiquidityMgr: ", address(sys.lm)); + console2.log("StrategyExec: ", address(executor)); + console2.log("Recenter intv: ", recenterInterval, " blocks"); + console2.log("Initial capital:", initialCapital, " (mock WETH wei)"); + console2.log("token0isWeth: ", sp.token0 == address(mockWeth)); // ----------------------------------------------------------------------- // Event replay (runs as local simulation — no broadcast required). // Each pool.mint / pool.swap / pool.burn call executes against the shadow // pool; vm.roll + vm.warp advance block state to match historical timing. + // After each block advancement, StrategyExecutor.maybeRecenter() is called + // to trigger LiquidityManager.recenter() at the configured interval. // ----------------------------------------------------------------------- try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) { if (bytes(eventsFile).length > 0) { @@ -137,7 +216,10 @@ contract BacktestRunner is Script { console2.log("\n=== Starting Event Replay ==="); console2.log("Events file: ", eventsFile); console2.log("Total events: ", totalEvents); - replayer.replay(eventsFile, totalEvents); + replayer.replay(eventsFile, totalEvents, executor); + + // Print final KrAIken strategy summary. + executor.logSummary(); } } catch {} } diff --git a/onchain/script/backtesting/EventReplayer.sol b/onchain/script/backtesting/EventReplayer.sol index ecb36e8..2b46db0 100644 --- a/onchain/script/backtesting/EventReplayer.sol +++ b/onchain/script/backtesting/EventReplayer.sol @@ -8,6 +8,7 @@ import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; import { Vm } from "forge-std/Vm.sol"; import { console2 } from "forge-std/console2.sol"; import { MockToken } from "./MockToken.sol"; +import { StrategyExecutor } from "./StrategyExecutor.sol"; /** * @title EventReplayer @@ -92,6 +93,27 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback { * Pass 0 to omit the denominator from progress logs. */ function replay(string memory eventsFile, uint256 totalEvents) external { + _replayWithStrategy(eventsFile, totalEvents, StrategyExecutor(address(0))); + } + + /** + * @notice Replay events and trigger KrAIken recenter logic after each block. + * @param eventsFile Path to the .jsonl events cache. + * @param totalEvents Total event count (for progress logs). + * @param strategyExecutor StrategyExecutor to call after every block advancement. + * Pass address(0) to disable strategy integration. + */ + function replay(string memory eventsFile, uint256 totalEvents, StrategyExecutor strategyExecutor) external { + _replayWithStrategy(eventsFile, totalEvents, strategyExecutor); + } + + function _replayWithStrategy( + string memory eventsFile, + uint256 totalEvents, + StrategyExecutor strategyExecutor + ) + internal + { uint256 idx = 0; // Track the last Swap event's expected state for drift measurement. @@ -113,6 +135,12 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback { _advanceChain(blockNum); + // After advancing chain state, give the strategy a chance to recenter. + // The call is best-effort: any revert is caught inside StrategyExecutor. + if (address(strategyExecutor) != address(0)) { + strategyExecutor.maybeRecenter(blockNum); + } + if (_streq(eventName, "Swap")) { (int24 expTick, uint160 expSqrtPrice) = _replaySwap(line); // Update reference state only when the swap was not skipped. diff --git a/onchain/script/backtesting/KrAIkenDeployer.sol b/onchain/script/backtesting/KrAIkenDeployer.sol new file mode 100644 index 0000000..eec7c28 --- /dev/null +++ b/onchain/script/backtesting/KrAIkenDeployer.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import { MockToken } from "./MockToken.sol"; +import { BacktestKraiken } from "./BacktestKraiken.sol"; +import { OptimizerV3Push3 } from "../../src/OptimizerV3Push3.sol"; +import { LiquidityManager } from "../../src/LiquidityManager.sol"; + +/** + * @notice Deployment result for the KrAIken system on the shadow pool. + */ +struct KrAIkenSystem { + BacktestKraiken kraiken; + /// @dev OptimizerV3Push3 is used as the optimizer address. It does not implement + /// getLiquidityParams(), so LiquidityManager's try/catch falls back to safe + /// bear-mode defaults on every recenter. This is intentional for backtesting. + OptimizerV3Push3 optimizer; + LiquidityManager lm; +} + +/** + * @title KrAIkenDeployer + * @notice Library that deploys the KrAIken protocol contracts on top of a shadow pool + * for backtesting purposes. + * + * Deployment order follows DeployLocal.sol: + * 1. Deploy OptimizerV3Push3 (no proxy — it only exposes isBullMarket(), so LM falls + * back to bear-mode defaults via try/catch on every recenter call). + * 2. Deploy LiquidityManager pointing at the shadow factory + mock WETH + BacktestKraiken. + * 3. Wire BacktestKraiken → LM (setLiquidityManager). + * 4. Set fee destination on LM. + * 5. Fund LM with mock WETH (initial capital). + * + * @dev All functions are `internal` so they are inlined into BacktestRunner.s.sol, + * keeping msg.sender consistent with the broadcaster throughout deployment. + */ +library KrAIkenDeployer { + uint256 internal constant DEFAULT_INITIAL_CAPITAL = 10 ether; + + /** + * @notice Deploy the KrAIken system with default initial capital (10 ETH equivalent). + */ + function deploy( + address shadowFactory, + address mockWeth, + address krkToken, + address feeDestination + ) + internal + returns (KrAIkenSystem memory sys) + { + return deploy(shadowFactory, mockWeth, krkToken, feeDestination, DEFAULT_INITIAL_CAPITAL); + } + + /** + * @notice Deploy the KrAIken system with configurable initial capital. + * + * @param shadowFactory Factory that created the shadow pool. + * @param mockWeth Mock WETH token address (18-decimal ERC-20, freely mintable). + * @param krkToken BacktestKraiken token address (setLiquidityManager not yet called). + * @param feeDestination Address that will receive LP fees; also used as the caller + * for subsequent setRecenterAccess() calls. + * @param initialCapital Mock WETH minted to LM so recenter() has capital to deploy. + */ + function deploy( + address shadowFactory, + address mockWeth, + address krkToken, + address feeDestination, + uint256 initialCapital + ) + internal + returns (KrAIkenSystem memory sys) + { + // 1. Deploy OptimizerV3Push3. + // LiquidityManager wraps getLiquidityParams() in a try/catch and falls back to + // safe bear-mode defaults when the call reverts. Since OptimizerV3Push3 only + // exposes isBullMarket(), every recenter uses bear defaults — conservative and + // correct for a baseline backtest. + OptimizerV3Push3 optimizer = new OptimizerV3Push3(); + + // 2. Deploy LiquidityManager. It computes the pool address from factory + WETH + + // KRK + FEE (10 000). The shadow pool must have been created with the same + // factory and the same fee tier (ShadowPoolDeployer.SHADOW_FEE == 10 000). + LiquidityManager lm = new LiquidityManager(shadowFactory, mockWeth, krkToken, address(optimizer)); + + // 3. Wire BacktestKraiken → LM so the restricted mint/burn functions work. + BacktestKraiken(krkToken).setLiquidityManager(address(lm)); + + // 4. Set fee destination (required before setRecenterAccess can be called). + lm.setFeeDestination(feeDestination); + + // 5. Fund LM with mock WETH. recenter() uses _getEthBalance() which reads + // weth.balanceOf(address(this)). Pre-funding avoids calling weth.deposit() + // (which MockToken does not implement). + MockToken(mockWeth).mint(address(lm), initialCapital); + + sys = KrAIkenSystem({ kraiken: BacktestKraiken(krkToken), optimizer: optimizer, lm: lm }); + } +} diff --git a/onchain/script/backtesting/StrategyExecutor.sol b/onchain/script/backtesting/StrategyExecutor.sol new file mode 100644 index 0000000..969d77c --- /dev/null +++ b/onchain/script/backtesting/StrategyExecutor.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import { console2 } from "forge-std/console2.sol"; +import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; +import { LiquidityManager } from "../../src/LiquidityManager.sol"; +import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol"; + +/** + * @title StrategyExecutor + * @notice Drives the KrAIken recenter loop during event replay. + * + * Called by EventReplayer after every block advancement. Triggers + * LiquidityManager.recenter() once per `recenterInterval` blocks and logs: + * - Block number and timestamp + * - Pre/post positions (tickLower, tickUpper, liquidity) for Floor, Anchor, Discovery + * - Fees collected (WETH + KRK) as the delta of feeDestination's balances + * - Revert reason on failure (logged and skipped — replay never halts) + * + * 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). + * + * TODO(#319): The negligible-impact assumption means we replay historical events + * as-is without accounting for KrAIken's own liquidity affecting swap outcomes. + * A future enhancement should add a feedback loop that adjusts replayed swap + * amounts based on KrAIken's active tick ranges. + */ +contract StrategyExecutor { + // ------------------------------------------------------------------------- + // Configuration (immutable) + // ------------------------------------------------------------------------- + + LiquidityManager public immutable lm; + IERC20 public immutable wethToken; + IERC20 public immutable krkToken; + address public immutable feeDestination; + /// @notice Minimum block gap between recenter attempts. + uint256 public immutable recenterInterval; + + // ------------------------------------------------------------------------- + // Runtime state + // ------------------------------------------------------------------------- + + uint256 public lastRecenterBlock; + uint256 public totalRecenters; + uint256 public failedRecenters; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor( + LiquidityManager _lm, + IERC20 _wethToken, + IERC20 _krkToken, + address _feeDestination, + uint256 _recenterInterval + ) { + lm = _lm; + wethToken = _wethToken; + krkToken = _krkToken; + feeDestination = _feeDestination; + recenterInterval = _recenterInterval; + } + + // ------------------------------------------------------------------------- + // Main entry point (called by EventReplayer after each block advancement) + // ------------------------------------------------------------------------- + + /** + * @notice Attempt a recenter if enough blocks have elapsed since the last one. + * @param blockNum Current block number (as advanced by EventReplayer via vm.roll). + */ + function maybeRecenter(uint256 blockNum) external { + if (blockNum - lastRecenterBlock < recenterInterval) return; + + // Snapshot pre-recenter positions. + (uint128 fLiqPre, int24 fLoPre, int24 fHiPre) = lm.positions(ThreePositionStrategy.Stage.FLOOR); + (uint128 aLiqPre, int24 aLoPre, int24 aHiPre) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); + (uint128 dLiqPre, int24 dLoPre, int24 dHiPre) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); + + // Snapshot fee destination balances to compute fees collected during this recenter. + uint256 wethPre = wethToken.balanceOf(feeDestination); + uint256 krkPre = krkToken.balanceOf(feeDestination); + + // Always advance lastRecenterBlock, even on failure, to avoid hammering a + // persistently failing condition on every subsequent block. + lastRecenterBlock = blockNum; + + bool success; + string memory failReason; + + try lm.recenter() { + success = true; + totalRecenters++; + } catch Error(string memory reason) { + failReason = reason; + failedRecenters++; + } catch { + failReason = "unknown revert"; + failedRecenters++; + } + + if (!success) { + console2.log( + string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason) + ); + return; + } + + // Snapshot post-recenter positions. + (uint128 fLiqPost, int24 fLoPost, int24 fHiPost) = lm.positions(ThreePositionStrategy.Stage.FLOOR); + (uint128 aLiqPost, int24 aLoPost, int24 aHiPost) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); + (uint128 dLiqPost, int24 dLoPost, int24 dHiPost) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); + + uint256 feesWeth = wethToken.balanceOf(feeDestination) - wethPre; + uint256 feesKrk = krkToken.balanceOf(feeDestination) - krkPre; + + _logRecenter( + blockNum, + fLiqPre, fLoPre, fHiPre, + aLiqPre, aLoPre, aHiPre, + dLiqPre, dLoPre, dHiPre, + fLiqPost, fLoPost, fHiPost, + aLiqPost, aLoPost, aHiPost, + dLiqPost, dLoPost, dHiPost, + feesWeth, + feesKrk + ); + } + + // ------------------------------------------------------------------------- + // Summary + // ------------------------------------------------------------------------- + + /** + * @notice Print a summary of all recenter activity. Call at end of replay. + */ + function logSummary() external view { + (uint128 fLiq, int24 fLo, int24 fHi) = lm.positions(ThreePositionStrategy.Stage.FLOOR); + (uint128 aLiq, int24 aLo, int24 aHi) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); + (uint128 dLiq, int24 dLo, int24 dHi) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); + + console2.log("=== KrAIken Strategy Summary ==="); + console2.log("Total recenters: ", totalRecenters); + console2.log("Failed recenters: ", failedRecenters); + console2.log("Recenter interval:", recenterInterval, "blocks"); + console2.log( + string.concat( + "Final Floor: tick [", + _istr(fLo), + ", ", + _istr(fHi), + "] liq=", + _str(uint256(fLiq)) + ) + ); + console2.log( + string.concat( + "Final Anchor: tick [", + _istr(aLo), + ", ", + _istr(aHi), + "] liq=", + _str(uint256(aLiq)) + ) + ); + console2.log( + string.concat( + "Final Discovery: tick [", + _istr(dLo), + ", ", + _istr(dHi), + "] liq=", + _str(uint256(dLiq)) + ) + ); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + function _logRecenter( + uint256 blockNum, + uint128 fLiqPre, int24 fLoPre, int24 fHiPre, + uint128 aLiqPre, int24 aLoPre, int24 aHiPre, + uint128 dLiqPre, int24 dLoPre, int24 dHiPre, + uint128 fLiqPost, int24 fLoPost, int24 fHiPost, + uint128 aLiqPost, int24 aLoPost, int24 aHiPost, + uint128 dLiqPost, int24 dLoPost, int24 dHiPost, + uint256 feesWeth, + uint256 feesKrk + ) + internal + view + { + console2.log( + string.concat("=== Recenter #", _str(totalRecenters), " @ block ", _str(blockNum), " ===") + ); + console2.log( + string.concat( + " Floor pre: tick [", + _istr(fLoPre), + ", ", + _istr(fHiPre), + "] liq=", + _str(uint256(fLiqPre)) + ) + ); + console2.log( + string.concat( + " Anchor pre: tick [", + _istr(aLoPre), + ", ", + _istr(aHiPre), + "] liq=", + _str(uint256(aLiqPre)) + ) + ); + console2.log( + string.concat( + " Disc pre: tick [", + _istr(dLoPre), + ", ", + _istr(dHiPre), + "] liq=", + _str(uint256(dLiqPre)) + ) + ); + console2.log( + string.concat( + " Floor post: tick [", + _istr(fLoPost), + ", ", + _istr(fHiPost), + "] liq=", + _str(uint256(fLiqPost)) + ) + ); + console2.log( + string.concat( + " Anchor post: tick [", + _istr(aLoPost), + ", ", + _istr(aHiPost), + "] liq=", + _str(uint256(aLiqPost)) + ) + ); + console2.log( + string.concat( + " Disc post: tick [", + _istr(dLoPost), + ", ", + _istr(dHiPost), + "] liq=", + _str(uint256(dLiqPost)) + ) + ); + console2.log( + string.concat(" Fees WETH: ", _str(feesWeth), " Fees KRK: ", _str(feesKrk)) + ); + } + + // ------------------------------------------------------------------------- + // Formatting helpers (no vm dependency required) + // ------------------------------------------------------------------------- + + function _str(uint256 v) internal pure returns (string memory) { + if (v == 0) return "0"; + uint256 tmp = v; + uint256 len; + while (tmp != 0) { + len++; + tmp /= 10; + } + bytes memory buf = new bytes(len); + while (v != 0) { + buf[--len] = bytes1(uint8(48 + v % 10)); + v /= 10; + } + return string(buf); + } + + function _istr(int256 v) internal pure returns (string memory) { + if (v >= 0) return _str(uint256(v)); + return string.concat("-", _str(uint256(-v))); + } +}