decoupled analysis from tests

This commit is contained in:
johba 2025-07-25 20:27:27 +02:00
parent 1dad2fb12a
commit 5b376885fd

View file

@ -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));
}
}
}
}