// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import { Script } from "forge-std/Script.sol"; import { console2 } from "forge-std/console2.sol"; import { MockToken } from "./MockToken.sol"; import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol"; import { EventReplayer } from "./EventReplayer.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. * * 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=] * * Price resolution order (first that succeeds wins): * 1. Environment variable INITIAL_SQRT_PRICE_X96 * 2. sqrtPriceX96 field from the first line of $BACKTEST_EVENTS_FILE * 3. Default: 2^96 (tick = 0, price 1:1) */ 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; // ------------------------------------------------------------------------- // Price resolution helpers // ------------------------------------------------------------------------- /** * @notice Parse sqrtPriceX96 from the first line of a JSON Lines events file. * @dev Must be `external` so it can be called via `this.` inside a try/catch. * fetch-events.ts serialises BigInt values as decimal strings, so we use * parseJsonString then parseUint rather than parseJsonUint. */ function _parseSqrtPriceFromFile(string memory eventsFile) external view returns (uint160) { string memory line = vm.readLine(eventsFile); string memory sqrtStr = vm.parseJsonString(line, ".sqrtPriceX96"); return uint160(vm.parseUint(sqrtStr)); } /** * @notice Resolve the initial sqrtPriceX96 without broadcasting any transaction. * @dev Call this BEFORE vm.startBroadcast() — the `this.` external call must not * be included in the broadcast. */ function _resolveSqrtPrice() internal view returns (uint160) { // 1. Explicit env override. try vm.envUint("INITIAL_SQRT_PRICE_X96") returns (uint256 val) { if (val != 0) return uint160(val); } catch {} // 2. First Swap event in the events cache. try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) { try this._parseSqrtPriceFromFile(eventsFile) returns (uint160 val) { if (val != 0) return val; } catch {} } catch {} // 3. Fallback default. return DEFAULT_SQRT_PRICE_X96; } // ------------------------------------------------------------------------- // Main entry point // ------------------------------------------------------------------------- function run() external { // Resolve price before broadcast so the self-call is not recorded. uint160 sqrtPriceX96 = _resolveSqrtPrice(); 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); // Deploy the event replayer with the canonical token ordering from the pool. EventReplayer replayer = new EventReplayer(sp.pool, MockToken(sp.token0), MockToken(sp.token1)); vm.stopBroadcast(); // Query pool state (view calls, no broadcast needed). (uint160 slot0SqrtPrice, int24 tick,,,,,) = sp.pool.slot0(); uint128 liquidity = sp.pool.liquidity(); int24 tickSpacing = sp.pool.tickSpacing(); console2.log("=== Backtesting Shadow Pool Deployment ==="); console2.log("Factory: ", address(sp.factory)); console2.log("Pool: ", address(sp.pool)); console2.log("Token0: ", sp.token0); console2.log("Token1: ", sp.token1); console2.log("Replayer: ", address(replayer)); console2.log("Fee tier: ", uint256(ShadowPoolDeployer.SHADOW_FEE)); console2.log("Tick spacing: ", int256(tickSpacing)); console2.log("sqrtPriceX96: ", uint256(slot0SqrtPrice)); console2.log("Initial tick: ", int256(tick)); console2.log("Liquidity: ", uint256(liquidity)); // ----------------------------------------------------------------------- // 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. // ----------------------------------------------------------------------- try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) { if (bytes(eventsFile).length > 0) { // Reset file position — _resolveSqrtPrice() may have consumed line 1. try vm.closeFile(eventsFile) {} catch {} // Pre-count events so replay() can show "[N/total]" progress lines. uint256 totalEvents = 0; { string memory l = vm.readLine(eventsFile); while (bytes(l).length > 0) { totalEvents++; l = vm.readLine(eventsFile); } } // Reset again before the replay pass. try vm.closeFile(eventsFile) {} catch {} console2.log("\n=== Starting Event Replay ==="); console2.log("Events file: ", eventsFile); console2.log("Total events: ", totalEvents); replayer.replay(eventsFile, totalEvents); } } catch {} } }