329 lines
No EOL
14 KiB
Solidity
329 lines
No EOL
14 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";
|
|
|
|
/**
|
|
* @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;
|
|
|
|
// 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 = 0; seed < 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 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++;
|
|
|
|
// Check profitability
|
|
if (finalBalance > initialBalance) {
|
|
profitableScenarios++;
|
|
marketProfitable++;
|
|
|
|
uint256 profit = finalBalance - initialBalance;
|
|
uint256 profitPercentage = (profit * 100) / initialBalance;
|
|
|
|
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitPercentage), "%"));
|
|
|
|
// Add to CSV
|
|
profitableCSV = string.concat(
|
|
profitableCSV,
|
|
optimizerClass, ",",
|
|
vm.toString(seed), ",",
|
|
vm.toString(initialBalance), ",",
|
|
vm.toString(finalBalance), ",",
|
|
vm.toString(profit), ",",
|
|
vm.toString(profitPercentage), "\n"
|
|
);
|
|
profitableCount++;
|
|
}
|
|
}
|
|
|
|
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)));
|
|
|
|
// 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));
|
|
}
|
|
|
|
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 < 40) { // 40% 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);
|
|
}
|
|
} else if (action < 80) { // 40% 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);
|
|
}
|
|
} else if (action < 95) { // 15% 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 {}
|
|
} else { // 5% chance wait
|
|
vm.warp(block.timestamp + 1 minutes + (rand % 2 hours));
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 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) {
|
|
string memory positionFilename = string.concat(
|
|
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
|
|
);
|
|
writeCSVToFile(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();
|
|
|
|
// 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);
|
|
|
|
// Create position data row
|
|
string memory row = string.concat(
|
|
label, ",",
|
|
vm.toString(currentTick), ",",
|
|
vm.toString(floorLiq), ",",
|
|
vm.toString(floorLower), ",",
|
|
vm.toString(floorUpper), ",",
|
|
vm.toString(anchorLiq), ",",
|
|
vm.toString(anchorLower), ",",
|
|
vm.toString(anchorUpper), ",",
|
|
vm.toString(discoveryLiq), ",",
|
|
vm.toString(discoveryLower), ",",
|
|
vm.toString(discoveryUpper), ",",
|
|
vm.toString(weth.balanceOf(address(lm))), ",",
|
|
vm.toString(harberg.balanceOf(address(lm)))
|
|
);
|
|
appendCSVRow(row);
|
|
}
|
|
} |