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>
241 lines
No EOL
8 KiB
Solidity
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;
|
|
}
|
|
} |