578 lines
No EOL
24 KiB
Solidity
578 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 {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
|
|
import {UniswapHelpers} from "../src/helpers/UniswapHelpers.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 account = makeAddr("trader");
|
|
address whale = makeAddr("whale");
|
|
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;
|
|
|
|
// OPTIMIZATION: Circular buffer for position tracking (max 50 positions)
|
|
uint256 constant MAX_TRACKED_POSITIONS = 50;
|
|
uint256[MAX_TRACKED_POSITIONS] public trackedPositions;
|
|
uint256 public positionWriteIndex;
|
|
uint256 public totalPositionsCreated;
|
|
mapping(uint256 => address) public positionOwners;
|
|
|
|
// Counter to limit CSV recording and prevent memory issues
|
|
uint256 private csvRecordCount;
|
|
|
|
// 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("Designed to reach discovery position with larger trades");
|
|
console.log(string.concat("Optimizer: ", optimizerClass));
|
|
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
|
|
console.log(string.concat("Staking enabled: ", enableStaking ? "true" : "false"));
|
|
console.log("");
|
|
|
|
testEnv = new TestEnvironment(feeDestination);
|
|
|
|
// Deploy factory once for all runs (gas optimization)
|
|
factory = UniswapHelpers.deployUniswapFactory();
|
|
|
|
// Get optimizer
|
|
address optimizerAddress = _getOptimizerByClass(optimizerClass);
|
|
|
|
// Track profitable scenarios (limit string concatenation to prevent memory issues)
|
|
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached\n";
|
|
uint256 profitableCount;
|
|
uint256 maxProfitableRecords = 20; // Limit to prevent memory issues
|
|
|
|
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 with existing factory
|
|
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
|
testEnv.setupEnvironmentWithExistingFactory(factory, seed % 2 == 0, feeDestination, optimizerAddress);
|
|
|
|
// Fund LiquidityManager with MORE ETH for deeper liquidity
|
|
vm.deal(address(lm), 200 ether); // Increased from 50
|
|
|
|
// Fund accounts with MORE capital
|
|
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(seed, "trader"))) % 150 ether); // 50-200 ETH
|
|
uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(seed, "whale"))) % 300 ether); // 200-500 ETH
|
|
|
|
vm.deal(account, traderFund * 2);
|
|
vm.deal(whale, whaleFund * 2);
|
|
|
|
vm.prank(account);
|
|
weth.deposit{value: traderFund}();
|
|
|
|
vm.prank(whale);
|
|
weth.deposit{value: whaleFund}();
|
|
|
|
// Create SwapExecutor once per scenario to avoid repeated deployments
|
|
swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
|
|
|
uint256 initialBalance = weth.balanceOf(account);
|
|
|
|
// Initial recenter
|
|
vm.prank(feeDestination);
|
|
try lm.recenter{gas: 50_000_000}() {} catch {}
|
|
|
|
// Initialize position tracking for each seed
|
|
if (trackPositions) {
|
|
// Reset tracking for new scenario
|
|
_resetPositionTracking();
|
|
clearCSV(); // Clear any previous data
|
|
csvRecordCount = 0; // Reset counter
|
|
initializePositionsCSV();
|
|
_recordPositionData("Initial");
|
|
csvRecordCount = 1; // Count the initial record
|
|
}
|
|
|
|
// 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)));
|
|
console.log(string.concat(" Profit: ", vm.toString(profit / 1e15), " finney (", vm.toString(profitPct), "%)"));
|
|
console.log(string.concat(" Discovery reached: ", reachedDiscovery ? "YES" : "NO"));
|
|
|
|
// Only record limited profitable scenarios to prevent memory issues
|
|
if (profitableCount < maxProfitableRecords) {
|
|
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) {
|
|
console.log("\n=== STAKING METRICS ===");
|
|
console.log(string.concat("Stakes attempted: ", vm.toString(totalStakesAttempted)));
|
|
console.log(string.concat("Stakes succeeded: ", vm.toString(totalStakesSucceeded)));
|
|
console.log(string.concat("Snatches attempted: ", vm.toString(totalSnatchesAttempted)));
|
|
console.log(string.concat("Snatches succeeded: ", vm.toString(totalSnatchesSucceeded)));
|
|
if (totalStakesAttempted > 0) {
|
|
console.log(string.concat("Stake success rate: ", vm.toString((totalStakesSucceeded * 100) / totalStakesAttempted), "%"));
|
|
}
|
|
if (totalSnatchesAttempted > 0) {
|
|
console.log(string.concat("Snatch success rate: ", vm.toString((totalSnatchesSucceeded * 100) / totalSnatchesAttempted), "%"));
|
|
}
|
|
}
|
|
|
|
if (profitableCount > 0) {
|
|
string memory filename = string.concat("improved_profitable_", vm.toString(block.timestamp), ".csv");
|
|
vm.writeFile(filename, profitableCSV);
|
|
console.log(string.concat("\nResults written to: ", filename));
|
|
}
|
|
}
|
|
|
|
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 some KRAIKEN tokens for trading/staking
|
|
_executeBuy(account, weth.balanceOf(account) / 4); // Buy 25% of ETH worth
|
|
_executeBuy(whale, weth.balanceOf(whale) / 4); // Whale buys 25% of ETH worth
|
|
|
|
// 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) {
|
|
console.log(" [DISCOVERY REACHED] at tick", vm.toString(currentTick));
|
|
if (trackPositions) {
|
|
_recordPositionData("Discovery_Reached");
|
|
}
|
|
}
|
|
|
|
// Final cleanup: sell all KRAIKEN
|
|
uint256 finalKraiken = harberg.balanceOf(account);
|
|
if (finalKraiken > 0) {
|
|
_executeSell(account, finalKraiken);
|
|
}
|
|
|
|
finalBalance = weth.balanceOf(account);
|
|
}
|
|
function _executeRandomLargeTrades(uint256 rand) internal {
|
|
console.log(" Strategy: Random Large Trades");
|
|
console.log(" Buy bias:", buyBias, "%");
|
|
console.log(" Trades:", tradesPerRun);
|
|
if (enableStaking) {
|
|
console.log(" Staking bias:", stakingBias, "%");
|
|
}
|
|
|
|
for (uint256 i = 0; i < tradesPerRun; i++) {
|
|
rand = uint256(keccak256(abi.encodePacked(rand, i)));
|
|
uint256 actor = rand % 2; // 0 = trader, 1 = whale
|
|
|
|
// 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)
|
|
}
|
|
|
|
address actorAddr = actor == 0 ? account : whale;
|
|
|
|
if (action == 0) {
|
|
// Large buy (30-80% of balance, or more with high buy bias)
|
|
uint256 buyPct = buyBias > 70 ? 50 + (rand % 40) : 30 + (rand % 51);
|
|
uint256 buyAmount = weth.balanceOf(actorAddr) * buyPct / 100;
|
|
if (buyAmount > 0) {
|
|
_executeBuy(actorAddr, buyAmount);
|
|
}
|
|
} else if (action == 1) {
|
|
// Large sell (reduced amounts with high buy bias)
|
|
uint256 sellPct = buyBias > 70 ? 10 + (rand % 30) : 30 + (rand % 71);
|
|
uint256 sellAmount = harberg.balanceOf(actorAddr) * sellPct / 100;
|
|
if (sellAmount > 0) {
|
|
_executeSell(actorAddr, 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) {
|
|
uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100;
|
|
if (stakingRoll < stakingBias) {
|
|
// Try to stake
|
|
_executeStake(rand + i * 1000);
|
|
} else {
|
|
// Try to unstake
|
|
_executeExitPosition(rand + i * 1000);
|
|
}
|
|
}
|
|
|
|
// Record position data for visualization (always record all trades)
|
|
if (trackPositions) {
|
|
_recordPositionDataSafe(string.concat("T", vm.toString(i)));
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
try swapExecutor.executeSell(amount, seller) {} catch {}
|
|
}
|
|
|
|
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"));
|
|
}
|
|
|
|
// Safe wrapper that prevents gas and memory issues
|
|
function _recordPositionDataSafe(string memory label) internal {
|
|
// Only record if we have enough gas remaining
|
|
if (gasleft() < 1_000_000) {
|
|
return; // Skip recording if low on gas
|
|
}
|
|
|
|
// Skip if CSV is getting extremely large (increased limit for all trades)
|
|
if (bytes(csv).length > 100000) {
|
|
return; // Skip to avoid memory issues
|
|
}
|
|
|
|
csvRecordCount++;
|
|
_recordPositionData(label);
|
|
}
|
|
|
|
function _recordPositionData(string memory label) internal {
|
|
// Absolutely prevent recording if buffer is too large
|
|
if (bytes(csv).length > 100000) {
|
|
return; // Stop recording to prevent memory issues
|
|
}
|
|
|
|
(,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);
|
|
|
|
// Skip staking data for trades to save memory (only get for key events)
|
|
uint256 pctStaked = 0;
|
|
uint256 avgTax = 0;
|
|
if (enableStaking && bytes(label).length > 1 && bytes(label)[0] != 0x54) { // Not a "T" trade label
|
|
try stake.getPercentageStaked{gas: 30000}() returns (uint256 pct) {
|
|
pctStaked = pct;
|
|
} catch {}
|
|
try stake.getAverageTaxRate{gas: 30000}() returns (uint256 tax) {
|
|
avgTax = tax;
|
|
} catch {}
|
|
}
|
|
|
|
// Build CSV row with minimal concatenations
|
|
string memory row = string.concat(
|
|
label, ",",
|
|
vm.toString(currentTick), ",",
|
|
vm.toString(floorLower), ","
|
|
);
|
|
|
|
row = string.concat(
|
|
row,
|
|
vm.toString(floorUpper), ",",
|
|
vm.toString(floorLiq), ","
|
|
);
|
|
|
|
row = string.concat(
|
|
row,
|
|
vm.toString(anchorLower), ",",
|
|
vm.toString(anchorUpper), ","
|
|
);
|
|
|
|
row = string.concat(
|
|
row,
|
|
vm.toString(anchorLiq), ",",
|
|
vm.toString(discoveryLower), ","
|
|
);
|
|
|
|
row = string.concat(
|
|
row,
|
|
vm.toString(discoveryUpper), ",",
|
|
vm.toString(discoveryLiq), ","
|
|
);
|
|
|
|
row = string.concat(
|
|
row,
|
|
token0isWeth ? "1" : "0", ",",
|
|
vm.toString(pctStaked), ",",
|
|
vm.toString(avgTax)
|
|
);
|
|
|
|
appendCSVRow(row);
|
|
}
|
|
|
|
|
|
function _executeStake(uint256 rand) internal {
|
|
address staker = rand % 2 == 0 ? account : whale;
|
|
uint256 harbBalance = harberg.balanceOf(staker);
|
|
|
|
if (harbBalance > harberg.minStake()) {
|
|
uint256 amount = harbBalance * (30 + (rand % 50)) / 100;
|
|
if (amount < harberg.minStake()) {
|
|
amount = harberg.minStake();
|
|
}
|
|
|
|
uint32 taxRate = uint32(rand % 30);
|
|
|
|
vm.prank(staker);
|
|
harberg.approve(address(stake), amount);
|
|
|
|
totalStakesAttempted++;
|
|
vm.prank(staker);
|
|
try stake.snatch{gas: 10_000_000}(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) {
|
|
totalStakesSucceeded++;
|
|
_addTrackedPosition(positionId, staker);
|
|
console.log(" STAKED:", amount / 1e18, "KRAIKEN");
|
|
if (trackPositions) {
|
|
_recordPositionDataSafe("Stake");
|
|
}
|
|
} catch {
|
|
// If regular stake fails, try snatching with higher tax rate
|
|
if (totalPositionsCreated > 0) {
|
|
_tryOptimizedSnatch(staker, amount);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function _tryOptimizedSnatch(address staker, uint256 amount) internal {
|
|
uint32 maxTaxRate = 29;
|
|
|
|
// Find up to 3 snatchable positions quickly
|
|
uint256[] memory toSnatch = _findSnatchableQuick(maxTaxRate, amount);
|
|
|
|
if (toSnatch.length > 0) {
|
|
totalSnatchesAttempted++;
|
|
vm.prank(staker);
|
|
try stake.snatch{gas: 15_000_000}(amount, staker, maxTaxRate, toSnatch) returns (uint256 positionId) {
|
|
totalSnatchesSucceeded++;
|
|
_addTrackedPosition(positionId, staker);
|
|
console.log(" SNATCHED", toSnatch.length, "positions");
|
|
if (trackPositions) {
|
|
_recordPositionDataSafe("Snatch");
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
function _executeExitPosition(uint256 rand) internal {
|
|
address staker = rand % 2 == 0 ? account : whale;
|
|
|
|
// Find a position owned by this staker
|
|
uint256 positionToExit = _findOwnedPosition(staker);
|
|
|
|
if (positionToExit > 0) {
|
|
vm.prank(staker);
|
|
try stake.exitPosition{gas: 5_000_000}(positionToExit) {
|
|
_removeTrackedPosition(positionToExit);
|
|
if (trackPositions) {
|
|
_recordPositionDataSafe("Unstake");
|
|
}
|
|
} catch {
|
|
// Position might be already exited/snatched
|
|
_removeTrackedPosition(positionToExit);
|
|
}
|
|
}
|
|
}
|
|
|
|
function _findSnatchableQuick(uint32 maxTaxRate, uint256 amountNeeded) internal view returns (uint256[] memory) {
|
|
uint256[] memory result = new uint256[](3); // Max 3 positions
|
|
uint256 count = 0;
|
|
uint256 totalShares = 0;
|
|
|
|
// Check only recent positions (last 20 in circular buffer)
|
|
uint256 checkCount = totalPositionsCreated < 20 ? totalPositionsCreated : 20;
|
|
uint256 startIdx = positionWriteIndex > checkCount ? positionWriteIndex - checkCount : 0;
|
|
|
|
for (uint256 i = 0; i < checkCount && count < 3; i++) {
|
|
uint256 idx = (startIdx + i) % MAX_TRACKED_POSITIONS;
|
|
uint256 positionId = trackedPositions[idx];
|
|
if (positionId == 0) continue;
|
|
|
|
(uint256 shares,, , , uint32 taxRate) = stake.positions(positionId);
|
|
|
|
if (shares > 0 && taxRate < maxTaxRate) {
|
|
result[count] = positionId;
|
|
totalShares += shares;
|
|
count++;
|
|
|
|
if (stake.sharesToAssets(totalShares) >= amountNeeded) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resize result
|
|
uint256[] memory finalResult = new uint256[](count);
|
|
for (uint256 i = 0; i < count; i++) {
|
|
finalResult[i] = result[i];
|
|
}
|
|
|
|
return finalResult;
|
|
}
|
|
|
|
// Helper functions for circular buffer position tracking
|
|
function _addTrackedPosition(uint256 positionId, address owner) internal {
|
|
trackedPositions[positionWriteIndex] = positionId;
|
|
positionOwners[positionId] = owner;
|
|
positionWriteIndex = (positionWriteIndex + 1) % MAX_TRACKED_POSITIONS;
|
|
totalPositionsCreated++;
|
|
}
|
|
|
|
function _removeTrackedPosition(uint256 positionId) internal {
|
|
delete positionOwners[positionId];
|
|
// Don't remove from circular buffer, just mark owner as deleted
|
|
}
|
|
|
|
function _resetPositionTracking() internal {
|
|
// Clear circular buffer
|
|
for (uint256 i = 0; i < MAX_TRACKED_POSITIONS; i++) {
|
|
trackedPositions[i] = 0;
|
|
}
|
|
positionWriteIndex = 0;
|
|
totalPositionsCreated = 0;
|
|
}
|
|
|
|
function _findOwnedPosition(address owner) internal view returns (uint256) {
|
|
// Check last 10 positions for ownership
|
|
uint256 checkCount = totalPositionsCreated < 10 ? totalPositionsCreated : 10;
|
|
uint256 startIdx = positionWriteIndex > checkCount ? positionWriteIndex - checkCount : 0;
|
|
|
|
for (uint256 i = 0; i < checkCount; i++) {
|
|
uint256 idx = (startIdx + i) % MAX_TRACKED_POSITIONS;
|
|
uint256 positionId = trackedPositions[idx];
|
|
|
|
if (positionId > 0 && positionOwners[positionId] == owner) {
|
|
return positionId;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
} |