harb/onchain/analysis/StreamlinedFuzzing.s.sol

383 lines
15 KiB
Solidity
Raw Normal View History

2025-08-23 22:32:41 +02:00
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
2025-09-16 22:46:43 +02:00
import "forge-std/console2.sol";
2025-08-23 22:32:41 +02:00
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 {UniswapHelpers} from "../src/helpers/UniswapHelpers.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 {SwapExecutor} from "./helpers/SwapExecutor.sol";
import {Optimizer} from "../src/Optimizer.sol";
import {BullMarketOptimizer} from "../test/mocks/BullMarketOptimizer.sol";
import {BearMarketOptimizer} from "../test/mocks/BearMarketOptimizer.sol";
import {NeutralMarketOptimizer} from "../test/mocks/NeutralMarketOptimizer.sol";
import {WhaleOptimizer} from "../test/mocks/WhaleOptimizer.sol";
import {ExtremeOptimizer} from "../test/mocks/ExtremeOptimizer.sol";
import {MaliciousOptimizer} from "../test/mocks/MaliciousOptimizer.sol";
contract StreamlinedFuzzing is Script {
// Test environment
TestEnvironment testEnv;
IUniswapV3Factory factory;
IUniswapV3Pool pool;
IWETH9 weth;
Kraiken kraiken;
Stake stake;
LiquidityManager lm;
SwapExecutor swapExecutor;
bool token0isWeth;
// Actors
address trader = makeAddr("trader");
address fees = makeAddr("fees");
// Staking tracking
mapping(address => uint256[]) public activePositions;
uint256[] public allPositionIds;
uint256 totalStakesAttempted;
uint256 totalStakesSucceeded;
uint256 totalSnatchesAttempted;
uint256 totalSnatchesSucceeded;
// CSV filename for current run
string csvFilename;
// Track cumulative fees
uint256 totalFees0;
uint256 totalFees1;
// Track recentering
uint256 lastRecenterBlock;
function run() public {
// Get configuration from environment
uint256 numRuns = vm.envOr("FUZZING_RUNS", uint256(20));
uint256 tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15));
bool enableStaking = vm.envOr("ENABLE_STAKING", true);
uint256 buyBias = vm.envOr("BUY_BIAS", uint256(50));
uint256 stakingBias = vm.envOr("STAKING_BIAS", uint256(80));
string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
2025-09-16 22:46:43 +02:00
console2.log("=== Streamlined Fuzzing Analysis ===");
console2.log("Optimizer:", optimizerClass);
2025-08-23 22:32:41 +02:00
// Deploy factory once for all runs (gas optimization)
testEnv = new TestEnvironment(fees);
factory = UniswapHelpers.deployUniswapFactory();
// Generate unique 4-character scenario ID
string memory scenarioCode = _generateScenarioId();
// Run fuzzing scenarios
for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) {
string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3)));
2025-09-16 22:46:43 +02:00
console2.log("\nRun:", runId);
2025-08-23 22:32:41 +02:00
// Initialize CSV file for this run
// Always write to analysis directory relative to project root
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\n";
vm.writeFile(csvFilename, header);
// Setup fresh environment for each run
_setupEnvironment(optimizerClass, runIndex % 2 == 0);
// Reset tracking variables
totalFees0 = 0;
totalFees1 = 0;
lastRecenterBlock = block.number;
2025-09-16 22:46:43 +02:00
// Fund trader based on run seed - increased for longer campaigns
uint256 traderFund = 500 ether + (uint256(keccak256(abi.encodePacked(runIndex, "trader"))) % 500 ether);
2025-08-23 22:32:41 +02:00
vm.deal(trader, traderFund * 2);
vm.prank(trader);
weth.deposit{value: traderFund}();
// Initial state
_recordState("INIT", 0);
// Execute trades
for (uint256 i = 0; i < tradesPerRun; i++) {
2025-08-24 18:38:48 +02:00
// Check for recenter opportunity on average every 3 trades
uint256 recenterRand = uint256(keccak256(abi.encodePacked(runIndex, i, "recenter"))) % 3;
if (recenterRand == 0) {
2025-08-23 22:32:41 +02:00
_tryRecenter();
}
// Determine trade based on bias
uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, i))) % 100;
if (rand < buyBias) {
_executeBuy(runIndex, i);
} else {
_executeSell(runIndex, i);
}
// Staking operations if enabled
if (enableStaking && i % 3 == 0) {
_executeStakingOperation(runIndex, i, stakingBias);
}
}
2025-09-16 22:46:43 +02:00
2025-08-23 22:32:41 +02:00
// Final state
2025-09-16 22:46:43 +02:00
_liquidateTraderHoldings();
2025-08-23 22:32:41 +02:00
_recordState("FINAL", 0);
}
2025-09-16 22:46:43 +02:00
console2.log("\n=== Analysis Complete ===");
console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
2025-08-23 22:32:41 +02:00
}
function _setupEnvironment(string memory optimizerClass, bool wethIsToken0) internal {
// Get optimizer address
address optimizer = _deployOptimizer(optimizerClass);
// Setup new environment
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
// Deploy swap executor with liquidity boundary checks
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
// Fund liquidity manager
vm.deal(address(lm), 200 ether);
// Initialize liquidity positions
// First need to give LM some WETH
vm.prank(address(lm));
weth.deposit{value: 100 ether}();
// Now try recenter from fee destination
vm.prank(fees);
try lm.recenter() returns (bool isUp) {
2025-09-16 22:46:43 +02:00
console2.log("Initial recenter successful, isUp:", isUp);
2025-08-23 22:32:41 +02:00
} catch Error(string memory reason) {
2025-09-16 22:46:43 +02:00
console2.log("Initial recenter failed:", reason);
2025-08-23 22:32:41 +02:00
} catch {
2025-09-16 22:46:43 +02:00
console2.log("Initial recenter failed with unknown error");
2025-08-23 22:32:41 +02:00
}
// Clear staking state
delete allPositionIds;
totalStakesAttempted = 0;
totalStakesSucceeded = 0;
totalSnatchesAttempted = 0;
totalSnatchesSucceeded = 0;
}
function _deployOptimizer(string memory optimizerClass) internal returns (address) {
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 {
// Default to bull market
return address(new BullMarketOptimizer());
}
}
function _executeBuy(uint256 runIndex, uint256 tradeIndex) internal {
uint256 amount = _getTradeAmount(runIndex, tradeIndex, true);
if (amount == 0 || weth.balanceOf(trader) < amount) return;
vm.startPrank(trader);
weth.transfer(address(swapExecutor), amount);
try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
if (actualAmount == 0) {
2025-09-16 22:46:43 +02:00
console2.log("Buy returned 0, requested:", amount);
2025-08-23 22:32:41 +02:00
}
_recordState("BUY", actualAmount);
} catch Error(string memory reason) {
2025-09-16 22:46:43 +02:00
console2.log("Buy failed:", reason);
2025-08-23 22:32:41 +02:00
_recordState("BUY_FAIL", amount);
}
vm.stopPrank();
}
function _executeSell(uint256 runIndex, uint256 tradeIndex) internal {
uint256 amount = _getTradeAmount(runIndex, tradeIndex, false);
if (amount == 0 || kraiken.balanceOf(trader) < amount) return;
vm.startPrank(trader);
kraiken.transfer(address(swapExecutor), amount);
try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) {
_recordState("SELL", actualAmount);
} catch {
_recordState("SELL_FAIL", amount);
}
vm.stopPrank();
}
function _executeStakingOperation(uint256, uint256, uint256) internal {
// Staking operations disabled for now - interface needs updating
// TODO: Update to use correct Stake contract interface
}
function _tryRecenter() internal {
2025-08-24 18:38:48 +02:00
vm.warp(block.timestamp + 1 hours);
vm.roll(block.number + 1); // Advance block
vm.prank(fees);
try lm.recenter{gas: 50_000_000}() {
lastRecenterBlock = block.number;
_recordState("RECENTER", 0);
} catch {}
2025-08-23 22:32:41 +02:00
}
function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal pure returns (uint256) {
2025-09-16 22:46:43 +02:00
uint256 baseAmount = 10 ether + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex))) % 90 ether);
2025-08-23 22:32:41 +02:00
return isBuy ? baseAmount : baseAmount * 1000;
}
2025-09-16 22:46:43 +02:00
function _liquidateTraderHoldings() internal {
uint256 remaining = kraiken.balanceOf(trader);
uint256 attempts;
// Repeatedly sell down inventory, respecting liquidity limits in SwapExecutor
while (remaining > 0 && attempts < 10) {
uint256 prevRemaining = remaining;
vm.startPrank(trader);
kraiken.transfer(address(swapExecutor), remaining);
try swapExecutor.executeSell(remaining, trader) returns (uint256 actualAmount) {
if (actualAmount == 0) {
vm.stopPrank();
console2.log("Liquidity liquidation halted: sell returned 0");
break;
}
} catch Error(string memory reason) {
vm.stopPrank();
console2.log("Liquidity liquidation failed:", reason);
break;
} catch {
vm.stopPrank();
console2.log("Liquidity liquidation failed with unknown error");
break;
}
vm.stopPrank();
remaining = kraiken.balanceOf(trader);
if (remaining >= prevRemaining) {
console2.log("Liquidity liquidation made no progress; remaining KRAIKEN:", remaining);
break;
}
unchecked {
attempts++;
}
}
if (kraiken.balanceOf(trader) > 0) {
console2.log("Warning: trader still holds KRAIKEN after liquidation:", kraiken.balanceOf(trader));
}
}
2025-08-23 22:32:41 +02:00
function _recordState(string memory action, uint256 amount) internal {
// Build CSV row in parts to avoid stack too deep
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();
// Get floor position
(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) {
// Get anchor and discovery positions
(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) {
// Get balances and fees
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;
return string(abi.encodePacked(
vm.toString(ethBalance), ",",
vm.toString(kraikenBalance), ",",
"0,", // vwap placeholder
vm.toString(deltaFees0), ",",
vm.toString(deltaFees1), ",",
"0" // recenter flag placeholder - no newline here
));
}
function _generateScenarioId() internal view returns (string memory) {
uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)));
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);
}
function _padNumber(uint256 num, uint256 digits) internal pure returns (string memory) {
string memory numStr = vm.toString(num);
bytes memory numBytes = bytes(numStr);
if (numBytes.length >= digits) {
return numStr;
}
bytes memory result = new bytes(digits);
uint256 padding = digits - numBytes.length;
for (uint256 i = 0; i < padding; i++) {
result[i] = '0';
}
for (uint256 i = 0; i < numBytes.length; i++) {
result[padding + i] = numBytes[i];
}
return string(result);
}
2025-09-16 22:46:43 +02:00
}