diff --git a/onchain/analysis/SimpleAnalysis.s.sol b/onchain/analysis/SimpleAnalysis.s.sol index 9be6bcb..64d392a 100644 --- a/onchain/analysis/SimpleAnalysis.s.sol +++ b/onchain/analysis/SimpleAnalysis.s.sol @@ -1,93 +1,105 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -/** - * @title Simple Scenario Analysis for LiquidityManager - * @notice Lightweight analysis script for researching profitable trading scenarios - * @dev Separated from unit tests to focus on research and scenario discovery - * Uses the modular LiquidityManager architecture for analysis - * Run with: forge script analysis/SimpleAnalysis.s.sol --ffi - */ - -import "../test/LiquidityManager.t.sol"; +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 "../test/mocks/BullMarketOptimizer.sol"; import "../test/mocks/NeutralMarketOptimizer.sol"; import "../test/mocks/BearMarketOptimizer.sol"; import "./CSVManager.sol"; -import "../src/helpers/UniswapHelpers.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import "@aperture/uni-v3-lib/TickMath.sol"; -import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; -import {WETH} from "solmate/tokens/WETH.sol"; -contract SimpleAnalysis is LiquidityManagerTest, CSVManager { - using UniswapHelpers for IUniswapV3Pool; +contract SimpleAnalysis 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"); uint256 public scenariosAnalyzed; uint256 public profitableScenarios; - // Market condition optimizers for sentiment analysis + // Test environment BullMarketOptimizer bullOptimizer; NeutralMarketOptimizer neutralOptimizer; BearMarketOptimizer bearOptimizer; - /// @notice Initialize setup for fuzzing without position validation - function _initializeForFuzzing() internal { - _skipSetup(); - setUpCustomToken0(false); // WETH as token1 - // Note: recenter access will be granted in _createFreshEnvironment - } + // CSV tracking for profitable scenarios + string[] profitableScenarioNames; + string[] profitableScenarioData; - /// @notice Entry point for forge script execution function run() public { - console.log("Starting LiquidityManager Fuzzing Analysis..."); - console.log("Testing 30-action sequences across 3 market conditions for profitable scenarios"); + console.log("Starting LiquidityManager Analysis..."); + console.log("Testing 30 trades across 3 market conditions\n"); - // Initialize custom setup to skip position validation - _initializeForFuzzing(); - - // Initialize CSV for potential profitable scenario logging - initializePositionsCSV(); + // Initialize test environment + testEnv = new TestEnvironment(feeDestination); // Test 3 different market sentiment optimizers string[3] memory scenarioNames = ["Bull Market", "Neutral Market", "Bear Market"]; for (uint256 i = 0; i < 3; i++) { - console.log(string.concat("\\n=== TESTING ", scenarioNames[i], " ===")); + console.log(string.concat("=== TESTING ", scenarioNames[i], " ===")); // Setup optimizer for this scenario - _setupScenarioOptimizer(i); + address optimizerAddress = _getOrCreateOptimizer(i); - // Run fuzzing loop for this scenario - bool foundProfit = _runFuzzingLoop(scenarioNames[i]); + // Create fresh environment + (factory, pool, weth, harberg, stake, lm,, token0isWeth) = + testEnv.setupEnvironmentWithOptimizer(false, feeDestination, optimizerAddress); + + // Fund account + vm.deal(account, 500 ether); + vm.prank(account); + weth.deposit{value: 200 ether}(); + + // Initial recenter + vm.warp(block.timestamp + 5 hours); + vm.prank(feeDestination); + try lm.recenter() { + console.log("Initial recenter successful"); + } catch { + console.log("Initial recenter failed"); + } + + // Run trading scenario + bool foundProfit = _runTradingScenario(scenarioNames[i]); if (foundProfit) { - console.log("Profitable scenario found! CSV written to ./analysis/profitable_scenario.csv"); - console.log("Exiting analysis after finding profitable trade."); - return; + console.log("PROFITABLE scenario found!"); } else { - console.log("No profitable scenarios found in this configuration."); + console.log("No profitable trades found"); } + console.log(""); } - console.log("\\n=== ANALYSIS COMPLETE ==="); - console.log("No profitable scenarios found across all market conditions."); - console.log("This indicates effective anti-arbitrage protection is working."); + console.log("=== ANALYSIS COMPLETE ==="); + console.log(string.concat("Scenarios analyzed: ", vm.toString(scenariosAnalyzed))); + console.log(string.concat("Profitable scenarios: ", vm.toString(profitableScenarios))); + + // Write CSV files for profitable scenarios + if (profitableScenarios > 0) { + console.log("\nWriting CSV files for profitable scenarios..."); + for (uint256 i = 0; i < profitableScenarioNames.length; i++) { + string memory filename = string.concat("analysis/profitable_", profitableScenarioNames[i], ".csv"); + csv = profitableScenarioData[i]; + writeCSVToFile(filename); + console.log(string.concat("Wrote: ", filename)); + } + } } - /// @notice Setup complete fresh environment for specific scenario - function _setupScenarioOptimizer(uint256 scenarioIndex) internal { - address optimizerAddress = _getOrCreateOptimizer(scenarioIndex); - _logScenarioParameters(scenarioIndex); - _createFreshEnvironment(optimizerAddress); - } - - /// @notice Deploy a fresh Uniswap factory for isolated testing - function deployFreshFactory() internal returns (IUniswapV3Factory) { - return UniswapHelpers.deployUniswapFactory(); - } - - /// @notice Get or create optimizer for given scenario index function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) { if (scenarioIndex == 0) { if (address(bullOptimizer) == address(0)) { @@ -107,248 +119,215 @@ contract SimpleAnalysis is LiquidityManagerTest, CSVManager { } } - /// @notice Log scenario parameters for given index - function _logScenarioParameters(uint256 scenarioIndex) internal view { - if (scenarioIndex == 0) { - console.log("Bull Market: 20% cap inefficiency, 80% anchor share, 30 width, 90% discovery"); - } else if (scenarioIndex == 1) { - console.log("Neutral Market: 50% cap inefficiency, 50% anchor share, 50 width, 50% discovery"); - } else { - console.log("Bear Market: 80% cap inefficiency, 20% anchor share, 80 width, 20% discovery"); - } - } - - /// @notice Grant recenter access with proper permissions for analysis - function _grantAnalysisRecenterAccess() internal { - vm.prank(feeDestination); - lm.setRecenterAccess(address(this)); - } - - /// @notice Configure all contracts with proper permissions and funding - function _configureContracts() internal { - lm.setFeeDestination(feeDestination); - harberg.setStakingPool(address(stake)); - - vm.prank(feeDestination); - harberg.setLiquidityManager(address(lm)); - - vm.deal(address(lm), 50 ether); - _grantAnalysisRecenterAccess(); - } - - /// @notice Setup test account with ETH and WETH - function _setupTestAccount() internal { - vm.deal(account, 500 ether); - vm.prank(account); - weth.deposit{value: 200 ether}(); - } - - /// @notice Perform initial recenter to establish positions - function _performInitialRecenter() internal { - try lm.recenter() returns (bool /* isUp */) { - console.log("Initial recenter successful"); - } catch Error(string memory reason) { - console.log("Initial recenter failed:", reason); - // Continue anyway for fuzzing analysis - } - _capturePositionData("initial_setup"); - } - - /// @notice Create fresh contracts for each scenario to avoid AddressAlreadySet errors - function _createFreshEnvironment(address optimizerAddress) internal { - // Create new factory - factory = deployFreshFactory(); - - // Create new WETH - weth = IWETH9(address(new WETH())); - - // Create new Kraiken token - harberg = new Kraiken("KRAIKEN", "HARB"); - - // Determine token order - token0isWeth = address(weth) < address(harberg); - - // Create new pool - pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); - pool.initializePoolFor1Cent(token0isWeth); - - // Create new Stake contract - stake = new Stake(address(harberg), feeDestination); - - // Create new LiquidityManager with the specific optimizer - lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress); - - // Configure all contracts in batch - _configureContracts(); - } - - /// @notice Run 30-action fuzzing loop for a specific scenario - function _runFuzzingLoop(string memory scenarioName) internal returns (bool foundProfit) { - _setupTestAccount(); - _performInitialRecenter(); - + function _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) { uint256 initialBalance = weth.balanceOf(account); - console.log("Starting balance:", initialBalance); + console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH")); - // Generate seed for this scenario's randomness - uint256 scenarioSeed = uint256(keccak256(abi.encodePacked(scenarioName, block.timestamp))); + // Initialize CSV for this scenario + initializeTimeSeriesCSV(); - // Execute 30 fuzzing actions - for (uint256 action = 0; action < 30; action++) { - uint256 actionSeed = uint256(keccak256(abi.encodePacked(scenarioSeed, action))); + // Force initial buy to get some HARB + _executeBuy(10 ether); + _recordTradeToCSV(block.timestamp, "BUY", 10 ether, 0); + + // Execute 30 trades + for (uint256 i = 1; i < 30; i++) { + uint256 seed = uint256(keccak256(abi.encodePacked(scenarioName, i))); + bool isBuy = (seed % 2) == 0; - // Determine if buy or sell (50/50 chance) - bool isBuy = (actionSeed % 2) == 0; - - // Generate random amount (1-50 ETH range) - uint256 amount = 1 ether + (actionSeed % (50 ether)); - - // Execute trade if (isBuy) { - // Ensure we don't exceed available WETH balance - uint256 wethBalance = weth.balanceOf(account); - if (amount > wethBalance) { - amount = wethBalance / 2; // Use half of available balance - } - if (amount > 0) { - buy(amount); - _capturePositionData(string.concat("buy_", vm.toString(amount))); - console.log("Action", action + 1, ": Buy", amount); + uint256 amount = 1 ether + (seed % 10 ether); + if (weth.balanceOf(account) >= amount) { + _executeBuy(amount); + _recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0); } } else { - // Sell KRAIKEN tokens - uint256 harbergBalance = harberg.balanceOf(account); - if (harbergBalance > 0) { - uint256 sellAmount = (actionSeed % harbergBalance) + 1; - if (sellAmount > harbergBalance) sellAmount = harbergBalance; - sell(sellAmount); - _capturePositionData(string.concat("sell_", vm.toString(sellAmount))); - console.log("Action", action + 1, ": Sell", sellAmount); - } - } - - // 30% chance to recenter after each trade - if ((actionSeed % 100) < 30) { - try lm.recenter() returns (bool isUp) { - _capturePositionData("recenter"); - console.log("Action", action + 1, ": Recenter (price moved", isUp ? "UP)" : "DOWN)"); - } catch Error(string memory reason) { - // Recenter can fail due to amplitude requirements - this is normal - if (keccak256(bytes(reason)) != keccak256("amplitude not reached.")) { - console.log("Recenter failed:", reason); + uint256 harbBalance = harberg.balanceOf(account); + if (harbBalance > 0) { + uint256 sellAmount = harbBalance / 4; + if (sellAmount > 0) { + _executeSell(sellAmount); + _recordTradeToCSV(block.timestamp + i * 60, "SELL", 0, sellAmount); } } } + + // Try recenter occasionally + if (i % 3 == 0) { + vm.prank(feeDestination); + try lm.recenter() { + console.log(" Recenter successful"); + } catch Error(string memory reason) { + console.log(string.concat(" Recenter failed: ", reason)); + } catch { + console.log(" Recenter failed: cooldown or other error"); + } + } } - // Final cleanup - sell all remaining KRAIKEN to realize profit/loss - uint256 finalHarbBalance = harberg.balanceOf(account); - if (finalHarbBalance > 0) { - sell(finalHarbBalance); - _capturePositionData(string.concat("final_sell_", vm.toString(finalHarbBalance))); - console.log("Final sell of remaining KRAIKEN:", finalHarbBalance); + // Sell all remaining HARB + uint256 finalHarb = harberg.balanceOf(account); + if (finalHarb > 0) { + _executeSell(finalHarb); + _recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb); } - // Final recenter - try lm.recenter() returns (bool /* isUp */) { - _capturePositionData("final_recenter"); - console.log("Final recenter completed"); + // Final recenter after all trades + vm.warp(block.timestamp + 5 hours); + vm.prank(feeDestination); + try lm.recenter() { + console.log("Final recenter successful"); + } catch Error(string memory reason) { + console.log(string.concat("Final recenter failed: ", reason)); } catch { - _capturePositionData("final_recenter_failed"); - console.log("Final recenter failed"); + console.log("Final recenter failed: unknown error"); } - // Check if scenario was profitable uint256 finalBalance = weth.balanceOf(account); - console.log("Final balance:", finalBalance); + console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH")); + scenariosAnalyzed++; if (finalBalance > initialBalance) { - uint256 profit = finalBalance - initialBalance; - console.log("\\n[ALERT] PROFITABLE SCENARIO FOUND!"); - console.log("Scenario:", scenarioName); - console.log("Profit:", profit, "wei"); - console.log("Profit:", profit / 1e18, "ETH"); - - // Mark end of profitable scenario in CSV - _capturePositionData("PROFITABLE_SCENARIO_END"); - - // Write CSV file - writeCSVToFile("./analysis/profitable_scenario.csv"); - - scenariosAnalyzed++; + console.log(string.concat("PROFIT: ", vm.toString((finalBalance - initialBalance) / 1e18), " ETH")); profitableScenarios++; + + // Store profitable scenario data + profitableScenarioNames.push(scenarioName); + profitableScenarioData.push(csv); + return true; } else { - console.log("Loss:", initialBalance - finalBalance, "wei"); - scenariosAnalyzed++; + console.log(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH")); return false; } } - /// @notice Capture complete position data for CSV analysis - function _capturePositionData(string memory actionType) internal { - Response memory liquidityResponse = checkLiquidity("analysis"); - (, int24 currentTick,,,,,) = pool.slot0(); + function _executeBuy(uint256 amount) internal { + console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth")); - // Build CSV row in chunks to avoid stack too deep - string memory part1 = string(abi.encodePacked( - actionType, ",", - vm.toString(currentTick), ",", - vm.toString(liquidityResponse.floorTickLower), ",", vm.toString(liquidityResponse.floorTickUpper), ",", - vm.toString(liquidityResponse.ethFloor), ",", vm.toString(liquidityResponse.harbergFloor) - )); + // Create a separate contract to handle the swap + SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); - string memory part2 = string(abi.encodePacked( - ",", vm.toString(liquidityResponse.anchorTickLower), ",", vm.toString(liquidityResponse.anchorTickUpper), ",", - vm.toString(liquidityResponse.ethAnchor), ",", vm.toString(liquidityResponse.harbergAnchor) - )); + // Transfer WETH to executor + vm.prank(account); + weth.transfer(address(executor), amount); - string memory part3 = string(abi.encodePacked( - ",", vm.toString(liquidityResponse.discoveryTickLower), ",", vm.toString(liquidityResponse.discoveryTickUpper), ",", - vm.toString(liquidityResponse.ethDiscovery), ",", vm.toString(liquidityResponse.harbergDiscovery), ",", - token0isWeth ? "true" : "false" - )); + // Execute the swap + try executor.executeBuy(amount, account) { + console.log(" Buy successful"); + } catch Error(string memory reason) { + console.log(string.concat(" Buy failed: ", reason)); + } catch { + console.log(" Buy failed: unknown error"); + } + } + + function _executeSell(uint256 amount) internal { + console.log(string.concat(" Sell ", vm.toString(amount / 1e18), " HARB")); + + // Create a separate contract to handle the swap + SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); + + // Transfer HARB to executor + vm.prank(account); + harberg.transfer(address(executor), amount); + + // Execute the swap + try executor.executeSell(amount, account) { + console.log(" Sell successful"); + } catch Error(string memory reason) { + console.log(string.concat(" Sell failed: ", reason)); + } catch { + console.log(" Sell failed: unknown error"); + } + } + + function _recordTradeToCSV(uint256 timestamp, string memory action, uint256 ethAmount, uint256 harbAmount) internal { + // Get current price + (uint160 sqrtPriceX96,,,,,, ) = pool.slot0(); + uint256 price = _sqrtPriceToPrice(sqrtPriceX96); + + // Get supply data + uint256 totalSupply = harberg.totalSupply(); + uint256 stakeShares = stake.outstandingStake(); + uint256 avgTaxRate = stake.getAverageTaxRate(); + + // Create CSV row + string memory row = string.concat( + vm.toString(timestamp), ",", + vm.toString(price), ",", + vm.toString(totalSupply), ",", + action, ",", + vm.toString(ethAmount), ",", + vm.toString(harbAmount), ",", + vm.toString(stakeShares), ",", + vm.toString(avgTaxRate) + ); - string memory row = string(abi.encodePacked(part1, part2, part3)); appendCSVRow(row); } - /// @notice Get actual token amounts from pool position data using proper Uniswap V3 math - function _getPositionTokenAmounts( - uint128 liquidity, - int24 tickLower, - int24 tickUpper - ) internal view returns (uint256 token0Amount, uint256 token1Amount) { - if (liquidity == 0) { - return (0, 0); - } - - // Get current price from pool - (, int24 currentTick,,,,,) = pool.slot0(); - - // Calculate sqrt prices for the position bounds and current price - uint160 sqrtPriceAX96 = TickMath.getSqrtRatioAtTick(tickLower); - uint160 sqrtPriceBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick); - - // Use LiquidityAmounts library for proper Uniswap V3 calculations - if (currentTick < tickLower) { - // Current price is below the position range - position only contains token0 - token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity); - token1Amount = 0; - } else if (currentTick >= tickUpper) { - // Current price is above the position range - position only contains token1 - token0Amount = 0; - token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity); + function _sqrtPriceToPrice(uint160 sqrtPriceX96) internal view returns (uint256) { + if (token0isWeth) { + // price = (sqrtPrice / 2^96)^2 * 10^18 + return (uint256(sqrtPriceX96) * uint256(sqrtPriceX96) * 1e18) >> 192; } else { - // Current price is within the position range - position contains both tokens - token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity); - token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity); + // price = 1 / ((sqrtPrice / 2^96)^2) * 10^18 + return (1e18 << 192) / (uint256(sqrtPriceX96) * uint256(sqrtPriceX96)); } } +} + +// Helper contract to execute swaps without address(this) issues +contract SwapExecutor { + IUniswapV3Pool public pool; + IWETH9 public weth; + Kraiken public harberg; + bool public token0isWeth; + + constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) { + pool = _pool; + weth = _weth; + harberg = _harberg; + token0isWeth = _token0isWeth; + } - /// @notice Get analysis statistics - function getStats() public view returns (uint256 total, uint256 profitable) { - return (scenariosAnalyzed, profitableScenarios); + function executeBuy(uint256 amount, address recipient) external { + pool.swap( + recipient, + token0isWeth, + int256(amount), + token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341, + "" + ); + } + + function executeSell(uint256 amount, address recipient) external { + pool.swap( + recipient, + !token0isWeth, + int256(amount), + !token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341, + "" + ); + } + + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { + require(msg.sender == address(pool), "Invalid caller"); + + if (amount0Delta > 0) { + if (token0isWeth) { + weth.transfer(msg.sender, uint256(amount0Delta)); + } else { + harberg.transfer(msg.sender, uint256(amount0Delta)); + } + } + if (amount1Delta > 0) { + if (token0isWeth) { + harberg.transfer(msg.sender, uint256(amount1Delta)); + } else { + weth.transfer(msg.sender, uint256(amount1Delta)); + } + } } } \ No newline at end of file