harb/onchain/analysis/StreamlinedFuzzing.s.sol
openhands e925538309 fix: Remove dead Optimizer V2/V3 — Push3 is the active optimizer (#312)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 19:37:12 +00:00

376 lines
16 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { Optimizer } from "../src/Optimizer.sol";
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol";
import { BullMarketOptimizer } from "../test/mocks/BullMarketOptimizer.sol";
import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol";
import { ExtremeOptimizer } from "../test/mocks/ExtremeOptimizer.sol";
import { MaliciousOptimizer } from "../test/mocks/MaliciousOptimizer.sol";
import { NeutralMarketOptimizer } from "../test/mocks/NeutralMarketOptimizer.sol";
import { WhaleOptimizer } from "../test/mocks/WhaleOptimizer.sol";
import { FuzzingBase } from "./helpers/FuzzingBase.sol";
import "forge-std/console2.sol";
/// @title StreamlinedFuzzing
/// @notice Per-run CSV fuzzer: deploys a named optimizer, runs N randomized
/// trade sequences, and writes one CSV per run with full position snapshots.
/// Supports ConfigurableOptimizer (env-driven params), uncapped swaps,
/// and random stake/unstake actions interleaved with trades.
contract StreamlinedFuzzing is FuzzingBase {
// CSV filename for current run
string csvFilename;
// Track cumulative fees
uint256 totalFees0;
uint256 totalFees1;
// Track recentering
uint256 lastRecenterBlock;
uint256 recenterCount;
// Staking position tracking (max 10 active positions per run)
uint256[10] activePositions;
uint256 numActivePositions;
// Batch seed for multi-batch uniqueness
uint256 bSeed;
function run() public {
uint256 numRuns = vm.envOr("FUZZING_RUNS", uint256(20));
uint256 tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15));
uint256 buyBias = vm.envOr("BUY_BIAS", uint256(50));
string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
bool uncapped = vm.envOr("UNCAPPED_SWAPS", false);
bSeed = vm.envOr("BATCH_SEED", uint256(0));
uint256 bgLpEthPerLayer = vm.envOr("BG_LP_ETH_PER_LAYER", uint256(0));
uint256 stakingLevel = vm.envOr("STAKING_LEVEL", uint256(0)); // 0-100%
uint256 stakingTaxRate = vm.envOr("STAKING_TAX_RATE", uint256(3)); // tax rate index 0-29
console2.log("=== Streamlined Fuzzing Analysis ===");
console2.log("Optimizer:", optimizerClass);
console2.log("Uncapped:", uncapped);
if (bgLpEthPerLayer > 0) {
console2.log("Background LP: ", bgLpEthPerLayer / 1e18, "ETH/layer x5");
}
if (stakingLevel > 0) {
console2.log("Staking level: ", stakingLevel, "% at tax rate index", stakingTaxRate);
}
_initInfrastructure();
string memory scenarioCode = _generateScenarioId(bSeed);
for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) {
string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3)));
console2.log("\nRun:", runId);
csvFilename = string(abi.encodePacked("analysis/fuzz-", runId, ".csv"));
string memory header =
"action,amount,tick,floor_lower,floor_upper,floor_liq,anchor_lower,anchor_upper,anchor_liq,discovery_lower,discovery_upper,discovery_liq,eth_balance,kraiken_balance,vwap,fees_eth,fees_kraiken,recenter,fee_dest_weth,fee_dest_krk\n";
vm.writeFile(csvFilename, header);
// Deploy fresh environment per run
address optimizer = _deployOptimizer(optimizerClass);
_setupEnvironment(optimizer, runIndex % 2 == 0, uncapped);
// Deploy background LP if configured
if (bgLpEthPerLayer > 0) {
_deployBackgroundLP(bgLpEthPerLayer);
}
// Pre-seed staking to set initial sentiment
if (stakingLevel > 0) {
_seedStaking(stakingLevel, uint32(stakingTaxRate));
}
// Reset tracking
totalFees0 = 0;
totalFees1 = 0;
lastRecenterBlock = block.number;
recenterCount = 0;
numActivePositions = 0;
// Fund trader
uint256 traderFund = 500 ether + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, "trader"))) % 500 ether);
vm.deal(trader, traderFund * 2);
vm.prank(trader);
weth.deposit{ value: traderFund }();
_recordState("INIT", 0);
for (uint256 i = 0; i < tradesPerRun; i++) {
// Recenter on average every 3 trades
uint256 recenterRand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i, "recenter"))) % 3;
if (recenterRand == 0) {
if (_tryRecenter()) {
lastRecenterBlock = block.number;
recenterCount++;
// Rebalance background LP every 10 recenters to track market
// (every recenter is too expensive for 2000-trade runs)
if (bgLpEthPerLayer > 0 && recenterCount % 10 == 0) {
_rebalanceBackgroundLP(bgLpEthPerLayer);
}
_recordState("RECENTER", 0);
}
}
// 10% chance of staking action instead of trade
uint256 stakeRand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i, "stake"))) % 100;
if (stakeRand < 10) {
_runStakingAction(runIndex, i);
continue;
}
uint256 rand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i))) % 100;
if (rand < buyBias) {
_runBuy(runIndex, i);
} else {
_runSell(runIndex, i);
}
}
// Exit all staking positions before liquidation
_exitAllPositions();
_liquidateTraderHoldings();
_recordState("FINAL", 0);
// Log fee revenue
(uint256 feeWeth, uint256 feeKrk) = _getFeeRevenue();
console2.log("Fee revenue: WETH=", feeWeth / 1e18, "KRK=", feeKrk / 1e18);
}
console2.log("\n=== Analysis Complete ===");
console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
}
function _runBuy(uint256 runIndex, uint256 tradeIndex) internal {
uint256 amount = _getTradeAmount(runIndex, tradeIndex, true);
if (_executeBuy(amount)) {
_recordState("BUY", amount);
} else {
_recordState("BUY_FAIL", amount);
}
}
function _runSell(uint256 runIndex, uint256 tradeIndex) internal {
uint256 amount = _getTradeAmount(runIndex, tradeIndex, false);
if (_executeSell(amount)) {
_recordState("SELL", amount);
} else {
_recordState("SELL_FAIL", amount);
}
}
function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal view returns (uint256) {
uint256 baseAmount = 10 ether + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex))) % 90 ether);
return isBuy ? baseAmount : baseAmount * 1000;
}
// ── Staking Actions ──────────────────────────────────────────────────
function _runStakingAction(uint256 runIndex, uint256 tradeIndex) internal {
// If we have active positions, 50/50 stake vs unstake; otherwise always stake
bool doStake = numActivePositions == 0 || uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "stakeOrUnstake"))) % 2 == 0;
if (doStake) {
_tryStake(runIndex, tradeIndex);
} else {
_tryUnstake(runIndex, tradeIndex);
}
}
function _tryStake(uint256 runIndex, uint256 tradeIndex) internal {
uint256 krkBalance = kraiken.balanceOf(trader);
if (krkBalance == 0 || numActivePositions >= 10) {
_recordState("STAKE_SKIP", 0);
return;
}
// Stake 5-20% of KRK holdings
uint256 pct = 5 + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "stakePct"))) % 16);
uint256 stakeAmount = krkBalance * pct / 100;
// Check minimum stake
try kraiken.minStake() returns (uint256 minStake) {
if (stakeAmount < minStake) stakeAmount = minStake;
if (stakeAmount > krkBalance) {
_recordState("STAKE_SKIP", 0);
return;
}
} catch {
_recordState("STAKE_SKIP", 0);
return;
}
// Random tax rate index (0-29), bias toward low rates
uint32 taxRate = uint32(uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "taxRate"))) % 10);
vm.startPrank(trader);
kraiken.approve(address(stake), stakeAmount);
uint256[] memory empty = new uint256[](0);
try stake.snatch(stakeAmount, trader, taxRate, empty) returns (uint256 positionId) {
vm.stopPrank();
activePositions[numActivePositions] = positionId;
numActivePositions++;
_recordState("STAKE", stakeAmount);
} catch {
vm.stopPrank();
_recordState("STAKE_FAIL", stakeAmount);
}
}
function _tryUnstake(uint256 runIndex, uint256 tradeIndex) internal {
if (numActivePositions == 0) return;
// Pick a random position to exit
uint256 idx = uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "unstakeIdx"))) % numActivePositions;
uint256 positionId = activePositions[idx];
// Advance time so we pass the 3-day tax floor
vm.warp(block.timestamp + 4 days);
vm.prank(trader);
try stake.exitPosition(positionId) {
// Remove from tracking by swapping with last
activePositions[idx] = activePositions[numActivePositions - 1];
numActivePositions--;
_recordState("UNSTAKE", positionId);
} catch {
_recordState("UNSTAKE_FAIL", positionId);
}
}
function _exitAllPositions() internal {
for (uint256 i = 0; i < numActivePositions; i++) {
vm.warp(block.timestamp + 4 days);
vm.prank(trader);
try stake.exitPosition(activePositions[i]) { } catch { }
}
numActivePositions = 0;
}
function _generateScenarioId(uint256 seed) internal view returns (string memory) {
uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, seed)));
bytes memory chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
bytes memory result = new bytes(4);
for (uint256 i = 0; i < 4; i++) {
result[i] = chars[rand % chars.length];
rand = rand / chars.length;
}
return string(result);
}
// ── Optimizer Deployment ─────────────────────────────────────────────
function _deployOptimizer(string memory optimizerClass) internal returns (address) {
if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ConfigurableOptimizer"))) {
uint256 ci = vm.envOr("CI_VALUE", uint256(0));
uint256 as_ = vm.envOr("AS_VALUE", uint256(100_000_000_000_000_000));
uint256 aw = vm.envOr("AW_VALUE", uint256(20));
uint256 dd = vm.envOr("DD_VALUE", uint256(500_000_000_000_000_000));
return address(new ConfigurableOptimizer(ci, as_, uint24(aw > 100 ? 100 : aw), dd));
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BullMarketOptimizer"))) {
return address(new BullMarketOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BearMarketOptimizer"))) {
return address(new BearMarketOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("NeutralMarketOptimizer"))) {
return address(new NeutralMarketOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("WhaleOptimizer"))) {
return address(new WhaleOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ExtremeOptimizer"))) {
return address(new ExtremeOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) {
return address(new MaliciousOptimizer());
} else {
return address(new BullMarketOptimizer());
}
}
// ── CSV Recording ─────────────────────────────────────────────────────
function _recordState(string memory action, uint256 amount) internal {
string memory row = _buildRowPart1(action, amount);
row = string(abi.encodePacked(row, _buildRowPart2()));
row = string(abi.encodePacked(row, _buildRowPart3()));
vm.writeLine(csvFilename, row);
}
function _buildRowPart1(string memory action, uint256 amount) internal view returns (string memory) {
(, int24 tick,,,,,) = pool.slot0();
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
return string(
abi.encodePacked(
action,
",",
vm.toString(amount),
",",
vm.toString(tick),
",",
vm.toString(floorLower),
",",
vm.toString(floorUpper),
",",
vm.toString(uint256(floorLiq)),
","
)
);
}
function _buildRowPart2() internal view returns (string memory) {
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
return string(
abi.encodePacked(
vm.toString(anchorLower),
",",
vm.toString(anchorUpper),
",",
vm.toString(uint256(anchorLiq)),
",",
vm.toString(discoveryLower),
",",
vm.toString(discoveryUpper),
",",
vm.toString(uint256(discoveryLiq)),
","
)
);
}
function _buildRowPart3() internal view returns (string memory) {
uint256 ethBalance = weth.balanceOf(trader);
uint256 kraikenBalance = kraiken.balanceOf(trader);
(uint128 fees0, uint128 fees1) = pool.protocolFees();
uint256 deltaFees0 = fees0 > totalFees0 ? fees0 - totalFees0 : 0;
uint256 deltaFees1 = fees1 > totalFees1 ? fees1 - totalFees1 : 0;
// Fee destination balances (LM fee revenue)
(uint256 feeDestWeth, uint256 feeDestKrk) = _getFeeRevenue();
return string(
abi.encodePacked(
vm.toString(ethBalance),
",",
vm.toString(kraikenBalance),
",",
"0,",
vm.toString(deltaFees0),
",",
vm.toString(deltaFees1),
",",
"0,",
vm.toString(feeDestWeth),
",",
vm.toString(feeDestKrk)
)
);
}
}