From c72fe56ad094206baf126cac6e5bfa804a3b49f2 Mon Sep 17 00:00:00 2001 From: johba Date: Sat, 23 Aug 2025 16:10:05 +0200 Subject: [PATCH] bounded buy/sell --- onchain/.gitignore | 2 +- .../analysis/ImprovedFuzzingAnalysis.s.sol | 157 +++++++++++------- onchain/analysis/helpers/SwapExecutor.sol | 40 +++-- onchain/analysis/replay-scenario.sh | 4 +- onchain/test/ReplayProfitableScenario.t.sol | 4 +- 5 files changed, 134 insertions(+), 73 deletions(-) diff --git a/onchain/.gitignore b/onchain/.gitignore index 16bafeb..a86b5eb 100644 --- a/onchain/.gitignore +++ b/onchain/.gitignore @@ -19,4 +19,4 @@ docs/ tags analysis/profitable_scenario.csv - +fuzzing_results_* diff --git a/onchain/analysis/ImprovedFuzzingAnalysis.s.sol b/onchain/analysis/ImprovedFuzzingAnalysis.s.sol index a4b9102..f002857 100644 --- a/onchain/analysis/ImprovedFuzzingAnalysis.s.sol +++ b/onchain/analysis/ImprovedFuzzingAnalysis.s.sol @@ -11,6 +11,9 @@ 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 {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; +import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; +import {Math} from "@openzeppelin/utils/math/Math.sol"; import "../test/mocks/BullMarketOptimizer.sol"; import "../test/mocks/WhaleOptimizer.sol"; import "./helpers/CSVManager.sol"; @@ -63,11 +66,8 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { _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); @@ -82,9 +82,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { 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))); - } + // Progress tracking removed // Create fresh environment with existing factory (factory, pool, weth, harberg, stake, lm,, token0isWeth) = @@ -110,14 +108,15 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { weth.deposit{value: traderFund}(); // Create SwapExecutor once per scenario to avoid repeated deployments - swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth); + swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth, lm); - uint256 initialBalance = weth.balanceOf(trader); - - // Initial recenter + // Initial recenter BEFORE recording initial balance vm.prank(feeDestination); try lm.recenter{gas: 50_000_000}() {} catch {} + // Record initial balance AFTER recenter so we account for pool state + uint256 initialBalance = weth.balanceOf(trader); + // Initialize position tracking for each seed if (trackPositions) { // Initialize CSV header for each seed (after clearCSV from previous run) @@ -139,9 +138,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { 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")); + console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " - Profit: ", vm.toString(profitPct), "%")); profitableCSV = string.concat( profitableCSV, @@ -169,31 +166,21 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { // 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), "%")); + 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), "%")); - } + // Staking metrics logged to CSV only } 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)); + // Results written to CSV } } @@ -203,8 +190,12 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { // Get initial discovery position (, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); - // Initial buy to generate some KRAIKEN tokens for trading/staking - _executeBuy(trader, weth.balanceOf(trader) / 4); // Buy 25% of ETH worth + // Initial buy to generate KRAIKEN tokens for trading/staking + // If staking is enabled with 100% bias, buy even more initially + uint256 initialBuyPercent = (enableStaking && stakingBias >= 100) ? 60 : + (enableStaking ? 40 : 25); // 60% if 100% staking, 40% if staking, 25% otherwise + uint256 initialBuyAmount = weth.balanceOf(trader) * initialBuyPercent / 100; + _executeBuy(trader, initialBuyAmount); // Always use random trading strategy for consistent behavior _executeRandomLargeTrades(rand); @@ -215,7 +206,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { reachedDiscovery = (currentTick >= discoveryLower && currentTick < discoveryUpper); if (reachedDiscovery) { - console.log(" [DISCOVERY REACHED] at tick", vm.toString(currentTick)); + // Discovery reached if (trackPositions) { _recordPositionData("Discovery_Reached"); } @@ -223,26 +214,16 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { // Check final balances before cleanup uint256 traderKraiken = harberg.balanceOf(trader); - uint256 stakedAmount = harberg.balanceOf(address(stake)); - uint256 outstandingSupply = harberg.outstandingSupply(); - uint256 minStake = harberg.minStake(); // Final cleanup: sell all KRAIKEN if (traderKraiken > 0) { _executeSell(trader, traderKraiken); } + // Calculate final balance finalBalance = weth.balanceOf(trader); } function _executeRandomLargeTrades(uint256 rand) internal { - console.log(" Strategy: Random Large Trades"); - console.log(string.concat(" Buy bias: ", vm.toString(buyBias), "%")); - console.log(string.concat(" Trades per run: ", vm.toString(tradesPerRun))); - if (enableStaking) { - console.log(string.concat(" Staking bias: ", vm.toString(stakingBias), "%")); - console.log(" Staking every 3rd trade"); - } - uint256 stakingAttempts = 0; for (uint256 i = 0; i < tradesPerRun; i++) { rand = uint256(keccak256(abi.encodePacked(rand, i))); @@ -261,15 +242,15 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { 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 buyPct = buyBias > 80 ? 60 + (rand % 31) : (buyBias > 70 ? 40 + (rand % 41) : 30 + (rand % 51)); uint256 wethBalance = weth.balanceOf(trader); uint256 buyAmount = wethBalance * buyPct / 100; if (buyAmount > 0 && wethBalance > 0) { _executeBuy(trader, buyAmount); } } else if (action == 1) { - // Large sell (reduced amounts with high buy bias) - uint256 sellPct = buyBias > 70 ? 10 + (rand % 30) : 30 + (rand % 71); + // Large sell (significantly reduced with high buy bias to maintain KRAIKEN balance) + uint256 sellPct = buyBias > 80 ? 5 + (rand % 16) : (buyBias > 70 ? 10 + (rand % 21) : 30 + (rand % 71)); uint256 sellAmount = harberg.balanceOf(trader) * sellPct / 100; if (sellAmount > 0) { _executeSell(trader, sellAmount); @@ -286,7 +267,29 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { stakingAttempts++; uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100; if (stakingRoll < stakingBias) { - // Try to stake + // Before staking, ensure we have tokens + uint256 harbBalance = harberg.balanceOf(trader); + uint256 minStakeAmount = harberg.minStake(); + + // With 100% staking bias, aggressively buy tokens if needed + // We want to maintain a large KRAIKEN balance for staking + if (stakingBias >= 100 && harbBalance <= minStakeAmount * 10) { + uint256 wethBalance = weth.balanceOf(trader); + if (wethBalance > 0) { + // Buy 30-50% of ETH worth to get substantial tokens for staking + uint256 buyAmount = wethBalance * (30 + (rand % 21)) / 100; + _executeBuy(trader, buyAmount); + } + } else if (harbBalance <= minStakeAmount * 2) { + uint256 wethBalance = weth.balanceOf(trader); + if (wethBalance > 0) { + // Buy 15-25% of ETH worth to get tokens for staking + uint256 buyAmount = wethBalance * (15 + (rand % 11)) / 100; + _executeBuy(trader, buyAmount); + } + } + + // Now try to stake _executeStake(rand + i * 1000); } else { // Try to unstake @@ -302,10 +305,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { uint256 expectedStakeActions = tradesPerRun / 3; uint256 expectedStakes = (expectedStakeActions * stakingBias) / 100; - console.log(string.concat(" Total staking actions: ", vm.toString(stakingAttempts), " (every 3rd trade)")); - console.log(string.concat(" Expected stake calls: ~", vm.toString(expectedStakes), " (", vm.toString(stakingBias), "% of actions)")); - console.log(string.concat(" Expected exit calls: ~", vm.toString(expectedStakeActions - expectedStakes), " (", vm.toString(100 - stakingBias), "% of actions)")); - console.log(" Note: Actual stakes limited by available KRAIKEN balance"); + // Staking actions configured } function _executeBuy(address buyer, uint256 amount) internal virtual { @@ -323,7 +323,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { vm.prank(seller); harberg.transfer(address(swapExecutor), amount); - try swapExecutor.executeSell(amount, seller) {} catch {} + swapExecutor.executeSell(amount, seller); // No try-catch, let errors bubble up } function _getOptimizerByClass(string memory class) internal returns (address) { @@ -407,12 +407,26 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { if (harbBalance > minStakeAmount) { - // Stake between 5% and 20% of balance for more granular positions - uint256 amount = harbBalance * (5 + (rand % 16)) / 100; + // With high staking bias (>= 90%), stake VERY aggressively + uint256 minPct = stakingBias >= 100 ? 50 : (stakingBias >= 90 ? 30 : 10); + uint256 maxPct = stakingBias >= 100 ? 100 : (stakingBias >= 90 ? 70 : 30); + + // Stake between minPct% and maxPct% of balance + uint256 amount = harbBalance * (minPct + (rand % (maxPct - minPct + 1))) / 100; if (amount < minStakeAmount) { amount = minStakeAmount; } + // With 100% staking bias, allow staking ALL tokens + // Otherwise keep a small reserve + if (stakingBias < 100) { + uint256 maxStake = harbBalance * 90 / 100; // Keep 10% for trading if not 100% bias + if (amount > maxStake) { + amount = maxStake; + } + } + // If stakingBias == 100, no limit - can stake entire balance + // Initial staking: use lower tax rates (0-15) to enable snatching later uint32 taxRate = uint32(rand % 16); // 0-15 instead of 0-29 @@ -431,7 +445,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { // Log pool status before attempting (currentPercentStaked is in 1e18, where 1e18 = 100%) if (currentPercentStaked > 95e16) { // > 95% - console.log(string.concat(" [POOL NEAR FULL] ", vm.toString(currentPercentStaked / 1e16), "% of authorized")); + // Pool near full } // First try to stake without snatching @@ -471,7 +485,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { } } catch { // Catch-all for non-string errors (likely ExceededAvailableStake) - console.log(string.concat(" Stake failed at ", vm.toString(currentPercentStaked / 1e16), "% - trying snatch")); + // Stake failed - trying snatch // Now try snatching with high tax rate uint32 snatchTaxRate = 28; @@ -604,4 +618,33 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager { } } } + + function _logCurrentPosition(int24 currentTick) internal view { + (, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); + (, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); + + if (currentTick >= anchorLower && currentTick <= anchorUpper) { + // In anchor position + } else if (currentTick >= discoveryLower && currentTick <= discoveryUpper) { + // In discovery position + } else { + // Outside all positions + } + } + + function _logInitialFloor() internal view { + // Removed to reduce logging + } + + function _logFinalState() internal view { + // Removed to reduce logging + uint160 sqrtPriceUpper = TickMath.getSqrtRatioAtTick(floorUpper); + + if (tick < floorLower) { + // All liquidity is in KRAIKEN + uint256 kraikenAmount = token0isWeth ? + LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity) : + LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity); + // All liquidity in KRAIKEN + } } \ No newline at end of file diff --git a/onchain/analysis/helpers/SwapExecutor.sol b/onchain/analysis/helpers/SwapExecutor.sol index f818821..fd469c7 100644 --- a/onchain/analysis/helpers/SwapExecutor.sol +++ b/onchain/analysis/helpers/SwapExecutor.sol @@ -5,6 +5,8 @@ import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Po import {IWETH9} from "../../src/interfaces/IWETH9.sol"; import {Kraiken} from "../../src/Kraiken.sol"; import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; +import {LiquidityBoundaryHelper} from "../../test/helpers/LiquidityBoundaryHelper.sol"; +import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol"; /** * @title SwapExecutor @@ -16,15 +18,26 @@ contract SwapExecutor { IWETH9 public weth; Kraiken public harberg; bool public token0isWeth; + ThreePositionStrategy public liquidityManager; - constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) { + constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth, ThreePositionStrategy _liquidityManager) { pool = _pool; weth = _weth; harberg = _harberg; token0isWeth = _token0isWeth; + liquidityManager = _liquidityManager; } function executeBuy(uint256 amount, address recipient) external { + // Calculate maximum safe buy amount based on liquidity + uint256 maxBuyAmount = LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth); + + // Cap the amount to the safe limit + uint256 safeAmount = amount > maxBuyAmount ? maxBuyAmount : amount; + + // Skip if no liquidity available + if (safeAmount == 0) return; + // For buying HARB with WETH, we're swapping in the direction that increases HARB price // zeroForOne = true if WETH is token0, false if WETH is token1 bool zeroForOne = token0isWeth; @@ -39,18 +52,25 @@ contract SwapExecutor { sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1; } - try pool.swap( + pool.swap( recipient, zeroForOne, - int256(amount), + int256(safeAmount), sqrtPriceLimitX96, "" - ) {} catch { - // Swap failed, likely due to extreme price - ignore - } + ); } function executeSell(uint256 amount, address recipient) external { + // Calculate maximum safe sell amount based on liquidity + uint256 maxSellAmount = LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth); + + // Cap the amount to the safe limit + uint256 safeAmount = amount > maxSellAmount ? maxSellAmount : amount; + + // Skip if no liquidity available + if (safeAmount == 0) return; + // For selling HARB for WETH, we're swapping in the direction that decreases HARB price // zeroForOne = false if WETH is token0, true if WETH is token1 bool zeroForOne = !token0isWeth; @@ -65,15 +85,13 @@ contract SwapExecutor { sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1; } - try pool.swap( + pool.swap( recipient, zeroForOne, - int256(amount), + int256(safeAmount), sqrtPriceLimitX96, "" - ) {} catch { - // Swap failed, likely due to extreme price - ignore - } + ); } // Callback required for Uniswap V3 swaps diff --git a/onchain/analysis/replay-scenario.sh b/onchain/analysis/replay-scenario.sh index ed4af84..22cfc7d 100755 --- a/onchain/analysis/replay-scenario.sh +++ b/onchain/analysis/replay-scenario.sh @@ -154,7 +154,7 @@ contract Replay_CONTRACT_ID_Seed_SEED is Test { function _executeBuy(address buyer, uint256 amount) internal { if (weth.balanceOf(buyer) < amount) return; - SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth); + SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); vm.prank(buyer); weth.transfer(address(executor), amount); try executor.executeBuy(amount, buyer) {} catch {} @@ -165,7 +165,7 @@ contract Replay_CONTRACT_ID_Seed_SEED is Test { amount = kraiken.balanceOf(seller); if (amount == 0) return; } - SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth); + SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); vm.prank(seller); kraiken.transfer(address(executor), amount); try executor.executeSell(amount, seller) {} catch {} diff --git a/onchain/test/ReplayProfitableScenario.t.sol b/onchain/test/ReplayProfitableScenario.t.sol index 4631509..a2890f5 100644 --- a/onchain/test/ReplayProfitableScenario.t.sol +++ b/onchain/test/ReplayProfitableScenario.t.sol @@ -205,7 +205,7 @@ contract ReplayProfitableScenario is Test { return; } - SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth); + SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); vm.prank(buyer); weth.transfer(address(executor), amount); @@ -221,7 +221,7 @@ contract ReplayProfitableScenario is Test { if (amount == 0) return; } - SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth); + SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); vm.prank(seller); kraiken.transfer(address(executor), amount);