feat: Add staking functionality to fuzzing analysis with snatching
- Integrate staking and exitPosition actions into fuzzing scenarios - Add staking metrics (percentageStaked, avgTaxRate) to CSV output - Implement snatching mechanism when stake pool reaches capacity - Add configurable staking parameters (enable/disable, buy bias, staking bias) - Support configurable number of trades per run - Simplify to single trader account with proportional ETH funding - Configure tax rates: 0-15 for initial stakes, 28 for snatching - Clean up debug logging and verbose output 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
74e09bb38b
commit
8b537302d8
5 changed files with 439 additions and 288 deletions
|
|
@ -34,8 +34,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
// Reusable swap executor to avoid repeated deployments
|
||||
SwapExecutor swapExecutor;
|
||||
|
||||
address account = makeAddr("trader");
|
||||
address whale = makeAddr("whale");
|
||||
address trader = makeAddr("trader");
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
// Analysis metrics
|
||||
|
|
@ -47,15 +46,9 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
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;
|
||||
// Staking tracking
|
||||
mapping(address => uint256[]) public activePositions;
|
||||
uint256[] public allPositionIds; // Track all positions for snatching
|
||||
|
||||
// Configuration
|
||||
uint256 public fuzzingRuns;
|
||||
|
|
@ -84,10 +77,9 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
// Get optimizer
|
||||
address optimizerAddress = _getOptimizerByClass(optimizerClass);
|
||||
|
||||
// Track profitable scenarios (limit string concatenation to prevent memory issues)
|
||||
// Track profitable scenarios
|
||||
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) {
|
||||
|
|
@ -98,26 +90,29 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
(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 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 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
|
||||
// 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
|
||||
|
||||
vm.deal(account, traderFund * 2);
|
||||
vm.deal(whale, whaleFund * 2);
|
||||
// Add some randomness but keep it proportional
|
||||
uint256 traderRandom = uint256(keccak256(abi.encodePacked(seed, "trader"))) % (tradesPerRun * 3 ether);
|
||||
traderFund += traderRandom;
|
||||
|
||||
vm.prank(account);
|
||||
|
||||
// Deal 2x to have extra for gas
|
||||
vm.deal(trader, traderFund * 2);
|
||||
|
||||
vm.prank(trader);
|
||||
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);
|
||||
uint256 initialBalance = weth.balanceOf(trader);
|
||||
|
||||
// Initial recenter
|
||||
vm.prank(feeDestination);
|
||||
|
|
@ -125,13 +120,9 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
|
||||
// Initialize position tracking for each seed
|
||||
if (trackPositions) {
|
||||
// Reset tracking for new scenario
|
||||
_resetPositionTracking();
|
||||
clearCSV(); // Clear any previous data
|
||||
csvRecordCount = 0; // Reset counter
|
||||
// Initialize CSV header for each seed (after clearCSV from previous run)
|
||||
initializePositionsCSV();
|
||||
_recordPositionData("Initial");
|
||||
csvRecordCount = 1; // Count the initial record
|
||||
}
|
||||
|
||||
// Run improved trading scenario
|
||||
|
|
@ -152,8 +143,6 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
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, ",",
|
||||
|
|
@ -164,7 +153,6 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
vm.toString(profitPct), ",",
|
||||
reachedDiscovery ? "true" : "false", "\n"
|
||||
);
|
||||
}
|
||||
profitableCount++;
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +175,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
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)));
|
||||
|
|
@ -215,8 +204,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
(, 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
|
||||
_executeBuy(trader, weth.balanceOf(trader) / 4); // Buy 25% of ETH worth
|
||||
|
||||
// Always use random trading strategy for consistent behavior
|
||||
_executeRandomLargeTrades(rand);
|
||||
|
|
@ -233,25 +221,31 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Check final balances before cleanup
|
||||
uint256 traderKraiken = harberg.balanceOf(trader);
|
||||
uint256 stakedAmount = harberg.balanceOf(address(stake));
|
||||
uint256 outstandingSupply = harberg.outstandingSupply();
|
||||
uint256 minStake = harberg.minStake();
|
||||
|
||||
// Final cleanup: sell all KRAIKEN
|
||||
uint256 finalKraiken = harberg.balanceOf(account);
|
||||
if (finalKraiken > 0) {
|
||||
_executeSell(account, finalKraiken);
|
||||
if (traderKraiken > 0) {
|
||||
_executeSell(trader, traderKraiken);
|
||||
}
|
||||
|
||||
finalBalance = weth.balanceOf(account);
|
||||
finalBalance = weth.balanceOf(trader);
|
||||
}
|
||||
function _executeRandomLargeTrades(uint256 rand) internal {
|
||||
console.log(" Strategy: Random Large Trades");
|
||||
console.log(" Buy bias:", buyBias, "%");
|
||||
console.log(" Trades:", tradesPerRun);
|
||||
console.log(string.concat(" Buy bias: ", vm.toString(buyBias), "%"));
|
||||
console.log(string.concat(" Trades per run: ", vm.toString(tradesPerRun)));
|
||||
if (enableStaking) {
|
||||
console.log(" Staking bias:", stakingBias, "%");
|
||||
console.log(string.concat(" Staking bias: ", vm.toString(stakingBias), "%"));
|
||||
console.log(" Staking every 3rd trade");
|
||||
}
|
||||
|
||||
uint256 stakingAttempts = 0;
|
||||
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;
|
||||
|
|
@ -265,21 +259,20 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
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);
|
||||
uint256 wethBalance = weth.balanceOf(trader);
|
||||
uint256 buyAmount = wethBalance * buyPct / 100;
|
||||
if (buyAmount > 0 && wethBalance > 0) {
|
||||
_executeBuy(trader, 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;
|
||||
uint256 sellAmount = harberg.balanceOf(trader) * sellPct / 100;
|
||||
if (sellAmount > 0) {
|
||||
_executeSell(actorAddr, sellAmount);
|
||||
_executeSell(trader, sellAmount);
|
||||
}
|
||||
} else {
|
||||
// Recenter
|
||||
|
|
@ -290,6 +283,7 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
|
||||
// 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) {
|
||||
// Try to stake
|
||||
|
|
@ -300,11 +294,18 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Record position data for visualization (always record all trades)
|
||||
if (trackPositions) {
|
||||
_recordPositionDataSafe(string.concat("T", vm.toString(i)));
|
||||
// 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;
|
||||
console.log(string.concat(" Total staking actions: ", vm.toString(stakingAttempts), " (every 3rd trade)"));
|
||||
console.log(string.concat(" Expected stake calls: ~", vm.toString(expectedStakes), " (", vm.toString(stakingBias), "% of actions)"));
|
||||
console.log(string.concat(" Expected exit calls: ~", vm.toString(expectedStakeActions - expectedStakes), " (", vm.toString(100 - stakingBias), "% of actions)"));
|
||||
console.log(" Note: Actual stakes limited by available KRAIKEN balance");
|
||||
}
|
||||
|
||||
function _executeBuy(address buyer, uint256 amount) internal virtual {
|
||||
|
|
@ -345,26 +346,16 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
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
|
||||
// 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();
|
||||
|
|
@ -374,205 +365,243 @@ contract ImprovedFuzzingAnalysis is Test, CSVManager {
|
|||
(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)
|
||||
);
|
||||
// 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;
|
||||
|
||||
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();
|
||||
// 70% chance to stake, 5% chance to exit (to fill pool faster)
|
||||
if (action < 70) {
|
||||
_executeStake(rand);
|
||||
} else if (action < 75) {
|
||||
_executeExitPosition(rand);
|
||||
}
|
||||
}
|
||||
|
||||
uint32 taxRate = uint32(rand % 30);
|
||||
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) {
|
||||
// Stake between 5% and 20% of balance for more granular positions
|
||||
uint256 amount = harbBalance * (5 + (rand % 16)) / 100;
|
||||
if (amount < minStakeAmount) {
|
||||
amount = minStakeAmount;
|
||||
}
|
||||
|
||||
// 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%
|
||||
console.log(string.concat(" [POOL NEAR FULL] ", vm.toString(currentPercentStaked / 1e16), "% of authorized"));
|
||||
}
|
||||
|
||||
// First try to stake without snatching
|
||||
totalStakesAttempted++;
|
||||
vm.prank(staker);
|
||||
try stake.snatch{gas: 10_000_000}(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) {
|
||||
|
||||
|
||||
try stake.snatch(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) {
|
||||
totalStakesSucceeded++;
|
||||
_addTrackedPosition(positionId, staker);
|
||||
console.log(" STAKED:", amount / 1e18, "KRAIKEN");
|
||||
activePositions[staker].push(positionId);
|
||||
allPositionIds.push(positionId);
|
||||
if (trackPositions) {
|
||||
_recordPositionDataSafe("Stake");
|
||||
_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 {
|
||||
// If regular stake fails, try snatching with higher tax rate
|
||||
if (totalPositionsCreated > 0) {
|
||||
_tryOptimizedSnatch(staker, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Catch-all for non-string errors (likely ExceededAvailableStake)
|
||||
console.log(string.concat(" Stake failed at ", vm.toString(currentPercentStaked / 1e16), "% - trying snatch"));
|
||||
|
||||
function _tryOptimizedSnatch(address staker, uint256 amount) internal {
|
||||
uint32 maxTaxRate = 29;
|
||||
// Now try snatching with high tax rate
|
||||
uint32 snatchTaxRate = 28;
|
||||
uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount);
|
||||
|
||||
// Find up to 3 snatchable positions quickly
|
||||
uint256[] memory toSnatch = _findSnatchableQuick(maxTaxRate, amount);
|
||||
|
||||
if (toSnatch.length > 0) {
|
||||
if (positionsToSnatch.length > 0) {
|
||||
totalSnatchesAttempted++;
|
||||
vm.prank(staker);
|
||||
try stake.snatch{gas: 15_000_000}(amount, staker, maxTaxRate, toSnatch) returns (uint256 positionId) {
|
||||
try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) {
|
||||
totalSnatchesSucceeded++;
|
||||
_addTrackedPosition(positionId, staker);
|
||||
console.log(" SNATCHED", toSnatch.length, "positions");
|
||||
activePositions[staker].push(positionId);
|
||||
allPositionIds.push(positionId);
|
||||
_removeSnatchedPositions(positionsToSnatch);
|
||||
|
||||
if (trackPositions) {
|
||||
_recordPositionDataSafe("Snatch");
|
||||
_recordPositionData("Snatch");
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function _executeExitPosition(uint256 rand) internal {
|
||||
address staker = rand % 2 == 0 ? account : whale;
|
||||
address staker = trader;
|
||||
|
||||
// Find a position owned by this staker
|
||||
uint256 positionToExit = _findOwnedPosition(staker);
|
||||
if (activePositions[staker].length > 0) {
|
||||
uint256 index = rand % activePositions[staker].length;
|
||||
uint256 positionId = activePositions[staker][index];
|
||||
|
||||
if (positionToExit > 0) {
|
||||
vm.prank(staker);
|
||||
try stake.exitPosition{gas: 5_000_000}(positionToExit) {
|
||||
_removeTrackedPosition(positionToExit);
|
||||
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) {
|
||||
_recordPositionDataSafe("Unstake");
|
||||
_recordPositionData("ExitStake");
|
||||
}
|
||||
} catch {
|
||||
// Position might be already exited/snatched
|
||||
_removeTrackedPosition(positionToExit);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Exit failed (position might be liquidated), remove from tracking
|
||||
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
|
||||
activePositions[staker].pop();
|
||||
|
||||
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) {
|
||||
// Also remove from allPositionIds
|
||||
for (uint256 j = 0; j < allPositionIds.length; j++) {
|
||||
if (allPositionIds[j] == positionId) {
|
||||
allPositionIds[j] = 0; // Mark as removed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize result
|
||||
uint256[] memory finalResult = new uint256[](count);
|
||||
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++) {
|
||||
finalResult[i] = result[i];
|
||||
result[i] = snatchable[i];
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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 _removeSnatchedPositions(uint256[] memory snatchedIds) internal {
|
||||
// Remove snatched positions from allPositionIds
|
||||
for (uint256 i = 0; i < snatchedIds.length; i++) {
|
||||
uint256 snatchedId = snatchedIds[i];
|
||||
|
||||
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;
|
||||
// Find and remove from allPositionIds
|
||||
for (uint256 j = 0; j < allPositionIds.length; j++) {
|
||||
if (allPositionIds[j] == snatchedId) {
|
||||
allPositionIds[j] = 0; // Mark as removed
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,46 +92,6 @@ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis {
|
|||
_recordAction("SELL", seller, amount, tickBefore, tickAfter);
|
||||
}
|
||||
|
||||
// Override each strategy to add recenter recording
|
||||
function _executeDiscoveryPush(uint256 rand) internal override {
|
||||
console.log(" Strategy: Discovery Push [RECORDING]");
|
||||
|
||||
// Both accounts buy large amounts first
|
||||
uint256 traderBuy = weth.balanceOf(account) * 7 / 10;
|
||||
uint256 whaleBuy = weth.balanceOf(whale) * 8 / 10;
|
||||
|
||||
_executeBuy(account, traderBuy);
|
||||
_executeBuy(whale, whaleBuy);
|
||||
|
||||
// Record position snapshot
|
||||
if (trackPositions) {
|
||||
_recordPositionData("MassiveBuy");
|
||||
}
|
||||
|
||||
// Whale dumps
|
||||
uint256 whaleKraiken = harberg.balanceOf(whale);
|
||||
_executeSell(whale, whaleKraiken);
|
||||
|
||||
if (trackPositions) {
|
||||
_recordPositionData("WhaleDump");
|
||||
}
|
||||
|
||||
// Trader actions
|
||||
uint256 traderKraiken = harberg.balanceOf(account);
|
||||
if (traderKraiken > 0) {
|
||||
_executeSell(account, traderKraiken / 2);
|
||||
|
||||
// Recenter
|
||||
_executeRecenter();
|
||||
|
||||
// Buy back
|
||||
uint256 remainingWeth = weth.balanceOf(account);
|
||||
if (remainingWeth > 0) {
|
||||
_executeBuy(account, remainingWeth / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _executeRecenter() internal {
|
||||
(, int24 tickBefore,,,,,) = pool.slot0();
|
||||
|
||||
|
|
@ -152,8 +112,8 @@ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis {
|
|||
(uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
recorder.recordPreState(
|
||||
weth.balanceOf(account),
|
||||
harberg.balanceOf(account),
|
||||
weth.balanceOf(trader),
|
||||
harberg.balanceOf(trader),
|
||||
currentTick,
|
||||
uint256(sqrtPriceX96),
|
||||
0, // VWAP placeholder
|
||||
|
|
@ -230,14 +190,14 @@ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis {
|
|||
script = string.concat(
|
||||
script,
|
||||
" _executeBuy(",
|
||||
action.actor == account ? "trader" : "whale",
|
||||
action.actor == trader ? "trader" : "system",
|
||||
", ", vm.toString(action.amount), ");\n"
|
||||
);
|
||||
} else if (keccak256(bytes(action.actionType)) == keccak256("SELL")) {
|
||||
script = string.concat(
|
||||
script,
|
||||
" _executeSell(",
|
||||
action.actor == account ? "trader" : "whale",
|
||||
action.actor == trader ? "trader" : "system",
|
||||
", ", vm.toString(action.amount), ");\n"
|
||||
);
|
||||
} else if (keccak256(bytes(action.actionType)) == keccak256("RECENTER")) {
|
||||
|
|
@ -288,7 +248,7 @@ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis {
|
|||
summary,
|
||||
" ", vm.toString(i + 1), ". ",
|
||||
action.actionType, " - ",
|
||||
action.actor == account ? "Trader" : action.actor == whale ? "Whale" : "System",
|
||||
action.actor == trader ? "Trader" : "System",
|
||||
action.amount > 0 ? string.concat(" - Amount: ", vm.toString(action.amount / 1e18), " ETH") : "",
|
||||
" - Tick: ", vm.toString(action.tickBefore), " -> ", vm.toString(action.tickAfter),
|
||||
"\n"
|
||||
|
|
|
|||
162
onchain/analysis/run-improved-fuzzing.sh
Executable file
162
onchain/analysis/run-improved-fuzzing.sh
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Usage: ./run-improved-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N]
|
||||
# Examples:
|
||||
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=50
|
||||
# ./run-improved-fuzzing.sh WhaleOptimizer runs=20 staking=off
|
||||
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
|
||||
# Configuration
|
||||
OPTIMIZER=${1:-BullMarketOptimizer}
|
||||
RUNS=${2:-runs=20}
|
||||
STAKING=${3:-staking=on}
|
||||
BUYBIAS=${4:-buybias=50}
|
||||
TRADES=${5:-trades=15}
|
||||
STAKINGBIAS=${6:-stakingbias=80}
|
||||
|
||||
# Parse runs parameter
|
||||
if [[ $RUNS == runs=* ]]; then
|
||||
RUNS_VALUE=${RUNS#runs=}
|
||||
else
|
||||
RUNS_VALUE=$RUNS
|
||||
fi
|
||||
|
||||
# Parse staking parameter
|
||||
STAKING_ENABLED="true"
|
||||
if [[ $STAKING == staking=* ]]; then
|
||||
STAKING_VALUE=${STAKING#staking=}
|
||||
if [[ $STAKING_VALUE == "off" ]] || [[ $STAKING_VALUE == "false" ]] || [[ $STAKING_VALUE == "0" ]]; then
|
||||
STAKING_ENABLED="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Parse buy bias parameter
|
||||
BUYBIAS_VALUE="50"
|
||||
if [[ $BUYBIAS == buybias=* ]]; then
|
||||
BUYBIAS_VALUE=${BUYBIAS#buybias=}
|
||||
fi
|
||||
|
||||
# Parse trades parameter
|
||||
TRADES_VALUE="15"
|
||||
if [[ $TRADES == trades=* ]]; then
|
||||
TRADES_VALUE=${TRADES#trades=}
|
||||
fi
|
||||
|
||||
# Parse staking bias parameter
|
||||
STAKINGBIAS_VALUE="80"
|
||||
if [[ $STAKINGBIAS == stakingbias=* ]]; then
|
||||
STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=}
|
||||
fi
|
||||
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
OUTPUT_DIR="fuzzing_results_improved_${OPTIMIZER}_${TIMESTAMP}"
|
||||
|
||||
echo -e "${GREEN}=== Improved Fuzzing Analysis ===${NC}"
|
||||
echo "Optimizer: $OPTIMIZER"
|
||||
echo "Total runs: $RUNS_VALUE"
|
||||
echo "Trades per run: $TRADES_VALUE"
|
||||
echo "Staking: $([ "$STAKING_ENABLED" = "true" ] && echo "enabled" || echo "disabled")"
|
||||
if [ "$STAKING_ENABLED" = "true" ]; then
|
||||
echo "Staking bias: $STAKINGBIAS_VALUE%"
|
||||
fi
|
||||
echo "Buy bias: $BUYBIAS_VALUE%"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Validate optimizer
|
||||
case $OPTIMIZER in
|
||||
BullMarketOptimizer|BearMarketOptimizer|NeutralMarketOptimizer|WhaleOptimizer|ExtremeOptimizer|MaliciousOptimizer)
|
||||
echo "Optimizer validation passed"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Invalid optimizer class${NC}"
|
||||
echo "Valid options: BullMarketOptimizer, BearMarketOptimizer, NeutralMarketOptimizer, WhaleOptimizer, ExtremeOptimizer, MaliciousOptimizer"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create output directory
|
||||
mkdir -p $OUTPUT_DIR
|
||||
|
||||
# Run the improved fuzzing with buy bias
|
||||
echo -e "${YELLOW}Starting improved fuzzing analysis with buy bias...${NC}"
|
||||
echo ""
|
||||
|
||||
FUZZING_RUNS=$RUNS_VALUE \
|
||||
OPTIMIZER_CLASS=$OPTIMIZER \
|
||||
ENABLE_STAKING=$STAKING_ENABLED \
|
||||
BUY_BIAS=$BUYBIAS_VALUE \
|
||||
TRADES_PER_RUN=$TRADES_VALUE \
|
||||
STAKING_BIAS=$STAKINGBIAS_VALUE \
|
||||
TRACK_POSITIONS=true \
|
||||
forge script analysis/ImprovedFuzzingAnalysis.s.sol:ImprovedFuzzingAnalysis --gas-limit 100000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log
|
||||
|
||||
# Extract key metrics
|
||||
echo ""
|
||||
echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}"
|
||||
|
||||
# Show summary from log
|
||||
tail -20 $OUTPUT_DIR/fuzzing.log | grep -E "Total scenarios|Profitable|Discovery|Stakes|Snatches" || true
|
||||
|
||||
# Check for position CSVs
|
||||
POSITION_CSV_COUNT=$(ls -1 improved_positions_*.csv 2>/dev/null | wc -l)
|
||||
|
||||
if [ $POSITION_CSV_COUNT -gt 0 ]; then
|
||||
# Move position CSVs to output directory
|
||||
mv improved_positions_*.csv $OUTPUT_DIR/
|
||||
echo ""
|
||||
echo -e "${GREEN}Position CSVs saved to: $OUTPUT_DIR/${NC}"
|
||||
|
||||
# Calculate max staking percentage
|
||||
echo ""
|
||||
echo -e "${YELLOW}=== Maximum Staking Level ===${NC}"
|
||||
for f in $OUTPUT_DIR/improved_positions_*.csv; do
|
||||
tail -1 "$f" | cut -d',' -f13
|
||||
done | sort -n | tail -1 | awk '{
|
||||
pct = $1/1e16
|
||||
printf "%.2f%% of authorized stake (%.2f%% of KRAIKEN supply)\n", pct, pct*0.2
|
||||
}'
|
||||
fi
|
||||
|
||||
# Check for snatching activity
|
||||
echo ""
|
||||
echo -e "${YELLOW}=== Snatching Activity ===${NC}"
|
||||
SNATCH_COUNT=$(grep -c "SNATCHED" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
|
||||
if [ -z "$SNATCH_COUNT" ]; then
|
||||
SNATCH_COUNT="0"
|
||||
fi
|
||||
if [ "$SNATCH_COUNT" -gt "0" ]; then
|
||||
echo -e "${GREEN}Snatching observed! Found $SNATCH_COUNT snatching events${NC}"
|
||||
grep "SNATCHED" $OUTPUT_DIR/fuzzing.log | head -5
|
||||
else
|
||||
echo "No snatching observed in this run"
|
||||
fi
|
||||
|
||||
# Check for profitable scenarios
|
||||
PROFITABLE_COUNT=$(grep -c "PROFITABLE!" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
|
||||
if [ -z "$PROFITABLE_COUNT" ]; then
|
||||
PROFITABLE_COUNT="0"
|
||||
fi
|
||||
if [ "$PROFITABLE_COUNT" -gt "0" ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}=== PROFITABLE SCENARIOS FOUND ===${NC}"
|
||||
echo "Found $PROFITABLE_COUNT profitable scenarios"
|
||||
grep "PROFITABLE!" $OUTPUT_DIR/fuzzing.log | head -5
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Full results saved to: $OUTPUT_DIR/${NC}"
|
||||
echo ""
|
||||
echo "To view detailed logs:"
|
||||
echo " cat $OUTPUT_DIR/fuzzing.log"
|
||||
echo ""
|
||||
echo "To visualize position movements (if CSVs generated):"
|
||||
echo " ./analysis/view-scenarios.sh $OUTPUT_DIR"
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 29926961b2ecfa89e0f61a6d874c71b6f8e29112
|
||||
Subproject commit 258ab89ee5da09a494d5721c18d27133ad844b63
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 89365b880c4f3c786bdd453d4b8e8fe410344a69
|
||||
Subproject commit c892309933b25c03d32b1b0d674df7ae292ba925
|
||||
Loading…
Add table
Add a link
Reference in a new issue