- Implement dynamic discovery depth based on anchor position share - Add configurable discovery_max_multiple (1.5-4x) for flexible adjustment - Update BullMarketOptimizer with new depth calculation logic - Fix scenario visualizer floor position visibility - Add comprehensive tests for discovery depth behavior The discovery position now dynamically adjusts its depth based on the anchor position's share of total liquidity, allowing for more effective price discovery while maintaining protection against manipulation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
498 lines
No EOL
24 KiB
Solidity
498 lines
No EOL
24 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);
|
|
|
|
// Calculate ETH and HARB amounts in each position using proper Uniswap math
|
|
uint256 floorEth = 0;
|
|
uint256 floorHarb = 0;
|
|
uint256 anchorEth = 0;
|
|
uint256 anchorHarb = 0;
|
|
uint256 discoveryEth = 0;
|
|
uint256 discoveryHarb = 0;
|
|
|
|
// 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, "%");
|
|
}
|
|
}
|
|
|
|
// Calculate amounts for each position using LiquidityAmounts library
|
|
if (floorLiq > 0) {
|
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorLower);
|
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorUpper);
|
|
|
|
// Calculate actual deposited amounts based on position relative to current price
|
|
if (token0isWeth) {
|
|
if (currentTick < floorLower) {
|
|
// Position is above current price - contains only token1 (KRAIKEN)
|
|
floorEth = 0;
|
|
// Use position's lower tick for actual deposited amount
|
|
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
|
|
} else if (currentTick >= floorUpper) {
|
|
// Position is below current price - contains only token0 (WETH)
|
|
// Use position's upper tick for actual deposited amount
|
|
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
|
|
floorHarb = 0;
|
|
} else {
|
|
// Current price is within the position
|
|
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
|
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
|
|
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
|
|
}
|
|
} else {
|
|
if (currentTick < floorLower) {
|
|
// Position is above current price
|
|
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
|
|
floorEth = 0;
|
|
} else if (currentTick >= floorUpper) {
|
|
// Position is below current price
|
|
floorHarb = 0;
|
|
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
|
|
} else {
|
|
// Current price is within the position
|
|
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
|
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
|
|
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anchorLiq > 0) {
|
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(anchorLower);
|
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(anchorUpper);
|
|
|
|
if (token0isWeth) {
|
|
if (currentTick < anchorLower) {
|
|
anchorEth = 0;
|
|
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
|
|
} else if (currentTick >= anchorUpper) {
|
|
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
|
|
anchorHarb = 0;
|
|
} else {
|
|
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
|
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
|
|
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
|
|
}
|
|
} else {
|
|
if (currentTick < anchorLower) {
|
|
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
|
|
anchorEth = 0;
|
|
} else if (currentTick >= anchorUpper) {
|
|
anchorHarb = 0;
|
|
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
|
|
} else {
|
|
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
|
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
|
|
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (discoveryLiq > 0) {
|
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(discoveryLower);
|
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(discoveryUpper);
|
|
|
|
if (token0isWeth) {
|
|
if (currentTick < discoveryLower) {
|
|
discoveryEth = 0;
|
|
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
|
|
} else if (currentTick >= discoveryUpper) {
|
|
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
|
|
discoveryHarb = 0;
|
|
} else {
|
|
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
|
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
|
|
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
|
|
}
|
|
} else {
|
|
if (currentTick < discoveryLower) {
|
|
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
|
|
discoveryEth = 0;
|
|
} else if (currentTick >= discoveryUpper) {
|
|
discoveryHarb = 0;
|
|
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
|
|
} else {
|
|
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
|
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
|
|
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create position data row matching the expected CSV format
|
|
string memory row = string.concat(
|
|
label, ", ",
|
|
vm.toString(currentTick), ", ",
|
|
vm.toString(floorLower), ", ",
|
|
vm.toString(floorUpper), ", ",
|
|
vm.toString(floorEth), ", ",
|
|
vm.toString(floorHarb), ", ",
|
|
vm.toString(anchorLower), ", ",
|
|
vm.toString(anchorUpper), ", ",
|
|
vm.toString(anchorEth), ", ",
|
|
vm.toString(anchorHarb), ", ",
|
|
vm.toString(discoveryLower), ", ",
|
|
vm.toString(discoveryUpper), ", ",
|
|
vm.toString(discoveryEth), ", ",
|
|
vm.toString(discoveryHarb), ", ",
|
|
token0isWeth ? "true" : "false"
|
|
);
|
|
appendCSVRow(row);
|
|
}
|
|
} |