305 lines
No EOL
11 KiB
Solidity
305 lines
No EOL
11 KiB
Solidity
// 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);
|
|
}
|
|
|
|
// Override each strategy to add recenter recording
|
|
function _executeDiscoveryPush(uint256 rand) internal override {
|
|
console.log(" Strategy: Discovery Push [RECORDING]");
|
|
|
|
// Both accounts buy large amounts first
|
|
uint256 traderBuy = weth.balanceOf(account) * 7 / 10;
|
|
uint256 whaleBuy = weth.balanceOf(whale) * 8 / 10;
|
|
|
|
_executeBuy(account, traderBuy);
|
|
_executeBuy(whale, whaleBuy);
|
|
|
|
// Record position snapshot
|
|
if (trackPositions) {
|
|
_recordPositionData("MassiveBuy");
|
|
}
|
|
|
|
// Whale dumps
|
|
uint256 whaleKraiken = harberg.balanceOf(whale);
|
|
_executeSell(whale, whaleKraiken);
|
|
|
|
if (trackPositions) {
|
|
_recordPositionData("WhaleDump");
|
|
}
|
|
|
|
// Trader actions
|
|
uint256 traderKraiken = harberg.balanceOf(account);
|
|
if (traderKraiken > 0) {
|
|
_executeSell(account, traderKraiken / 2);
|
|
|
|
// Recenter
|
|
_executeRecenter();
|
|
|
|
// Buy back
|
|
uint256 remainingWeth = weth.balanceOf(account);
|
|
if (remainingWeth > 0) {
|
|
_executeBuy(account, remainingWeth / 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(account),
|
|
harberg.balanceOf(account),
|
|
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 == account ? "trader" : "whale",
|
|
", ", vm.toString(action.amount), ");\n"
|
|
);
|
|
} else if (keccak256(bytes(action.actionType)) == keccak256("SELL")) {
|
|
script = string.concat(
|
|
script,
|
|
" _executeSell(",
|
|
action.actor == account ? "trader" : "whale",
|
|
", ", 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 == account ? "Trader" : action.actor == whale ? "Whale" : "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;
|
|
}
|
|
} |