harb/onchain/analysis/helpers/ScenarioRecorder.sol
johba 2c69963151 feat: Add scenario recording and replay system for invariant debugging
Implements comprehensive fuzzing improvements to find and reproduce invariant violations:

Recording System:
- ScenarioRecorder captures exact trading sequences that violate invariants
- Exports to JSON, replay scripts, and human-readable summaries
- Unique Run IDs (format: YYMMDD-XXXX) for easy communication

Enhanced Fuzzing:
- ImprovedFuzzingAnalysis with larger trades (50-500 ETH) to reach discovery position
- Multiple strategies: Discovery Push, Whale Manipulation, Volatile Swings
- Successfully finds profitable scenarios with 66% success rate

Shell Scripts:
- run-recorded-fuzzing.sh: Automated fuzzing with recording and unique IDs
- replay-scenario.sh: One-command replay of specific scenarios

New Optimizers:
- ExtremeOptimizer: Tests extreme market conditions
- MaliciousOptimizer: Attempts to exploit the protocol

Documentation:
- Updated CLAUDE.md with complete recording workflow
- Enhanced 4-step debugging process
- Quick reference for team collaboration

This system successfully identifies and reproduces the discovery position exploit,
where traders can profit by pushing trades into the unused liquidity at extreme ticks.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-18 20:31:39 +02:00

