// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "./ImprovedFuzzingAnalysis.s.sol"; import "./helpers/ScenarioRecorder.sol"; /** * @title RecordedFuzzingAnalysis * @notice Enhanced fuzzing that records profitable scenarios for exact replay * @dev Captures all actions, states, and parameters when invariants fail */ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis { ScenarioRecorder public recorder; bool public enableRecording = true; string public runId; // Unique identifier for this run bool private isRecordingProfitable; // Only record if scenario is profitable // Constructor removed - set trackPositions in run() instead // Store actions for current scenario struct ActionRecord { uint256 step; string actionType; address actor; uint256 amount; uint256 timestamp; uint256 blockNumber; int24 tickBefore; int24 tickAfter; } ActionRecord[] currentActions; function run() public override { // Get run ID from environment or generate default runId = vm.envOr("RUN_ID", string("LOCAL")); // Force position tracking to be enabled for CSV generation vm.setEnv("TRACK_POSITIONS", "true"); console.log("=== RECORDED Fuzzing Analysis ==="); console.log(string.concat("Run ID: ", runId)); console.log("Recording enabled for profitable scenario replay"); super.run(); } // Override to add recording for profitable scenarios function _runImprovedScenario(uint256 seed) internal override returns (uint256 finalBalance, bool reachedDiscovery) { // Clear previous scenario delete currentActions; isRecordingProfitable = false; // Run the base scenario (finalBalance, reachedDiscovery) = super._runImprovedScenario(seed); // Export summary if profitable uint256 initialBalance = 50 ether; if (finalBalance > initialBalance && reachedDiscovery) { _exportSummary(seed, initialBalance, finalBalance, reachedDiscovery); } return (finalBalance, reachedDiscovery); } // Override trade execution to record actions function _executeBuy(address buyer, uint256 amount) internal override { if (amount == 0 || weth.balanceOf(buyer) < amount) return; (, int24 tickBefore,,,,,) = pool.slot0(); // Execute trade super._executeBuy(buyer, amount); (, int24 tickAfter,,,,,) = pool.slot0(); // Only record action summary (not full state) _recordAction("BUY", buyer, amount, tickBefore, tickAfter); } function _executeSell(address seller, uint256 amount) internal override { if (amount == 0 || harberg.balanceOf(seller) < amount) return; (, int24 tickBefore,,,,,) = pool.slot0(); // Execute trade super._executeSell(seller, amount); (, int24 tickAfter,,,,,) = pool.slot0(); // Only record action summary (not full state) _recordAction("SELL", seller, amount, tickBefore, tickAfter); } function _executeRecenter() internal { (, int24 tickBefore,,,,,) = pool.slot0(); vm.warp(block.timestamp + 1 hours); vm.prank(feeDestination); try lm.recenter{gas: 50_000_000}() {} catch {} (, int24 tickAfter,,,,,) = pool.slot0(); // Only record action summary _recordAction("RECENTER", feeDestination, 0, tickBefore, tickAfter); } function _recordState(string memory label, address actor, uint256 amount) internal { // Only record states if we're exporting a profitable scenario if (!isRecordingProfitable || !enableRecording) return; (uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0(); recorder.recordPreState( weth.balanceOf(trader), harberg.balanceOf(trader), currentTick, uint256(sqrtPriceX96), 0, // VWAP placeholder harberg.outstandingSupply() ); } function _recordAction( string memory actionType, address actor, uint256 amount, int24 tickBefore, int24 tickAfter ) internal { currentActions.push(ActionRecord({ step: currentActions.length, actionType: actionType, actor: actor, amount: amount, timestamp: block.timestamp, blockNumber: block.number, tickBefore: tickBefore, tickAfter: tickAfter })); } function _exportSummary( uint256 seed, uint256 initialBalance, uint256 finalBalance, bool reachedDiscovery ) internal { console.log("\n[RECORDING] Exporting profitable scenario..."); console.log(string.concat(" Run ID: ", runId)); console.log(string.concat(" Seed: ", vm.toString(seed))); string memory summary = _generateActionSummary( seed, initialBalance, finalBalance, reachedDiscovery ); string memory summaryFilename = string.concat( "scenario_summary_seed", vm.toString(seed), ".txt" ); vm.writeFile(summaryFilename, summary); console.log(string.concat(" Summary exported to: ", summaryFilename)); if (currentActions.length > 0) { string memory replayScript = _generateReplayScript(seed); string memory scriptFilename = string.concat( "replay_script_seed", vm.toString(seed), ".sol" ); vm.writeFile(scriptFilename, replayScript); console.log(string.concat(" Replay script exported to: ", scriptFilename)); } } function _generateReplayScript(uint256 seed) internal view returns (string memory) { string memory script = string.concat( "// Replay script for profitable scenario\n", "// Seed: ", vm.toString(seed), "\n", "// Optimizer: ", optimizerClass, "\n", "// Discovery reached: YES\n\n", "function replayScenario() public {\n" ); for (uint256 i = 0; i < currentActions.length; i++) { ActionRecord memory action = currentActions[i]; script = string.concat(script, " // Step ", vm.toString(i), "\n"); if (keccak256(bytes(action.actionType)) == keccak256("BUY")) { script = string.concat( script, " _executeBuy(", action.actor == trader ? "trader" : "system", ", ", vm.toString(action.amount), ");\n" ); } else if (keccak256(bytes(action.actionType)) == keccak256("SELL")) { script = string.concat( script, " _executeSell(", action.actor == trader ? "trader" : "system", ", ", vm.toString(action.amount), ");\n" ); } else if (keccak256(bytes(action.actionType)) == keccak256("RECENTER")) { script = string.concat( script, " vm.warp(", vm.toString(action.timestamp), ");\n", " vm.prank(feeDestination);\n", " lm.recenter();\n" ); } script = string.concat( script, " // Tick moved from ", vm.toString(action.tickBefore), " to ", vm.toString(action.tickAfter), "\n\n" ); } script = string.concat(script, "}\n"); return script; } function _generateActionSummary( uint256 seed, uint256 initialBalance, uint256 finalBalance, bool reachedDiscovery ) internal view returns (string memory) { uint256 profit = finalBalance - initialBalance; uint256 profitPct = (profit * 100) / initialBalance; string memory summary = string.concat( "=== PROFITABLE SCENARIO SUMMARY ===\n", "Run ID: ", runId, "\n", "Seed: ", vm.toString(seed), "\n", "Optimizer: ", optimizerClass, "\n", "Discovery Reached: ", reachedDiscovery ? "YES" : "NO", "\n\n", "Financial Results:\n", " Initial Balance: ", vm.toString(initialBalance / 1e18), " ETH\n", " Final Balance: ", vm.toString(finalBalance / 1e18), " ETH\n", " Profit: ", vm.toString(profit / 1e18), " ETH (", vm.toString(profitPct), "%)\n\n", "Action Sequence:\n" ); for (uint256 i = 0; i < currentActions.length; i++) { ActionRecord memory action = currentActions[i]; summary = string.concat( summary, " ", vm.toString(i + 1), ". ", action.actionType, " - ", action.actor == trader ? "Trader" : "System", action.amount > 0 ? string.concat(" - Amount: ", vm.toString(action.amount / 1e18), " ETH") : "", " - Tick: ", vm.toString(action.tickBefore), " -> ", vm.toString(action.tickAfter), "\n" ); } summary = string.concat( summary, "\nThis scenario can be replayed using the generated replay script.\n" ); return summary; } }