384 lines
No EOL
18 KiB
Solidity
384 lines
No EOL
18 KiB
Solidity
// 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 {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
|
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
|
|
import "../test/mocks/BullMarketOptimizer.sol";
|
|
import "../test/mocks/NeutralMarketOptimizer.sol";
|
|
import "../test/mocks/BearMarketOptimizer.sol";
|
|
import "../test/mocks/WhaleOptimizer.sol";
|
|
import "../test/mocks/MockOptimizer.sol";
|
|
import "../test/mocks/RandomScenarioOptimizer.sol";
|
|
import "./helpers/CSVManager.sol";
|
|
import "./helpers/SwapExecutor.sol";
|
|
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
|
|
|
/**
|
|
* @title FuzzingAnalysis
|
|
* @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager
|
|
* @dev Configurable via environment variables:
|
|
* - FUZZING_RUNS: Number of fuzzing iterations per market (default 100)
|
|
* - TRACK_POSITIONS: Track detailed position data (default false)
|
|
*/
|
|
contract FuzzingAnalysis 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");
|
|
|
|
// Analysis metrics
|
|
uint256 public scenariosAnalyzed;
|
|
uint256 public profitableScenarios;
|
|
|
|
// Configuration
|
|
uint256 public fuzzingRuns;
|
|
bool public trackPositions;
|
|
string public optimizerClass;
|
|
uint256 public tradesPerRun;
|
|
uint256 public seedOffset;
|
|
|
|
// Optimizers
|
|
BullMarketOptimizer bullOptimizer;
|
|
NeutralMarketOptimizer neutralOptimizer;
|
|
BearMarketOptimizer bearOptimizer;
|
|
WhaleOptimizer whaleOptimizer;
|
|
MockOptimizer mockOptimizer;
|
|
RandomScenarioOptimizer randomOptimizer;
|
|
|
|
function run() public {
|
|
_loadConfiguration();
|
|
|
|
console.log("=== Fuzzing Analysis ===");
|
|
console.log(string.concat("Optimizer: ", optimizerClass));
|
|
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
|
|
console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun)));
|
|
console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled"));
|
|
console.log("");
|
|
|
|
testEnv = new TestEnvironment(feeDestination);
|
|
|
|
// Get optimizer based on class name
|
|
address optimizerAddress = _getOptimizerByClass(optimizerClass);
|
|
|
|
// Initialize CSV for profitable scenarios
|
|
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n";
|
|
uint256 profitableCount;
|
|
uint256 marketProfitable = 0;
|
|
|
|
console.log(string.concat("=== FUZZING with ", optimizerClass, " ==="));
|
|
|
|
for (uint256 seed = seedOffset; seed < seedOffset + fuzzingRuns; seed++) {
|
|
if (seed % 10 == 0 && seed > 0) {
|
|
console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns)));
|
|
}
|
|
|
|
// Create fresh environment for each run
|
|
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
|
testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress);
|
|
|
|
// Fund LiquidityManager with initial ETH
|
|
vm.deal(address(lm), 50 ether);
|
|
|
|
// Fund account with random amount (10-50 ETH)
|
|
uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether);
|
|
vm.deal(account, fundAmount * 2);
|
|
vm.prank(account);
|
|
weth.deposit{value: fundAmount}();
|
|
|
|
uint256 initialBalance = weth.balanceOf(account);
|
|
|
|
// Initial recenter
|
|
vm.warp(block.timestamp + 5 hours);
|
|
vm.prank(feeDestination);
|
|
try lm.recenter() {} catch {}
|
|
|
|
// Run trading scenario
|
|
uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed);
|
|
|
|
scenariosAnalyzed++;
|
|
|
|
// Calculate profit/loss
|
|
bool isProfitable = finalBalance > initialBalance;
|
|
uint256 profitOrLoss;
|
|
uint256 profitOrLossPercentage;
|
|
|
|
if (isProfitable) {
|
|
profitOrLoss = finalBalance - initialBalance;
|
|
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
|
|
profitableScenarios++;
|
|
marketProfitable++;
|
|
|
|
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitOrLossPercentage), "%"));
|
|
|
|
// Add to CSV
|
|
profitableCSV = string.concat(
|
|
profitableCSV,
|
|
optimizerClass, ",",
|
|
vm.toString(seed), ",",
|
|
vm.toString(initialBalance), ",",
|
|
vm.toString(finalBalance), ",",
|
|
vm.toString(profitOrLoss), ",",
|
|
vm.toString(profitOrLossPercentage), "\n"
|
|
);
|
|
profitableCount++;
|
|
} else {
|
|
profitOrLoss = initialBalance - finalBalance;
|
|
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
|
|
}
|
|
|
|
// Always log result for cumulative tracking
|
|
console.log(string.concat("RESULT|SEED:", vm.toString(seed), "|INITIAL:", vm.toString(initialBalance), "|FINAL:", vm.toString(finalBalance), "|PNL:", isProfitable ? "+" : "-", vm.toString(profitOrLoss)));
|
|
}
|
|
|
|
console.log(string.concat("\nResults for ", optimizerClass, ":"));
|
|
console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns)));
|
|
console.log("");
|
|
|
|
console.log("=== ANALYSIS COMPLETE ===");
|
|
console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
|
|
console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios)));
|
|
console.log(string.concat("Profitable rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
|
|
|
|
// Write profitable scenarios CSV if any found
|
|
if (profitableCount > 0) {
|
|
console.log("Writing profitable scenarios CSV...");
|
|
string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv");
|
|
vm.writeFile(filename, profitableCSV);
|
|
console.log(string.concat("\nProfitable scenarios written to: ", filename));
|
|
} else {
|
|
console.log("\nNo profitable scenarios found.");
|
|
}
|
|
|
|
console.log("Script execution complete.");
|
|
}
|
|
|
|
function _loadConfiguration() internal {
|
|
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100));
|
|
trackPositions = vm.envOr("TRACK_POSITIONS", false);
|
|
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
|
|
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20));
|
|
seedOffset = vm.envOr("SEED_OFFSET", uint256(0));
|
|
}
|
|
|
|
function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) {
|
|
// Initialize position tracking CSV if enabled
|
|
if (trackPositions) {
|
|
initializePositionsCSV();
|
|
_recordPositionData("Initial");
|
|
}
|
|
|
|
// Use seed for randomness
|
|
uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp)));
|
|
|
|
// Use configured number of trades (with some randomness)
|
|
uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades
|
|
if (numTrades < 5) numTrades = 5; // Minimum 5 trades
|
|
|
|
// Initial buy if no HARB
|
|
if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) {
|
|
uint256 initialBuy = weth.balanceOf(account) / 10;
|
|
_executeBuy(initialBuy);
|
|
}
|
|
|
|
// Execute random trades
|
|
for (uint256 i = 0; i < numTrades; i++) {
|
|
rand = uint256(keccak256(abi.encodePacked(rand, i)));
|
|
uint256 action = rand % 100;
|
|
|
|
if (action < 25) { // 25% chance buy
|
|
uint256 wethBal = weth.balanceOf(account);
|
|
if (wethBal > 0) {
|
|
uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100%
|
|
uint256 buyAmount = (wethBal * buyPercent) / 1000;
|
|
if (buyAmount > 0) {
|
|
_executeBuy(buyAmount);
|
|
if (trackPositions) {
|
|
_recordPositionData(string.concat("Buy_", vm.toString(i)));
|
|
}
|
|
}
|
|
}
|
|
} else if (action < 50) { // 25% chance sell
|
|
uint256 harbBal = harberg.balanceOf(account);
|
|
if (harbBal > 0) {
|
|
uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100%
|
|
uint256 sellAmount = (harbBal * sellPercent) / 1000;
|
|
if (sellAmount > 0) {
|
|
_executeSell(sellAmount);
|
|
if (trackPositions) {
|
|
_recordPositionData(string.concat("Sell_", vm.toString(i)));
|
|
}
|
|
}
|
|
}
|
|
} else { // 50% chance recenter
|
|
uint256 waitTime = 1 minutes + (rand % 10 hours);
|
|
vm.warp(block.timestamp + waitTime);
|
|
vm.prank(feeDestination);
|
|
try lm.recenter() {
|
|
if (trackPositions) {
|
|
_recordPositionData(string.concat("Recenter_", vm.toString(i)));
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// Skip trades at extreme ticks
|
|
(, int24 currentTick, , , , , ) = pool.slot0();
|
|
if (currentTick < -887000 || currentTick > 887000) continue;
|
|
}
|
|
|
|
// Sell remaining HARB
|
|
uint256 finalHarb = harberg.balanceOf(account);
|
|
if (finalHarb > 0) {
|
|
_executeSell(finalHarb);
|
|
if (trackPositions) {
|
|
_recordPositionData("Final_Sell");
|
|
}
|
|
}
|
|
|
|
// Final recenters
|
|
for (uint256 j = 0; j < 1 + (rand % 3); j++) {
|
|
vm.warp(block.timestamp + 5 hours);
|
|
vm.prank(feeDestination);
|
|
try lm.recenter() {} catch {}
|
|
}
|
|
|
|
// Write position tracking CSV if enabled
|
|
if (trackPositions) {
|
|
_recordPositionData("Final");
|
|
string memory positionFilename = string.concat(
|
|
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
|
|
);
|
|
writeCSVToFile(positionFilename);
|
|
console.log(string.concat("Position tracking CSV written to: ", positionFilename));
|
|
}
|
|
|
|
return weth.balanceOf(account);
|
|
}
|
|
|
|
function _executeBuy(uint256 amount) internal {
|
|
if (amount == 0 || weth.balanceOf(account) < amount) return;
|
|
|
|
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
|
vm.prank(account);
|
|
weth.transfer(address(executor), amount);
|
|
|
|
try executor.executeBuy(amount, account) {} catch {}
|
|
}
|
|
|
|
function _executeSell(uint256 amount) internal {
|
|
if (amount == 0 || harberg.balanceOf(account) < amount) return;
|
|
|
|
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
|
vm.prank(account);
|
|
harberg.transfer(address(executor), amount);
|
|
|
|
try executor.executeSell(amount, account) {} catch {}
|
|
}
|
|
|
|
function _getOrCreateOptimizer(uint256 index) internal returns (address) {
|
|
if (index == 0) {
|
|
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
|
|
return address(bullOptimizer);
|
|
} else if (index == 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 _getOptimizerByClass(string memory className) internal returns (address) {
|
|
bytes32 classHash = keccak256(abi.encodePacked(className));
|
|
|
|
if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) {
|
|
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
|
|
return address(bullOptimizer);
|
|
} else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) {
|
|
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
|
|
return address(neutralOptimizer);
|
|
} else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) {
|
|
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
|
|
return address(bearOptimizer);
|
|
} else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) {
|
|
if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer();
|
|
return address(whaleOptimizer);
|
|
} else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) {
|
|
if (address(mockOptimizer) == address(0)) {
|
|
mockOptimizer = new MockOptimizer();
|
|
mockOptimizer.initialize(address(harberg), address(stake));
|
|
}
|
|
return address(mockOptimizer);
|
|
} else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) {
|
|
if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer();
|
|
return address(randomOptimizer);
|
|
} else {
|
|
revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer"));
|
|
}
|
|
}
|
|
|
|
function _recordPositionData(string memory label) internal {
|
|
(,int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
// Cap currentTick to avoid overflow in extreme cases
|
|
if (currentTick > 887000) currentTick = 887000;
|
|
if (currentTick < -887000) currentTick = -887000;
|
|
|
|
// Get each position
|
|
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
|
|
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
|
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
|
|
|
// Debug: Log liquidity values
|
|
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
|
|
console.log("=== LIQUIDITY VALUES ===");
|
|
console.log("Label:", label);
|
|
console.log("Current tick:", uint256(int256(currentTick)));
|
|
console.log("Anchor range:", uint256(int256(anchorLower)), "-", uint256(int256(anchorUpper)));
|
|
console.log("Anchor liquidity:", uint256(anchorLiq));
|
|
console.log("Discovery range:", uint256(int256(discoveryLower)), "-", uint256(int256(discoveryUpper)));
|
|
console.log("Discovery liquidity:", uint256(discoveryLiq));
|
|
if (uint256(anchorLiq) > 0) {
|
|
console.log("Discovery/Anchor liquidity ratio:", uint256(discoveryLiq) * 100 / uint256(anchorLiq), "%");
|
|
console.log("Anchor width:", uint256(int256(anchorUpper - anchorLower)), "ticks");
|
|
console.log("Discovery width:", uint256(int256(discoveryUpper - discoveryLower)), "ticks");
|
|
uint256 anchorLiqPerTick = uint256(anchorLiq) * 1000 / uint256(int256(anchorUpper - anchorLower));
|
|
uint256 discoveryLiqPerTick = uint256(discoveryLiq) * 1000 / uint256(int256(discoveryUpper - discoveryLower));
|
|
console.log("Anchor liquidity per tick (x1000):", anchorLiqPerTick);
|
|
console.log("Discovery liquidity per tick (x1000):", discoveryLiqPerTick);
|
|
console.log("Discovery/Anchor per tick ratio:", discoveryLiqPerTick * 100 / anchorLiqPerTick, "%");
|
|
}
|
|
}
|
|
|
|
// Create position data row with liquidity values directly
|
|
string memory row = string.concat(
|
|
label, ", ",
|
|
vm.toString(currentTick), ", ",
|
|
vm.toString(floorLower), ", ",
|
|
vm.toString(floorUpper), ", ",
|
|
vm.toString(uint256(floorLiq)), ", ",
|
|
vm.toString(anchorLower), ", ",
|
|
vm.toString(anchorUpper), ", ",
|
|
vm.toString(uint256(anchorLiq)), ", ",
|
|
vm.toString(discoveryLower), ", ",
|
|
vm.toString(discoveryUpper), ", ",
|
|
vm.toString(uint256(discoveryLiq)), ", ",
|
|
token0isWeth ? "true" : "false"
|
|
);
|
|
appendCSVRow(row);
|
|
}
|
|
} |