650 lines
No EOL
28 KiB
Solidity
650 lines
No EOL
28 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 {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
|
|
import {UniswapHelpers} from "../src/helpers/UniswapHelpers.sol";
|
|
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
|
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
|
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
|
import "../test/mocks/BullMarketOptimizer.sol";
|
|
import "../test/mocks/WhaleOptimizer.sol";
|
|
import "./helpers/CSVManager.sol";
|
|
import "./helpers/SwapExecutor.sol";
|
|
|
|
/**
|
|
* @title ImprovedFuzzingAnalysis
|
|
* @notice Enhanced fuzzing with larger trades designed to reach discovery position
|
|
* @dev Uses more aggressive trading patterns to explore the full liquidity range
|
|
*/
|
|
contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|
TestEnvironment testEnv;
|
|
IUniswapV3Factory factory;
|
|
IUniswapV3Pool pool;
|
|
IWETH9 weth;
|
|
Kraiken harberg;
|
|
Stake stake;
|
|
LiquidityManager lm;
|
|
bool token0isWeth;
|
|
|
|
// Reusable swap executor to avoid repeated deployments
|
|
SwapExecutor swapExecutor;
|
|
|
|
address trader = makeAddr("trader");
|
|
address feeDestination = makeAddr("fees");
|
|
|
|
// Analysis metrics
|
|
uint256 public scenariosAnalyzed;
|
|
uint256 public profitableScenarios;
|
|
uint256 public discoveryReachedCount;
|
|
uint256 public totalStakesAttempted;
|
|
uint256 public totalStakesSucceeded;
|
|
uint256 public totalSnatchesAttempted;
|
|
uint256 public totalSnatchesSucceeded;
|
|
|
|
// Staking tracking
|
|
mapping(address => uint256[]) public activePositions;
|
|
uint256[] public allPositionIds; // Track all positions for snatching
|
|
|
|
// Configuration
|
|
uint256 public fuzzingRuns;
|
|
bool public trackPositions;
|
|
bool public enableStaking;
|
|
uint256 public buyBias; // 0-100, percentage bias towards buying vs selling
|
|
uint256 public tradesPerRun; // Number of trades/actions per scenario
|
|
uint256 public stakingBias; // 0-100, percentage bias towards staking vs unstaking
|
|
string public optimizerClass;
|
|
|
|
function run() public virtual {
|
|
_loadConfiguration();
|
|
|
|
console.log("=== IMPROVED Fuzzing Analysis ===");
|
|
console.log(string.concat("Optimizer: ", optimizerClass));
|
|
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
|
|
|
|
testEnv = new TestEnvironment(feeDestination);
|
|
|
|
// Deploy factory once for all runs (gas optimization)
|
|
factory = UniswapHelpers.deployUniswapFactory();
|
|
|
|
// Get optimizer
|
|
address optimizerAddress = _getOptimizerByClass(optimizerClass);
|
|
|
|
// Track profitable scenarios
|
|
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached\n";
|
|
uint256 profitableCount;
|
|
|
|
for (uint256 seed = 0; seed < fuzzingRuns; seed++) {
|
|
// Progress tracking removed
|
|
|
|
// Create fresh environment with existing factory
|
|
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
|
testEnv.setupEnvironmentWithExistingFactory(factory, seed % 2 == 0, feeDestination, optimizerAddress);
|
|
|
|
// Fund LiquidityManager with ETH proportional to trades
|
|
uint256 lmFunding = 100 ether + (tradesPerRun * 2 ether); // Base 100 + 2 ETH per trade
|
|
vm.deal(address(lm), lmFunding);
|
|
|
|
// Fund trader with capital proportional to number of trades
|
|
// Combine what was previously split between trader and whale
|
|
uint256 traderFund = 150 ether + (tradesPerRun * 8 ether); // 150 ETH base + 8 ETH per trade
|
|
|
|
// Add some randomness but keep it proportional
|
|
uint256 traderRandom = uint256(keccak256(abi.encodePacked(seed, "trader"))) % (tradesPerRun * 3 ether);
|
|
traderFund += traderRandom;
|
|
|
|
|
|
// Deal 2x to have extra for gas
|
|
vm.deal(trader, traderFund * 2);
|
|
|
|
vm.prank(trader);
|
|
weth.deposit{value: traderFund}();
|
|
|
|
// Create SwapExecutor once per scenario to avoid repeated deployments
|
|
swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth, lm);
|
|
|
|
// Initial recenter BEFORE recording initial balance
|
|
vm.prank(feeDestination);
|
|
try lm.recenter{gas: 50_000_000}() {} catch {}
|
|
|
|
// Record initial balance AFTER recenter so we account for pool state
|
|
uint256 initialBalance = weth.balanceOf(trader);
|
|
|
|
// Initialize position tracking for each seed
|
|
if (trackPositions) {
|
|
// Initialize CSV header for each seed (after clearCSV from previous run)
|
|
initializePositionsCSV();
|
|
_recordPositionData("Initial");
|
|
}
|
|
|
|
// Run improved trading scenario
|
|
(uint256 finalBalance, bool reachedDiscovery) = _runImprovedScenario(seed);
|
|
|
|
scenariosAnalyzed++;
|
|
if (reachedDiscovery) {
|
|
discoveryReachedCount++;
|
|
}
|
|
|
|
// Check profitability
|
|
if (finalBalance > initialBalance) {
|
|
uint256 profit = finalBalance - initialBalance;
|
|
uint256 profitPct = (profit * 100) / initialBalance;
|
|
profitableScenarios++;
|
|
|
|
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " - Profit: ", vm.toString(profitPct), "%"));
|
|
|
|
profitableCSV = string.concat(
|
|
profitableCSV,
|
|
optimizerClass, ",",
|
|
vm.toString(seed), ",",
|
|
vm.toString(initialBalance), ",",
|
|
vm.toString(finalBalance), ",",
|
|
vm.toString(profit), ",",
|
|
vm.toString(profitPct), ",",
|
|
reachedDiscovery ? "true" : "false", "\n"
|
|
);
|
|
profitableCount++;
|
|
}
|
|
|
|
// Write position CSV if tracking
|
|
if (trackPositions) {
|
|
_recordPositionData("Final");
|
|
string memory positionFilename = string.concat(
|
|
"improved_positions_", optimizerClass, "_", vm.toString(seed), ".csv"
|
|
);
|
|
writeCSVToFile(positionFilename);
|
|
clearCSV(); // Clear buffer for next run
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
console.log("\n=== ANALYSIS COMPLETE ===");
|
|
console.log(string.concat(" Total scenarios: ", vm.toString(scenariosAnalyzed)));
|
|
console.log(string.concat(" Profitable scenarios: ", vm.toString(profitableScenarios)));
|
|
console.log(string.concat(" Discovery reached: ", vm.toString(discoveryReachedCount), " times"));
|
|
console.log(string.concat(" Discovery rate: ", vm.toString((discoveryReachedCount * 100) / scenariosAnalyzed), "%"));
|
|
console.log(string.concat(" Profit rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
|
|
|
|
|
|
if (enableStaking) {
|
|
// Staking metrics logged to CSV only
|
|
}
|
|
|
|
if (profitableCount > 0) {
|
|
string memory filename = string.concat("improved_profitable_", vm.toString(block.timestamp), ".csv");
|
|
vm.writeFile(filename, profitableCSV);
|
|
// Results written to CSV
|
|
}
|
|
}
|
|
|
|
function _runImprovedScenario(uint256 seed) internal virtual returns (uint256 finalBalance, bool reachedDiscovery) {
|
|
uint256 rand = uint256(keccak256(abi.encodePacked(seed, block.timestamp)));
|
|
|
|
// Get initial discovery position
|
|
(, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
|
|
|
// Initial buy to generate KRAIKEN tokens for trading/staking
|
|
// If staking is enabled with 100% bias, buy even more initially
|
|
uint256 initialBuyPercent = (enableStaking && stakingBias >= 100) ? 60 :
|
|
(enableStaking ? 40 : 25); // 60% if 100% staking, 40% if staking, 25% otherwise
|
|
uint256 initialBuyAmount = weth.balanceOf(trader) * initialBuyPercent / 100;
|
|
_executeBuy(trader, initialBuyAmount);
|
|
|
|
// Always use random trading strategy for consistent behavior
|
|
_executeRandomLargeTrades(rand);
|
|
|
|
|
|
// Check if we reached discovery
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
reachedDiscovery = (currentTick >= discoveryLower && currentTick < discoveryUpper);
|
|
|
|
if (reachedDiscovery) {
|
|
// Discovery reached
|
|
if (trackPositions) {
|
|
_recordPositionData("Discovery_Reached");
|
|
}
|
|
}
|
|
|
|
// Check final balances before cleanup
|
|
uint256 traderKraiken = harberg.balanceOf(trader);
|
|
|
|
// Final cleanup: sell all KRAIKEN
|
|
if (traderKraiken > 0) {
|
|
_executeSell(trader, traderKraiken);
|
|
}
|
|
|
|
// Calculate final balance
|
|
finalBalance = weth.balanceOf(trader);
|
|
}
|
|
function _executeRandomLargeTrades(uint256 rand) internal {
|
|
uint256 stakingAttempts = 0;
|
|
for (uint256 i = 0; i < tradesPerRun; i++) {
|
|
rand = uint256(keccak256(abi.encodePacked(rand, i)));
|
|
|
|
// Use buy bias to determine action
|
|
uint256 actionRoll = rand % 100;
|
|
uint256 action;
|
|
|
|
if (actionRoll < buyBias) {
|
|
action = 0; // Buy (biased towards buying when buyBias > 50)
|
|
} else if (actionRoll < 90) {
|
|
action = 1; // Sell (reduced probability when buyBias is high)
|
|
} else {
|
|
action = 2; // Recenter (10% chance)
|
|
}
|
|
|
|
if (action == 0) {
|
|
// Large buy (30-80% of balance, or more with high buy bias)
|
|
uint256 buyPct = buyBias > 80 ? 60 + (rand % 31) : (buyBias > 70 ? 40 + (rand % 41) : 30 + (rand % 51));
|
|
uint256 wethBalance = weth.balanceOf(trader);
|
|
uint256 buyAmount = wethBalance * buyPct / 100;
|
|
if (buyAmount > 0 && wethBalance > 0) {
|
|
_executeBuy(trader, buyAmount);
|
|
}
|
|
} else if (action == 1) {
|
|
// Large sell (significantly reduced with high buy bias to maintain KRAIKEN balance)
|
|
uint256 sellPct = buyBias > 80 ? 5 + (rand % 16) : (buyBias > 70 ? 10 + (rand % 21) : 30 + (rand % 71));
|
|
uint256 sellAmount = harberg.balanceOf(trader) * sellPct / 100;
|
|
if (sellAmount > 0) {
|
|
_executeSell(trader, sellAmount);
|
|
}
|
|
} else {
|
|
// Recenter
|
|
vm.warp(block.timestamp + (rand % 2 hours));
|
|
vm.prank(feeDestination);
|
|
try lm.recenter{gas: 50_000_000}() {} catch {}
|
|
}
|
|
|
|
// Every 3rd trade, attempt staking/unstaking if enabled
|
|
if (enableStaking && i % 3 == 2) {
|
|
stakingAttempts++;
|
|
uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100;
|
|
if (stakingRoll < stakingBias) {
|
|
// Before staking, ensure we have tokens
|
|
uint256 harbBalance = harberg.balanceOf(trader);
|
|
uint256 minStakeAmount = harberg.minStake();
|
|
|
|
// With 100% staking bias, aggressively buy tokens if needed
|
|
// We want to maintain a large KRAIKEN balance for staking
|
|
if (stakingBias >= 100 && harbBalance <= minStakeAmount * 10) {
|
|
uint256 wethBalance = weth.balanceOf(trader);
|
|
if (wethBalance > 0) {
|
|
// Buy 30-50% of ETH worth to get substantial tokens for staking
|
|
uint256 buyAmount = wethBalance * (30 + (rand % 21)) / 100;
|
|
_executeBuy(trader, buyAmount);
|
|
}
|
|
} else if (harbBalance <= minStakeAmount * 2) {
|
|
uint256 wethBalance = weth.balanceOf(trader);
|
|
if (wethBalance > 0) {
|
|
// Buy 15-25% of ETH worth to get tokens for staking
|
|
uint256 buyAmount = wethBalance * (15 + (rand % 11)) / 100;
|
|
_executeBuy(trader, buyAmount);
|
|
}
|
|
}
|
|
|
|
// Now try to stake
|
|
_executeStake(rand + i * 1000);
|
|
} else {
|
|
// Try to unstake
|
|
_executeExitPosition(rand + i * 1000);
|
|
}
|
|
}
|
|
|
|
// Only record every 5th trade to avoid memory issues with large trade counts
|
|
if (trackPositions && i % 5 == 0) {
|
|
_recordPositionData("Trade");
|
|
}
|
|
}
|
|
|
|
uint256 expectedStakeActions = tradesPerRun / 3;
|
|
uint256 expectedStakes = (expectedStakeActions * stakingBias) / 100;
|
|
// Staking actions configured
|
|
}
|
|
|
|
function _executeBuy(address buyer, uint256 amount) internal virtual {
|
|
if (amount == 0 || weth.balanceOf(buyer) < amount) return;
|
|
|
|
vm.prank(buyer);
|
|
weth.transfer(address(swapExecutor), amount);
|
|
|
|
try swapExecutor.executeBuy(amount, buyer) {} catch {}
|
|
}
|
|
|
|
function _executeSell(address seller, uint256 amount) internal virtual {
|
|
if (amount == 0 || harberg.balanceOf(seller) < amount) return;
|
|
|
|
vm.prank(seller);
|
|
harberg.transfer(address(swapExecutor), amount);
|
|
|
|
swapExecutor.executeSell(amount, seller); // No try-catch, let errors bubble up
|
|
}
|
|
|
|
function _getOptimizerByClass(string memory class) internal returns (address) {
|
|
if (keccak256(bytes(class)) == keccak256("BullMarketOptimizer")) {
|
|
return address(new BullMarketOptimizer());
|
|
} else if (keccak256(bytes(class)) == keccak256("WhaleOptimizer")) {
|
|
return address(new WhaleOptimizer());
|
|
} else {
|
|
return address(new BullMarketOptimizer());
|
|
}
|
|
}
|
|
|
|
function _loadConfiguration() internal {
|
|
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(20));
|
|
trackPositions = vm.envOr("TRACK_POSITIONS", false);
|
|
enableStaking = vm.envOr("ENABLE_STAKING", true); // Default to true
|
|
buyBias = vm.envOr("BUY_BIAS", uint256(50)); // Default 50% (balanced)
|
|
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); // Default 15 trades
|
|
stakingBias = vm.envOr("STAKING_BIAS", uint256(80)); // Default 80% stake vs 20% unstake
|
|
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
|
|
}
|
|
|
|
function _recordPositionData(string memory label) internal {
|
|
// Split into separate function calls to avoid stack too deep
|
|
_recordPositionDataInternal(label);
|
|
}
|
|
|
|
function _recordPositionDataInternal(string memory label) private {
|
|
// Disable position tracking if it causes memory issues
|
|
if (bytes(csv).length > 50000) {
|
|
// CSV is getting too large, skip recording
|
|
return;
|
|
}
|
|
|
|
(,int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
// Get position data
|
|
(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);
|
|
|
|
// Use simpler row building to avoid memory issues
|
|
string memory row = label;
|
|
row = string.concat(row, ",", vm.toString(currentTick));
|
|
row = string.concat(row, ",", vm.toString(floorLower));
|
|
row = string.concat(row, ",", vm.toString(floorUpper));
|
|
row = string.concat(row, ",", vm.toString(floorLiq));
|
|
row = string.concat(row, ",", vm.toString(anchorLower));
|
|
row = string.concat(row, ",", vm.toString(anchorUpper));
|
|
row = string.concat(row, ",", vm.toString(anchorLiq));
|
|
row = string.concat(row, ",", vm.toString(discoveryLower));
|
|
row = string.concat(row, ",", vm.toString(discoveryUpper));
|
|
row = string.concat(row, ",", vm.toString(discoveryLiq));
|
|
row = string.concat(row, ",", token0isWeth ? "true" : "false");
|
|
row = string.concat(row, ",", vm.toString(stake.getPercentageStaked()));
|
|
row = string.concat(row, ",", vm.toString(stake.getAverageTaxRate()));
|
|
|
|
appendCSVRow(row);
|
|
}
|
|
|
|
function _executeStakingAction(uint256 rand) internal {
|
|
uint256 action = rand % 100;
|
|
|
|
// 70% chance to stake, 5% chance to exit (to fill pool faster)
|
|
if (action < 70) {
|
|
_executeStake(rand);
|
|
} else if (action < 75) {
|
|
_executeExitPosition(rand);
|
|
}
|
|
}
|
|
|
|
function _executeStakeWithAmount(address staker, uint256 amount, uint32 taxRate) internal {
|
|
// Direct stake with specific amount and tax rate
|
|
_doStake(staker, amount, taxRate);
|
|
}
|
|
|
|
function _executeStake(uint256 rand) internal {
|
|
address staker = trader;
|
|
uint256 harbBalance = harberg.balanceOf(staker);
|
|
uint256 minStakeAmount = harberg.minStake();
|
|
|
|
|
|
if (harbBalance > minStakeAmount) {
|
|
// With high staking bias (>= 90%), stake VERY aggressively
|
|
uint256 minPct = stakingBias >= 100 ? 50 : (stakingBias >= 90 ? 30 : 10);
|
|
uint256 maxPct = stakingBias >= 100 ? 100 : (stakingBias >= 90 ? 70 : 30);
|
|
|
|
// Stake between minPct% and maxPct% of balance
|
|
uint256 amount = harbBalance * (minPct + (rand % (maxPct - minPct + 1))) / 100;
|
|
if (amount < minStakeAmount) {
|
|
amount = minStakeAmount;
|
|
}
|
|
|
|
// With 100% staking bias, allow staking ALL tokens
|
|
// Otherwise keep a small reserve
|
|
if (stakingBias < 100) {
|
|
uint256 maxStake = harbBalance * 90 / 100; // Keep 10% for trading if not 100% bias
|
|
if (amount > maxStake) {
|
|
amount = maxStake;
|
|
}
|
|
}
|
|
// If stakingBias == 100, no limit - can stake entire balance
|
|
|
|
// Initial staking: use lower tax rates (0-15) to enable snatching later
|
|
uint32 taxRate = uint32(rand % 16); // 0-15 instead of 0-29
|
|
|
|
vm.prank(staker);
|
|
harberg.approve(address(stake), amount);
|
|
_doStake(staker, amount, taxRate);
|
|
}
|
|
// Silently skip if insufficient balance - this is expected behavior
|
|
}
|
|
|
|
function _doStake(address staker, uint256 amount, uint32 taxRate) internal {
|
|
vm.startPrank(staker);
|
|
|
|
// Check current pool capacity before attempting stake
|
|
uint256 currentPercentStaked = stake.getPercentageStaked();
|
|
|
|
// Log pool status before attempting (currentPercentStaked is in 1e18, where 1e18 = 100%)
|
|
if (currentPercentStaked > 95e16) { // > 95%
|
|
// Pool near full
|
|
}
|
|
|
|
// First try to stake without snatching
|
|
totalStakesAttempted++;
|
|
|
|
|
|
try stake.snatch(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) {
|
|
totalStakesSucceeded++;
|
|
activePositions[staker].push(positionId);
|
|
allPositionIds.push(positionId);
|
|
if (trackPositions) {
|
|
_recordPositionData("Stake");
|
|
}
|
|
} catch Error(string memory reason) {
|
|
// Caught string error - try snatching
|
|
|
|
// Use high tax rate (28) for snatching - can snatch anything with rate 0-27
|
|
uint32 snatchTaxRate = 28;
|
|
|
|
// Find positions to snatch (those with lower tax rates)
|
|
uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount);
|
|
|
|
if (positionsToSnatch.length > 0) {
|
|
totalSnatchesAttempted++;
|
|
try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) {
|
|
totalSnatchesSucceeded++;
|
|
activePositions[staker].push(positionId);
|
|
allPositionIds.push(positionId);
|
|
|
|
// Remove snatched positions from tracking
|
|
_removeSnatchedPositions(positionsToSnatch);
|
|
|
|
if (trackPositions) {
|
|
_recordPositionData("Snatch");
|
|
}
|
|
} catch {}
|
|
}
|
|
} catch {
|
|
// Catch-all for non-string errors (likely ExceededAvailableStake)
|
|
// Stake failed - trying snatch
|
|
|
|
// Now try snatching with high tax rate
|
|
uint32 snatchTaxRate = 28;
|
|
uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount);
|
|
|
|
if (positionsToSnatch.length > 0) {
|
|
totalSnatchesAttempted++;
|
|
try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) {
|
|
totalSnatchesSucceeded++;
|
|
activePositions[staker].push(positionId);
|
|
allPositionIds.push(positionId);
|
|
_removeSnatchedPositions(positionsToSnatch);
|
|
|
|
if (trackPositions) {
|
|
_recordPositionData("Snatch");
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function _executeExitPosition(uint256 rand) internal {
|
|
address staker = trader;
|
|
|
|
if (activePositions[staker].length > 0) {
|
|
uint256 index = rand % activePositions[staker].length;
|
|
uint256 positionId = activePositions[staker][index];
|
|
|
|
vm.prank(staker);
|
|
try stake.exitPosition(positionId) {
|
|
// Remove from array
|
|
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
|
|
activePositions[staker].pop();
|
|
|
|
// Also remove from allPositionIds
|
|
for (uint256 j = 0; j < allPositionIds.length; j++) {
|
|
if (allPositionIds[j] == positionId) {
|
|
allPositionIds[j] = 0; // Mark as removed
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (trackPositions) {
|
|
_recordPositionData("ExitStake");
|
|
}
|
|
} catch {
|
|
// Exit failed (position might be liquidated), remove from tracking
|
|
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
|
|
activePositions[staker].pop();
|
|
|
|
// Also remove from allPositionIds
|
|
for (uint256 j = 0; j < allPositionIds.length; j++) {
|
|
if (allPositionIds[j] == positionId) {
|
|
allPositionIds[j] = 0; // Mark as removed
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function _findSnatchablePositions(uint32 snatchTaxRate, uint256 amountNeeded) internal view returns (uint256[] memory) {
|
|
// Find positions with tax rates lower than snatchTaxRate
|
|
uint256[] memory snatchable = new uint256[](10); // Max 10 positions to snatch
|
|
uint256 count = 0;
|
|
uint256 totalShares = 0;
|
|
uint256 skippedHighTax = 0;
|
|
uint256 skippedEmpty = 0;
|
|
|
|
for (uint256 i = 0; i < allPositionIds.length && count < 10; i++) {
|
|
uint256 positionId = allPositionIds[i];
|
|
if (positionId == 0) continue;
|
|
|
|
// Get position info
|
|
(uint256 shares, address owner,, , uint32 taxRate) = stake.positions(positionId);
|
|
|
|
// Skip if position doesn't exist or tax rate is too high
|
|
if (shares == 0) {
|
|
skippedEmpty++;
|
|
continue;
|
|
}
|
|
if (taxRate >= snatchTaxRate) {
|
|
skippedHighTax++;
|
|
continue;
|
|
}
|
|
|
|
snatchable[count] = positionId;
|
|
totalShares += shares;
|
|
count++;
|
|
|
|
// Check if we have enough shares to cover the amount needed
|
|
uint256 assetsFromShares = stake.sharesToAssets(totalShares);
|
|
if (assetsFromShares >= amountNeeded) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Resize array to actual count
|
|
uint256[] memory result = new uint256[](count);
|
|
for (uint256 i = 0; i < count; i++) {
|
|
result[i] = snatchable[i];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function _removeSnatchedPositions(uint256[] memory snatchedIds) internal {
|
|
// Remove snatched positions from allPositionIds
|
|
for (uint256 i = 0; i < snatchedIds.length; i++) {
|
|
uint256 snatchedId = snatchedIds[i];
|
|
|
|
// Find and remove from allPositionIds
|
|
for (uint256 j = 0; j < allPositionIds.length; j++) {
|
|
if (allPositionIds[j] == snatchedId) {
|
|
allPositionIds[j] = 0; // Mark as removed
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Remove from trader's activePositions
|
|
uint256[] storage traderPositions = activePositions[trader];
|
|
for (uint256 m = 0; m < traderPositions.length; m++) {
|
|
if (traderPositions[m] == snatchedId) {
|
|
traderPositions[m] = traderPositions[traderPositions.length - 1];
|
|
traderPositions.pop();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function _logCurrentPosition(int24 currentTick) internal view {
|
|
(, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
|
(, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
|
|
|
if (currentTick >= anchorLower && currentTick <= anchorUpper) {
|
|
// In anchor position
|
|
} else if (currentTick >= discoveryLower && currentTick <= discoveryUpper) {
|
|
// In discovery position
|
|
} else {
|
|
// Outside all positions
|
|
}
|
|
}
|
|
|
|
function _logInitialFloor() internal view {
|
|
// Removed to reduce logging
|
|
}
|
|
|
|
function _logFinalState() internal view {
|
|
// Removed to reduce logging
|
|
uint160 sqrtPriceUpper = TickMath.getSqrtRatioAtTick(floorUpper);
|
|
|
|
if (tick < floorLower) {
|
|
// All liquidity is in KRAIKEN
|
|
uint256 kraikenAmount = token0isWeth ?
|
|
LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity) :
|
|
LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity);
|
|
// All liquidity in KRAIKEN
|
|
}
|
|
} |