harb/onchain/analysis/FuzzingAnalysis.s.sol
johba 2205ae719b feat: Optimize discovery position depth calculation
- Implement dynamic discovery depth based on anchor position share
- Add configurable discovery_max_multiple (1.5-4x) for flexible adjustment
- Update BullMarketOptimizer with new depth calculation logic
- Fix scenario visualizer floor position visibility
- Add comprehensive tests for discovery depth behavior

The discovery position now dynamically adjusts its depth based on the anchor
position's share of total liquidity, allowing for more effective price discovery
while maintaining protection against manipulation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-16 16:45:24 +02:00

498 lines
No EOL
24 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";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.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;
uint256 public seedOffset;
// 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 = seedOffset; seed < seedOffset + 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 LiquidityManager with initial ETH
vm.deal(address(lm), 50 ether);
// 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++;
// Calculate profit/loss
bool isProfitable = finalBalance > initialBalance;
uint256 profitOrLoss;
uint256 profitOrLossPercentage;
if (isProfitable) {
profitOrLoss = finalBalance - initialBalance;
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
profitableScenarios++;
marketProfitable++;
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitOrLossPercentage), "%"));
// Add to CSV
profitableCSV = string.concat(
profitableCSV,
optimizerClass, ",",
vm.toString(seed), ",",
vm.toString(initialBalance), ",",
vm.toString(finalBalance), ",",
vm.toString(profitOrLoss), ",",
vm.toString(profitOrLossPercentage), "\n"
);
profitableCount++;
} else {
profitOrLoss = initialBalance - finalBalance;
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
}
// Always log result for cumulative tracking
console.log(string.concat("RESULT|SEED:", vm.toString(seed), "|INITIAL:", vm.toString(initialBalance), "|FINAL:", vm.toString(finalBalance), "|PNL:", isProfitable ? "+" : "-", vm.toString(profitOrLoss)));
}
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)));
console.log(string.concat("Profitable rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
// 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));
seedOffset = vm.envOr("SEED_OFFSET", uint256(0));
}
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 < 25) { // 25% 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);
if (trackPositions) {
_recordPositionData(string.concat("Buy_", vm.toString(i)));
}
}
}
} else if (action < 50) { // 25% 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);
if (trackPositions) {
_recordPositionData(string.concat("Sell_", vm.toString(i)));
}
}
}
} else { // 50% 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 {}
}
// 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);
if (trackPositions) {
_recordPositionData("Final_Sell");
}
}
// 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) {
_recordPositionData("Final");
string memory positionFilename = string.concat(
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
);
writeCSVToFile(positionFilename);
console.log(string.concat("Position tracking CSV written to: ", 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();
// Cap currentTick to avoid overflow in extreme cases
if (currentTick > 887000) currentTick = 887000;
if (currentTick < -887000) currentTick = -887000;
// 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);
// Calculate ETH and HARB amounts in each position using proper Uniswap math
uint256 floorEth = 0;
uint256 floorHarb = 0;
uint256 anchorEth = 0;
uint256 anchorHarb = 0;
uint256 discoveryEth = 0;
uint256 discoveryHarb = 0;
// Debug: Log liquidity values
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
console.log("=== LIQUIDITY VALUES ===");
console.log("Label:", label);
console.log("Current tick:", uint256(int256(currentTick)));
console.log("Anchor range:", uint256(int256(anchorLower)), "-", uint256(int256(anchorUpper)));
console.log("Anchor liquidity:", uint256(anchorLiq));
console.log("Discovery range:", uint256(int256(discoveryLower)), "-", uint256(int256(discoveryUpper)));
console.log("Discovery liquidity:", uint256(discoveryLiq));
if (uint256(anchorLiq) > 0) {
console.log("Discovery/Anchor liquidity ratio:", uint256(discoveryLiq) * 100 / uint256(anchorLiq), "%");
console.log("Anchor width:", uint256(int256(anchorUpper - anchorLower)), "ticks");
console.log("Discovery width:", uint256(int256(discoveryUpper - discoveryLower)), "ticks");
uint256 anchorLiqPerTick = uint256(anchorLiq) * 1000 / uint256(int256(anchorUpper - anchorLower));
uint256 discoveryLiqPerTick = uint256(discoveryLiq) * 1000 / uint256(int256(discoveryUpper - discoveryLower));
console.log("Anchor liquidity per tick (x1000):", anchorLiqPerTick);
console.log("Discovery liquidity per tick (x1000):", discoveryLiqPerTick);
console.log("Discovery/Anchor per tick ratio:", discoveryLiqPerTick * 100 / anchorLiqPerTick, "%");
}
}
// Calculate amounts for each position using LiquidityAmounts library
if (floorLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorUpper);
// Calculate actual deposited amounts based on position relative to current price
if (token0isWeth) {
if (currentTick < floorLower) {
// Position is above current price - contains only token1 (KRAIKEN)
floorEth = 0;
// Use position's lower tick for actual deposited amount
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
} else if (currentTick >= floorUpper) {
// Position is below current price - contains only token0 (WETH)
// Use position's upper tick for actual deposited amount
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
floorHarb = 0;
} else {
// Current price is within the position
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
}
} else {
if (currentTick < floorLower) {
// Position is above current price
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
floorEth = 0;
} else if (currentTick >= floorUpper) {
// Position is below current price
floorHarb = 0;
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
} else {
// Current price is within the position
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
}
}
}
if (anchorLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(anchorLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(anchorUpper);
if (token0isWeth) {
if (currentTick < anchorLower) {
anchorEth = 0;
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
} else if (currentTick >= anchorUpper) {
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
anchorHarb = 0;
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
}
} else {
if (currentTick < anchorLower) {
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
anchorEth = 0;
} else if (currentTick >= anchorUpper) {
anchorHarb = 0;
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
}
}
}
if (discoveryLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(discoveryLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(discoveryUpper);
if (token0isWeth) {
if (currentTick < discoveryLower) {
discoveryEth = 0;
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
} else if (currentTick >= discoveryUpper) {
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
discoveryHarb = 0;
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
}
} else {
if (currentTick < discoveryLower) {
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
discoveryEth = 0;
} else if (currentTick >= discoveryUpper) {
discoveryHarb = 0;
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
}
}
}
// Create position data row matching the expected CSV format
string memory row = string.concat(
label, ", ",
vm.toString(currentTick), ", ",
vm.toString(floorLower), ", ",
vm.toString(floorUpper), ", ",
vm.toString(floorEth), ", ",
vm.toString(floorHarb), ", ",
vm.toString(anchorLower), ", ",
vm.toString(anchorUpper), ", ",
vm.toString(anchorEth), ", ",
vm.toString(anchorHarb), ", ",
vm.toString(discoveryLower), ", ",
vm.toString(discoveryUpper), ", ",
vm.toString(discoveryEth), ", ",
vm.toString(discoveryHarb), ", ",
token0isWeth ? "true" : "false"
);
appendCSVRow(row);
}
}