// 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 "../test/mocks/BullMarketOptimizer.sol"; import "../test/mocks/NeutralMarketOptimizer.sol"; import "../test/mocks/BearMarketOptimizer.sol"; import "./CSVManager.sol"; 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; // Test environment BullMarketOptimizer bullOptimizer; NeutralMarketOptimizer neutralOptimizer; BearMarketOptimizer bearOptimizer; // CSV tracking for profitable scenarios string[] profitableScenarioNames; string[] profitableScenarioData; function run() public { console.log("Starting LiquidityManager Analysis..."); console.log("Testing 30 trades across 3 market conditions\n"); // 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("=== TESTING ", scenarioNames[i], " ===")); // Setup optimizer for this scenario address optimizerAddress = _getOrCreateOptimizer(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!"); } else { console.log("No profitable trades found"); } console.log(""); } 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)); } } } function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) { if (scenarioIndex == 0) { if (address(bullOptimizer) == address(0)) { bullOptimizer = new BullMarketOptimizer(); } return address(bullOptimizer); } else if (scenarioIndex == 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 _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) { uint256 initialBalance = weth.balanceOf(account); console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH")); // Initialize CSV for this scenario initializeTimeSeriesCSV(); // 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; if (isBuy) { uint256 amount = 1 ether + (seed % 10 ether); if (weth.balanceOf(account) >= amount) { _executeBuy(amount); _recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0); } } else { 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"); } } } // Sell all remaining HARB uint256 finalHarb = harberg.balanceOf(account); if (finalHarb > 0) { _executeSell(finalHarb); _recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb); } // 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 { console.log("Final recenter failed: unknown error"); } uint256 finalBalance = weth.balanceOf(account); console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH")); scenariosAnalyzed++; if (finalBalance > initialBalance) { 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(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH")); return false; } } function _executeBuy(uint256 amount) internal { console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth")); // Create a separate contract to handle the swap SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); // Transfer WETH to executor vm.prank(account); weth.transfer(address(executor), amount); // 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) ); appendCSVRow(row); } 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 { // 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; } 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)); } } } }