354 lines
No EOL
15 KiB
Solidity
354 lines
No EOL
15 KiB
Solidity
// 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 "../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;
|
|
|
|
uint256 public scenariosAnalyzed;
|
|
uint256 public profitableScenarios;
|
|
|
|
// Market condition optimizers for sentiment analysis
|
|
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
|
|
}
|
|
|
|
/// @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");
|
|
|
|
// Initialize custom setup to skip position validation
|
|
_initializeForFuzzing();
|
|
|
|
// Initialize CSV for potential profitable scenario logging
|
|
initializePositionsCSV();
|
|
|
|
// 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], " ==="));
|
|
|
|
// Setup optimizer for this scenario
|
|
_setupScenarioOptimizer(i);
|
|
|
|
// Run fuzzing loop for this scenario
|
|
bool foundProfit = _runFuzzingLoop(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;
|
|
} else {
|
|
console.log("No profitable scenarios found in this configuration.");
|
|
}
|
|
}
|
|
|
|
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.");
|
|
}
|
|
|
|
/// @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)) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// @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();
|
|
|
|
uint256 initialBalance = weth.balanceOf(account);
|
|
console.log("Starting balance:", initialBalance);
|
|
|
|
// Generate seed for this scenario's randomness
|
|
uint256 scenarioSeed = uint256(keccak256(abi.encodePacked(scenarioName, block.timestamp)));
|
|
|
|
// Execute 30 fuzzing actions
|
|
for (uint256 action = 0; action < 30; action++) {
|
|
uint256 actionSeed = uint256(keccak256(abi.encodePacked(scenarioSeed, action)));
|
|
|
|
// 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);
|
|
}
|
|
} 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Final recenter
|
|
try lm.recenter() returns (bool /* isUp */) {
|
|
_capturePositionData("final_recenter");
|
|
console.log("Final recenter completed");
|
|
} catch {
|
|
_capturePositionData("final_recenter_failed");
|
|
console.log("Final recenter failed");
|
|
}
|
|
|
|
// Check if scenario was profitable
|
|
uint256 finalBalance = weth.balanceOf(account);
|
|
console.log("Final balance:", finalBalance);
|
|
|
|
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++;
|
|
profitableScenarios++;
|
|
return true;
|
|
} else {
|
|
console.log("Loss:", initialBalance - finalBalance, "wei");
|
|
scenariosAnalyzed++;
|
|
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();
|
|
|
|
// 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)
|
|
));
|
|
|
|
string memory part2 = string(abi.encodePacked(
|
|
",", vm.toString(liquidityResponse.anchorTickLower), ",", vm.toString(liquidityResponse.anchorTickUpper), ",",
|
|
vm.toString(liquidityResponse.ethAnchor), ",", vm.toString(liquidityResponse.harbergAnchor)
|
|
));
|
|
|
|
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"
|
|
));
|
|
|
|
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);
|
|
} else {
|
|
// Current price is within the position range - position contains both tokens
|
|
token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
|
|
token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
|
|
}
|
|
}
|
|
|
|
/// @notice Get analysis statistics
|
|
function getStats() public view returns (uint256 total, uint256 profitable) {
|
|
return (scenariosAnalyzed, profitableScenarios);
|
|
}
|
|
} |