harb/onchain/analysis/FuzzingAnalysis.s.sol
2025-08-09 18:03:31 +02:00

329 lines
No EOL
14 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 {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
import "../test/mocks/BullMarketOptimizer.sol";
import "../test/mocks/NeutralMarketOptimizer.sol";
import "../test/mocks/BearMarketOptimizer.sol";
import "../test/mocks/WhaleOptimizer.sol";
import "../test/mocks/MockOptimizer.sol";
import "../test/mocks/RandomScenarioOptimizer.sol";
import "./helpers/CSVManager.sol";
import "./helpers/SwapExecutor.sol";
/**
* @title FuzzingAnalysis
* @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager
* @dev Configurable via environment variables:
* - FUZZING_RUNS: Number of fuzzing iterations per market (default 100)
* - TRACK_POSITIONS: Track detailed position data (default false)
*/
contract FuzzingAnalysis is Test, CSVManager {
TestEnvironment testEnv;
IUniswapV3Factory factory;
IUniswapV3Pool pool;
IWETH9 weth;
Kraiken harberg;
Stake stake;
LiquidityManager lm;
bool token0isWeth;
address account = makeAddr("trader");
address feeDestination = makeAddr("fees");
// Analysis metrics
uint256 public scenariosAnalyzed;
uint256 public profitableScenarios;
// Configuration
uint256 public fuzzingRuns;
bool public trackPositions;
string public optimizerClass;
uint256 public tradesPerRun;
// Optimizers
BullMarketOptimizer bullOptimizer;
NeutralMarketOptimizer neutralOptimizer;
BearMarketOptimizer bearOptimizer;
WhaleOptimizer whaleOptimizer;
MockOptimizer mockOptimizer;
RandomScenarioOptimizer randomOptimizer;
function run() public {
_loadConfiguration();
console.log("=== Fuzzing Analysis ===");
console.log(string.concat("Optimizer: ", optimizerClass));
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun)));
console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled"));
console.log("");
testEnv = new TestEnvironment(feeDestination);
// Get optimizer based on class name
address optimizerAddress = _getOptimizerByClass(optimizerClass);
// Initialize CSV for profitable scenarios
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n";
uint256 profitableCount;
uint256 marketProfitable = 0;
console.log(string.concat("=== FUZZING with ", optimizerClass, " ==="));
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 for each run
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress);
// Fund account with random amount (10-50 ETH)
uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether);
vm.deal(account, fundAmount * 2);
vm.prank(account);
weth.deposit{value: fundAmount}();
uint256 initialBalance = weth.balanceOf(account);
// Initial recenter
vm.warp(block.timestamp + 5 hours);
vm.prank(feeDestination);
try lm.recenter() {} catch {}
// Run trading scenario
uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed);
scenariosAnalyzed++;
// Check profitability
if (finalBalance > initialBalance) {
profitableScenarios++;
marketProfitable++;
uint256 profit = finalBalance - initialBalance;
uint256 profitPercentage = (profit * 100) / initialBalance;
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitPercentage), "%"));
// Add to CSV
profitableCSV = string.concat(
profitableCSV,
optimizerClass, ",",
vm.toString(seed), ",",
vm.toString(initialBalance), ",",
vm.toString(finalBalance), ",",
vm.toString(profit), ",",
vm.toString(profitPercentage), "\n"
);
profitableCount++;
}
}
console.log(string.concat("\nResults for ", optimizerClass, ":"));
console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns)));
console.log("");
console.log("=== ANALYSIS COMPLETE ===");
console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios)));
// Write profitable scenarios CSV if any found
if (profitableCount > 0) {
console.log("Writing profitable scenarios CSV...");
string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv");
vm.writeFile(filename, profitableCSV);
console.log(string.concat("\nProfitable scenarios written to: ", filename));
} else {
console.log("\nNo profitable scenarios found.");
}
console.log("Script execution complete.");
}
function _loadConfiguration() internal {
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100));
trackPositions = vm.envOr("TRACK_POSITIONS", false);
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20));
}
function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) {
// Initialize position tracking CSV if enabled
if (trackPositions) {
initializePositionsCSV();
_recordPositionData("Initial");
}
// Use seed for randomness
uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp)));
// Use configured number of trades (with some randomness)
uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades
if (numTrades < 5) numTrades = 5; // Minimum 5 trades
// Initial buy if no HARB
if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) {
uint256 initialBuy = weth.balanceOf(account) / 10;
_executeBuy(initialBuy);
}
// Execute random trades
for (uint256 i = 0; i < numTrades; i++) {
rand = uint256(keccak256(abi.encodePacked(rand, i)));
uint256 action = rand % 100;
if (action < 40) { // 40% chance buy
uint256 wethBal = weth.balanceOf(account);
if (wethBal > 0) {
uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100%
uint256 buyAmount = (wethBal * buyPercent) / 1000;
if (buyAmount > 0) _executeBuy(buyAmount);
}
} else if (action < 80) { // 40% chance sell
uint256 harbBal = harberg.balanceOf(account);
if (harbBal > 0) {
uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100%
uint256 sellAmount = (harbBal * sellPercent) / 1000;
if (sellAmount > 0) _executeSell(sellAmount);
}
} else if (action < 95) { // 15% chance recenter
uint256 waitTime = 1 minutes + (rand % 10 hours);
vm.warp(block.timestamp + waitTime);
vm.prank(feeDestination);
try lm.recenter() {
if (trackPositions) {
_recordPositionData(string.concat("Recenter_", vm.toString(i)));
}
} catch {}
} else { // 5% chance wait
vm.warp(block.timestamp + 1 minutes + (rand % 2 hours));
}
// Skip trades at extreme ticks
(, int24 currentTick, , , , , ) = pool.slot0();
if (currentTick < -887000 || currentTick > 887000) continue;
}
// Sell remaining HARB
uint256 finalHarb = harberg.balanceOf(account);
if (finalHarb > 0) _executeSell(finalHarb);
// Final recenters
for (uint256 j = 0; j < 1 + (rand % 3); j++) {
vm.warp(block.timestamp + 5 hours);
vm.prank(feeDestination);
try lm.recenter() {} catch {}
}
// Write position tracking CSV if enabled
if (trackPositions) {
string memory positionFilename = string.concat(
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
);
writeCSVToFile(positionFilename);
}
return weth.balanceOf(account);
}
function _executeBuy(uint256 amount) internal {
if (amount == 0 || weth.balanceOf(account) < amount) return;
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
vm.prank(account);
weth.transfer(address(executor), amount);
try executor.executeBuy(amount, account) {} catch {}
}
function _executeSell(uint256 amount) internal {
if (amount == 0 || harberg.balanceOf(account) < amount) return;
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
vm.prank(account);
harberg.transfer(address(executor), amount);
try executor.executeSell(amount, account) {} catch {}
}
function _getOrCreateOptimizer(uint256 index) internal returns (address) {
if (index == 0) {
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
return address(bullOptimizer);
} else if (index == 1) {
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
return address(neutralOptimizer);
} else {
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
return address(bearOptimizer);
}
}
function _getOptimizerByClass(string memory className) internal returns (address) {
bytes32 classHash = keccak256(abi.encodePacked(className));
if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) {
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
return address(bullOptimizer);
} else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) {
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
return address(neutralOptimizer);
} else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) {
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
return address(bearOptimizer);
} else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) {
if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer();
return address(whaleOptimizer);
} else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) {
if (address(mockOptimizer) == address(0)) {
mockOptimizer = new MockOptimizer();
mockOptimizer.initialize(address(harberg), address(stake));
}
return address(mockOptimizer);
} else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) {
if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer();
return address(randomOptimizer);
} else {
revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer"));
}
}
function _recordPositionData(string memory label) internal {
(,int24 currentTick,,,,,) = pool.slot0();
// Get each position
(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);
// Create position data row
string memory row = string.concat(
label, ",",
vm.toString(currentTick), ",",
vm.toString(floorLiq), ",",
vm.toString(floorLower), ",",
vm.toString(floorUpper), ",",
vm.toString(anchorLiq), ",",
vm.toString(anchorLower), ",",
vm.toString(anchorUpper), ",",
vm.toString(discoveryLiq), ",",
vm.toString(discoveryLower), ",",
vm.toString(discoveryUpper), ",",
vm.toString(weth.balanceOf(address(lm))), ",",
vm.toString(harberg.balanceOf(address(lm)))
);
appendCSVRow(row);
}
}