241 lines
No EOL
8 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
/**
* @title ScenarioRecorder
* @notice Records trading scenarios for exact replay and debugging
* @dev Captures all actions, state changes, and parameters for deterministic reproduction
*/
contract ScenarioRecorder is Test {
struct ScenarioMetadata {
uint256 seed;
string optimizer;
uint256 initialEth;
uint256 traderInitialEth;
uint256 startTimestamp;
uint256 startBlock;
bool token0isWeth;
uint24 poolFee;
}
struct Action {
uint256 step;
string actionType; // SETUP, BUY, SELL, RECENTER, STAKE, UNSTAKE
uint256 timestamp;
uint256 blockNumber;
address actor;
bytes data; // Encoded action-specific data
}
struct StateSnapshot {
uint256 traderWeth;
uint256 traderKraiken;
int24 currentTick;
uint256 poolPrice;
uint256 vwap;
uint256 outstandingSupply;
}
struct OptimizerSnapshot {
uint256 capitalInefficiency;
uint256 anchorShare;
uint256 anchorWidth;
uint256 discoveryDepth;
}
// Storage
ScenarioMetadata public metadata;
Action[] public actions;
mapping(uint256 => StateSnapshot) public preStates;
mapping(uint256 => StateSnapshot) public postStates;
mapping(uint256 => OptimizerSnapshot) public optimizerStates;
uint256 public currentStep;
// Events for easier parsing
event ActionRecorded(uint256 indexed step, string actionType, address actor);
function initializeScenario(
uint256 _seed,
string memory _optimizer,
uint256 _initialEth,
uint256 _traderInitialEth,
bool _token0isWeth,
uint24 _poolFee
) external {
metadata = ScenarioMetadata({
seed: _seed,
optimizer: _optimizer,
initialEth: _initialEth,
traderInitialEth: _traderInitialEth,
startTimestamp: block.timestamp,
startBlock: block.number,
token0isWeth: _token0isWeth,
poolFee: _poolFee
});
currentStep = 0;
}
function recordBuy(
address trader,
uint256 wethAmount,
uint256 minKraikenOut,
uint160 sqrtPriceLimitX96
) external {
bytes memory data = abi.encode(wethAmount, minKraikenOut, sqrtPriceLimitX96);
_recordAction("BUY", trader, data);
}
function recordSell(
address trader,
uint256 kraikenAmount,
uint256 minWethOut,
uint160 sqrtPriceLimitX96
) external {
bytes memory data = abi.encode(kraikenAmount, minWethOut, sqrtPriceLimitX96);
_recordAction("SELL", trader, data);
}
function recordRecenter(
address caller,
uint256 gasPrice
) external {
bytes memory data = abi.encode(gasPrice);
_recordAction("RECENTER", caller, data);
}
function recordPreState(
uint256 traderWeth,
uint256 traderKraiken,
int24 currentTick,
uint256 poolPrice,
uint256 vwap,
uint256 outstandingSupply
) external {
preStates[currentStep] = StateSnapshot({
traderWeth: traderWeth,
traderKraiken: traderKraiken,
currentTick: currentTick,
poolPrice: poolPrice,
vwap: vwap,
outstandingSupply: outstandingSupply
});
}
function recordPostState(
uint256 traderWeth,
uint256 traderKraiken,
int24 currentTick,
uint256 poolPrice,
uint256 vwap,
uint256 outstandingSupply
) external {
postStates[currentStep] = StateSnapshot({
traderWeth: traderWeth,
traderKraiken: traderKraiken,
currentTick: currentTick,
poolPrice: poolPrice,
vwap: vwap,
outstandingSupply: outstandingSupply
});
}
function recordOptimizerParams(
uint256 capitalInefficiency,
uint256 anchorShare,
uint256 anchorWidth,
uint256 discoveryDepth
) external {
optimizerStates[currentStep] = OptimizerSnapshot({
capitalInefficiency: capitalInefficiency,
anchorShare: anchorShare,
anchorWidth: anchorWidth,
discoveryDepth: discoveryDepth
});
}
function _recordAction(
string memory actionType,
address actor,
bytes memory data
) private {
actions.push(Action({
step: currentStep,
actionType: actionType,
timestamp: block.timestamp,
blockNumber: block.number,
actor: actor,
data: data
}));
emit ActionRecorded(currentStep, actionType, actor);
currentStep++;
}
/**
* @notice Export scenario to JSON format
* @dev This function generates JSON that can be written to file
*/
function exportToJson() external view returns (string memory) {
string memory json = "{";
// Metadata
json = string.concat(json, '"scenario":{');
json = string.concat(json, '"seed":', vm.toString(metadata.seed), ',');
json = string.concat(json, '"optimizer":"', metadata.optimizer, '",');
json = string.concat(json, '"initialEth":"', vm.toString(metadata.initialEth), '",');
json = string.concat(json, '"traderInitialEth":"', vm.toString(metadata.traderInitialEth), '",');
json = string.concat(json, '"startTimestamp":', vm.toString(metadata.startTimestamp), ',');
json = string.concat(json, '"startBlock":', vm.toString(metadata.startBlock), ',');
json = string.concat(json, '"token0isWeth":', metadata.token0isWeth ? "true" : "false", ',');
json = string.concat(json, '"poolFee":', vm.toString(metadata.poolFee));
json = string.concat(json, '},');
// Actions
json = string.concat(json, '"actions":[');
for (uint256 i = 0; i < actions.length; i++) {
if (i > 0) json = string.concat(json, ',');
json = string.concat(json, _actionToJson(i));
}
json = string.concat(json, ']}');
return json;
}
function _actionToJson(uint256 index) private view returns (string memory) {
Action memory action = actions[index];
string memory json = "{";
json = string.concat(json, '"step":', vm.toString(action.step), ',');
json = string.concat(json, '"type":"', action.actionType, '",');
json = string.concat(json, '"timestamp":', vm.toString(action.timestamp), ',');
json = string.concat(json, '"block":', vm.toString(action.blockNumber), ',');
json = string.concat(json, '"actor":"', vm.toString(action.actor), '"');
// Add pre/post states if they exist
if (preStates[index].poolPrice > 0) {
json = string.concat(json, ',"preState":', _stateToJson(preStates[index]));
}
if (postStates[index].poolPrice > 0) {
json = string.concat(json, ',"postState":', _stateToJson(postStates[index]));
}
json = string.concat(json, '}');
return json;
}
function _stateToJson(StateSnapshot memory state) private view returns (string memory) {
string memory json = "{";
json = string.concat(json, '"traderWeth":"', vm.toString(state.traderWeth), '",');
json = string.concat(json, '"traderKraiken":"', vm.toString(state.traderKraiken), '",');
json = string.concat(json, '"currentTick":', vm.toString(state.currentTick), ',');
json = string.concat(json, '"poolPrice":"', vm.toString(state.poolPrice), '",');
json = string.concat(json, '"vwap":"', vm.toString(state.vwap), '",');
json = string.concat(json, '"outstandingSupply":"', vm.toString(state.outstandingSupply), '"');
json = string.concat(json, '}');
return json;
}
}