harb/onchain/analysis/ImprovedFuzzingAnalysis.s.sol
johba 10702f5aa3 feat: Enhance fuzzing with staking integration and configurable parameters
- Add staking/unstaking actions to fuzzing scenarios (stake every 3rd trade)
- Implement snatching logic when stake pool reaches capacity (uses max tax rate)
- Add configurable parameters:
  - buyBias: Control buy vs sell ratio (0-100%)
  - stakingBias: Control stake vs unstake ratio (0-100%)
  - tradesPerRun: Configure number of trades per scenario
  - staking: Enable/disable staking entirely
- Simplify to single trading strategy (_executeRandomLargeTrades)
- Fix memory issues by recording only every 5th trade to CSV
- Track staking metrics (stakes attempted/succeeded, snatches attempted/succeeded)
- Update CLAUDE.md with new fuzzing parameters and usage examples
- Clean up old TODO files and unused code

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 14:57:49 +02:00

570 lines
No EOL
25 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import {TestEnvironment} from "../test/helpers/TestBase.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import {IWETH9} from "../src/interfaces/IWETH9.sol";
import {Kraiken} from "../src/Kraiken.sol";
import {Stake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
import {UniswapHelpers} from "../src/helpers/UniswapHelpers.sol";
import "../test/mocks/BullMarketOptimizer.sol";
import "../test/mocks/WhaleOptimizer.sol";
import "./helpers/CSVManager.sol";
import "./helpers/SwapExecutor.sol";
/**
* @title ImprovedFuzzingAnalysis
* @notice Enhanced fuzzing with larger trades designed to reach discovery position
* @dev Uses more aggressive trading patterns to explore the full liquidity range
*/
contract ImprovedFuzzingAnalysis is Test, CSVManager {
TestEnvironment testEnv;
IUniswapV3Factory factory;
IUniswapV3Pool pool;
IWETH9 weth;
Kraiken harberg;
Stake stake;
LiquidityManager lm;
bool token0isWeth;
// Reusable swap executor to avoid repeated deployments
SwapExecutor swapExecutor;
address account = makeAddr("trader");
address whale = makeAddr("whale");
address feeDestination = makeAddr("fees");
// Analysis metrics
uint256 public scenariosAnalyzed;
uint256 public profitableScenarios;
uint256 public discoveryReachedCount;
uint256 public totalStakesAttempted;
uint256 public totalStakesSucceeded;
uint256 public totalSnatchesAttempted;
uint256 public totalSnatchesSucceeded;
// Staking tracking
mapping(address => uint256[]) public activePositions;
uint256[] public allPositionIds; // Track all positions for snatching
// Configuration
uint256 public fuzzingRuns;
bool public trackPositions;
bool public enableStaking;
uint256 public buyBias; // 0-100, percentage bias towards buying vs selling
uint256 public tradesPerRun; // Number of trades/actions per scenario
uint256 public stakingBias; // 0-100, percentage bias towards staking vs unstaking
string public optimizerClass;
function run() public virtual {
_loadConfiguration();
console.log("=== IMPROVED Fuzzing Analysis ===");
console.log("Designed to reach discovery position with larger trades");
console.log(string.concat("Optimizer: ", optimizerClass));
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
console.log(string.concat("Staking enabled: ", enableStaking ? "true" : "false"));
console.log("");
testEnv = new TestEnvironment(feeDestination);
// Deploy factory once for all runs (gas optimization)
factory = UniswapHelpers.deployUniswapFactory();
// Get optimizer
address optimizerAddress = _getOptimizerByClass(optimizerClass);
// Track profitable scenarios
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached\n";
uint256 profitableCount;
for (uint256 seed = 0; seed < fuzzingRuns; seed++) {
if (seed % 10 == 0 && seed > 0) {
console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns)));
}
// Create fresh environment with existing factory
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithExistingFactory(factory, seed % 2 == 0, feeDestination, optimizerAddress);
// Fund LiquidityManager with MORE ETH for deeper liquidity
vm.deal(address(lm), 200 ether); // Increased from 50
// Fund accounts with MORE capital
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(seed, "trader"))) % 150 ether); // 50-200 ETH
uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(seed, "whale"))) % 300 ether); // 200-500 ETH
vm.deal(account, traderFund * 2);
vm.deal(whale, whaleFund * 2);
vm.prank(account);
weth.deposit{value: traderFund}();
vm.prank(whale);
weth.deposit{value: whaleFund}();
// Create SwapExecutor once per scenario to avoid repeated deployments
swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth);
uint256 initialBalance = weth.balanceOf(account);
// Initial recenter
vm.prank(feeDestination);
try lm.recenter{gas: 50_000_000}() {} catch {}
// Initialize position tracking for each seed
if (trackPositions) {
// Initialize CSV header for each seed (after clearCSV from previous run)
initializePositionsCSV();
_recordPositionData("Initial");
}
// Run improved trading scenario
(uint256 finalBalance, bool reachedDiscovery) = _runImprovedScenario(seed);
scenariosAnalyzed++;
if (reachedDiscovery) {
discoveryReachedCount++;
}
// Check profitability
if (finalBalance > initialBalance) {
uint256 profit = finalBalance - initialBalance;
uint256 profitPct = (profit * 100) / initialBalance;
profitableScenarios++;
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed)));
console.log(string.concat(" Profit: ", vm.toString(profit / 1e15), " finney (", vm.toString(profitPct), "%)"));
console.log(string.concat(" Discovery reached: ", reachedDiscovery ? "YES" : "NO"));
profitableCSV = string.concat(
profitableCSV,
optimizerClass, ",",
vm.toString(seed), ",",
vm.toString(initialBalance), ",",
vm.toString(finalBalance), ",",
vm.toString(profit), ",",
vm.toString(profitPct), ",",
reachedDiscovery ? "true" : "false", "\n"
);
profitableCount++;
}
// Write position CSV if tracking
if (trackPositions) {
_recordPositionData("Final");
string memory positionFilename = string.concat(
"improved_positions_", optimizerClass, "_", vm.toString(seed), ".csv"
);
writeCSVToFile(positionFilename);
clearCSV(); // Clear buffer for next run
}
}
// Summary
console.log("\n=== ANALYSIS COMPLETE ===");
console.log(string.concat("Total scenarios: ", vm.toString(scenariosAnalyzed)));
console.log(string.concat("Profitable scenarios: ", vm.toString(profitableScenarios)));
console.log(string.concat("Discovery reached: ", vm.toString(discoveryReachedCount), " times"));
console.log(string.concat("Discovery rate: ", vm.toString((discoveryReachedCount * 100) / scenariosAnalyzed), "%"));
console.log(string.concat("Profit rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
if (enableStaking) {
console.log("\n=== STAKING METRICS ===");
console.log(string.concat("Stakes attempted: ", vm.toString(totalStakesAttempted)));
console.log(string.concat("Stakes succeeded: ", vm.toString(totalStakesSucceeded)));
console.log(string.concat("Snatches attempted: ", vm.toString(totalSnatchesAttempted)));
console.log(string.concat("Snatches succeeded: ", vm.toString(totalSnatchesSucceeded)));
if (totalStakesAttempted > 0) {
console.log(string.concat("Stake success rate: ", vm.toString((totalStakesSucceeded * 100) / totalStakesAttempted), "%"));
}
if (totalSnatchesAttempted > 0) {
console.log(string.concat("Snatch success rate: ", vm.toString((totalSnatchesSucceeded * 100) / totalSnatchesAttempted), "%"));
}
}
if (profitableCount > 0) {
string memory filename = string.concat("improved_profitable_", vm.toString(block.timestamp), ".csv");
vm.writeFile(filename, profitableCSV);
console.log(string.concat("\nResults written to: ", filename));
}
}
function _runImprovedScenario(uint256 seed) internal virtual returns (uint256 finalBalance, bool reachedDiscovery) {
uint256 rand = uint256(keccak256(abi.encodePacked(seed, block.timestamp)));
// Get initial discovery position
(, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Initial buy to generate some KRAIKEN tokens for trading/staking
_executeBuy(account, weth.balanceOf(account) / 4); // Buy 25% of ETH worth
_executeBuy(whale, weth.balanceOf(whale) / 4); // Whale buys 25% of ETH worth
// Always use random trading strategy for consistent behavior
_executeRandomLargeTrades(rand);
// Check if we reached discovery
(, int24 currentTick,,,,,) = pool.slot0();
reachedDiscovery = (currentTick >= discoveryLower && currentTick < discoveryUpper);
if (reachedDiscovery) {
console.log(" [DISCOVERY REACHED] at tick", vm.toString(currentTick));
if (trackPositions) {
_recordPositionData("Discovery_Reached");
}
}
// Final cleanup: sell all KRAIKEN
uint256 finalKraiken = harberg.balanceOf(account);
if (finalKraiken > 0) {
_executeSell(account, finalKraiken);
}
finalBalance = weth.balanceOf(account);
}
function _executeRandomLargeTrades(uint256 rand) internal {
console.log(" Strategy: Random Large Trades");
console.log(" Buy bias:", buyBias, "%");
console.log(" Trades:", tradesPerRun);
if (enableStaking) {
console.log(" Staking bias:", stakingBias, "%");
}
for (uint256 i = 0; i < tradesPerRun; i++) {
rand = uint256(keccak256(abi.encodePacked(rand, i)));
uint256 actor = rand % 2; // 0 = trader, 1 = whale
// Use buy bias to determine action
uint256 actionRoll = rand % 100;
uint256 action;
if (actionRoll < buyBias) {
action = 0; // Buy (biased towards buying when buyBias > 50)
} else if (actionRoll < 90) {
action = 1; // Sell (reduced probability when buyBias is high)
} else {
action = 2; // Recenter (10% chance)
}
address actorAddr = actor == 0 ? account : whale;
if (action == 0) {
// Large buy (30-80% of balance, or more with high buy bias)
uint256 buyPct = buyBias > 70 ? 50 + (rand % 40) : 30 + (rand % 51);
uint256 buyAmount = weth.balanceOf(actorAddr) * buyPct / 100;
if (buyAmount > 0) {
_executeBuy(actorAddr, buyAmount);
}
} else if (action == 1) {
// Large sell (reduced amounts with high buy bias)
uint256 sellPct = buyBias > 70 ? 10 + (rand % 30) : 30 + (rand % 71);
uint256 sellAmount = harberg.balanceOf(actorAddr) * sellPct / 100;
if (sellAmount > 0) {
_executeSell(actorAddr, sellAmount);
}
} else {
// Recenter
vm.warp(block.timestamp + (rand % 2 hours));
vm.prank(feeDestination);
try lm.recenter{gas: 50_000_000}() {} catch {}
}
// Every 3rd trade, attempt staking/unstaking if enabled
if (enableStaking && i % 3 == 2) {
uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100;
if (stakingRoll < stakingBias) {
// Try to stake
_executeStake(rand + i * 1000);
} else {
// Try to unstake
_executeExitPosition(rand + i * 1000);
}
}
// Only record every 5th trade to avoid memory issues with large trade counts
if (trackPositions && i % 5 == 0) {
_recordPositionData("Trade");
}
}
}
function _executeBuy(address buyer, uint256 amount) internal virtual {
if (amount == 0 || weth.balanceOf(buyer) < amount) return;
vm.prank(buyer);
weth.transfer(address(swapExecutor), amount);
try swapExecutor.executeBuy(amount, buyer) {} catch {}
}
function _executeSell(address seller, uint256 amount) internal virtual {
if (amount == 0 || harberg.balanceOf(seller) < amount) return;
vm.prank(seller);
harberg.transfer(address(swapExecutor), amount);
try swapExecutor.executeSell(amount, seller) {} catch {}
}
function _getOptimizerByClass(string memory class) internal returns (address) {
if (keccak256(bytes(class)) == keccak256("BullMarketOptimizer")) {
return address(new BullMarketOptimizer());
} else if (keccak256(bytes(class)) == keccak256("WhaleOptimizer")) {
return address(new WhaleOptimizer());
} else {
return address(new BullMarketOptimizer());
}
}
function _loadConfiguration() internal {
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(20));
trackPositions = vm.envOr("TRACK_POSITIONS", false);
enableStaking = vm.envOr("ENABLE_STAKING", true); // Default to true
buyBias = vm.envOr("BUY_BIAS", uint256(50)); // Default 50% (balanced)
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); // Default 15 trades
stakingBias = vm.envOr("STAKING_BIAS", uint256(80)); // Default 80% stake vs 20% unstake
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
}
function _recordPositionData(string memory label) internal {
// Split into separate function calls to avoid stack too deep
_recordPositionDataInternal(label);
}
function _recordPositionDataInternal(string memory label) private {
// Disable position tracking if it causes memory issues
if (bytes(csv).length > 50000) {
// CSV is getting too large, skip recording
return;
}
(,int24 currentTick,,,,,) = pool.slot0();
// Get position data
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Use simpler row building to avoid memory issues
string memory row = label;
row = string.concat(row, ",", vm.toString(currentTick));
row = string.concat(row, ",", vm.toString(floorLower));
row = string.concat(row, ",", vm.toString(floorUpper));
row = string.concat(row, ",", vm.toString(floorLiq));
row = string.concat(row, ",", vm.toString(anchorLower));
row = string.concat(row, ",", vm.toString(anchorUpper));
row = string.concat(row, ",", vm.toString(anchorLiq));
row = string.concat(row, ",", vm.toString(discoveryLower));
row = string.concat(row, ",", vm.toString(discoveryUpper));
row = string.concat(row, ",", vm.toString(discoveryLiq));
row = string.concat(row, ",", token0isWeth ? "true" : "false");
row = string.concat(row, ",", vm.toString(stake.getPercentageStaked()));
row = string.concat(row, ",", vm.toString(stake.getAverageTaxRate()));
appendCSVRow(row);
}
function _executeStakingAction(uint256 rand) internal {
uint256 action = rand % 100;
// 70% chance to stake, 5% chance to exit (to fill pool faster)
if (action < 70) {
_executeStake(rand);
} else if (action < 75) {
_executeExitPosition(rand);
}
}
function _executeStakeWithAmount(address staker, uint256 amount, uint32 taxRate) internal {
// Direct stake with specific amount and tax rate
_doStake(staker, amount, taxRate);
}
function _executeStake(uint256 rand) internal {
address staker = rand % 2 == 0 ? account : whale;
uint256 harbBalance = harberg.balanceOf(staker);
if (harbBalance > harberg.minStake()) {
// Stake between 30% and 80% of balance (increased from 10-50%)
uint256 amount = harbBalance * (30 + (rand % 50)) / 100;
if (amount < harberg.minStake()) {
amount = harberg.minStake();
}
// Random tax rate (index 0-29)
uint32 taxRate = uint32(rand % 30);
vm.prank(staker);
harberg.approve(address(stake), amount);
_doStake(staker, amount, taxRate);
}
}
function _doStake(address staker, uint256 amount, uint32 taxRate) internal {
vm.startPrank(staker);
// Check current pool capacity before attempting stake
uint256 currentPercentStaked = stake.getPercentageStaked();
// First try to stake without snatching
totalStakesAttempted++;
try stake.snatch(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) {
totalStakesSucceeded++;
activePositions[staker].push(positionId);
allPositionIds.push(positionId);
console.log(" STAKED:", amount / 1e18);
console.log(" Tax rate index:", taxRate);
console.log(" Pool now at:", currentPercentStaked / 1e16);
if (trackPositions) {
_recordPositionData("Stake");
}
} catch Error(string memory reason) {
// If staking failed (likely pool is full), try snatching with max tax rate
console.log(" Stake failed:", reason);
console.log(" Pool at:", currentPercentStaked / 1e16, "percent - attempting snatch...");
// Use max tax rate (29) for snatching
uint32 maxTaxRate = 29;
// Find positions to snatch (those with lower tax rates)
uint256[] memory positionsToSnatch = _findSnatchablePositions(maxTaxRate, amount);
if (positionsToSnatch.length > 0) {
totalSnatchesAttempted++;
try stake.snatch(amount, staker, maxTaxRate, positionsToSnatch) returns (uint256 positionId) {
totalSnatchesSucceeded++;
activePositions[staker].push(positionId);
allPositionIds.push(positionId);
// Remove snatched positions from tracking
_removeSnatchedPositions(positionsToSnatch);
console.log(" SNATCHED positions:", positionsToSnatch.length);
console.log(" Staked amount:", amount / 1e18);
if (trackPositions) {
_recordPositionData("Snatch");
}
} catch Error(string memory sReason) {
console.log(" Snatching failed:", sReason);
} catch {
console.log(" Snatching failed with unknown error");
}
} else {
console.log(" No snatchable positions found");
}
} catch {
// Catch-all for non-string errors
console.log(" Stake failed (unknown error) at", currentPercentStaked / 1e16, "percent");
}
vm.stopPrank();
}
function _executeExitPosition(uint256 rand) internal {
address staker = rand % 2 == 0 ? account : whale;
if (activePositions[staker].length > 0) {
uint256 index = rand % activePositions[staker].length;
uint256 positionId = activePositions[staker][index];
vm.prank(staker);
try stake.exitPosition(positionId) {
// Remove from array
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
activePositions[staker].pop();
// Also remove from allPositionIds
for (uint256 j = 0; j < allPositionIds.length; j++) {
if (allPositionIds[j] == positionId) {
allPositionIds[j] = 0; // Mark as removed
break;
}
}
if (trackPositions) {
_recordPositionData("ExitStake");
}
} catch {
// Exit failed (position might be liquidated), remove from tracking
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
activePositions[staker].pop();
// Also remove from allPositionIds
for (uint256 j = 0; j < allPositionIds.length; j++) {
if (allPositionIds[j] == positionId) {
allPositionIds[j] = 0; // Mark as removed
break;
}
}
}
}
}
function _findSnatchablePositions(uint32 maxTaxRate, uint256 amountNeeded) internal view returns (uint256[] memory) {
// Find positions with tax rates lower than maxTaxRate
uint256[] memory snatchable = new uint256[](10); // Max 10 positions to snatch
uint256 count = 0;
uint256 totalShares = 0;
for (uint256 i = 0; i < allPositionIds.length && count < 10; i++) {
uint256 positionId = allPositionIds[i];
if (positionId == 0) continue;
// Get position info
(uint256 shares, address owner,, , uint32 taxRate) = stake.positions(positionId);
// Skip if position doesn't exist or tax rate is too high
if (shares == 0 || taxRate >= maxTaxRate) continue;
snatchable[count] = positionId;
totalShares += shares;
count++;
// Check if we have enough shares to cover the amount needed
uint256 assetsFromShares = stake.sharesToAssets(totalShares);
if (assetsFromShares >= amountNeeded) {
break;
}
}
// Resize array to actual count
uint256[] memory result = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
result[i] = snatchable[i];
}
return result;
}
function _removeSnatchedPositions(uint256[] memory snatchedIds) internal {
// Remove snatched positions from allPositionIds
for (uint256 i = 0; i < snatchedIds.length; i++) {
uint256 snatchedId = snatchedIds[i];
// Find and remove from allPositionIds
for (uint256 j = 0; j < allPositionIds.length; j++) {
if (allPositionIds[j] == snatchedId) {
allPositionIds[j] = 0; // Mark as removed
break;
}
}
// Remove from owner's activePositions
for (uint256 k = 0; k < 2; k++) {
address owner = k == 0 ? account : whale;
uint256[] storage ownerPositions = activePositions[owner];
for (uint256 m = 0; m < ownerPositions.length; m++) {
if (ownerPositions[m] == snatchedId) {
ownerPositions[m] = ownerPositions[ownerPositions.length - 1];
ownerPositions.pop();
break;
}
}
}
}
}
}