2025-08-23 22:32:41 +02:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
import { Kraiken } from "../src/Kraiken.sol";
|
|
|
|
|
|
|
|
|
|
import { LiquidityManager } from "../src/LiquidityManager.sol";
|
|
|
|
|
|
|
|
|
|
import { Optimizer } from "../src/Optimizer.sol";
|
|
|
|
|
import { Stake } from "../src/Stake.sol";
|
|
|
|
|
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
|
|
|
|
|
import { UniswapHelpers } from "../src/helpers/UniswapHelpers.sol";
|
|
|
|
|
import { IWETH9 } from "../src/interfaces/IWETH9.sol";
|
|
|
|
|
import { TestEnvironment } from "../test/helpers/TestBase.sol";
|
|
|
|
|
|
|
|
|
|
import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol";
|
|
|
|
|
import { BullMarketOptimizer } from "../test/mocks/BullMarketOptimizer.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 { SwapExecutor } from "./helpers/SwapExecutor.sol";
|
|
|
|
|
import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|
|
|
|
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
2025-08-23 22:32:41 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
contract StreamlinedFuzzing is Script {
|
|
|
|
|
// Test environment
|
|
|
|
|
TestEnvironment testEnv;
|
|
|
|
|
IUniswapV3Factory factory;
|
|
|
|
|
IUniswapV3Pool pool;
|
|
|
|
|
IWETH9 weth;
|
|
|
|
|
Kraiken kraiken;
|
|
|
|
|
Stake stake;
|
|
|
|
|
LiquidityManager lm;
|
|
|
|
|
SwapExecutor swapExecutor;
|
|
|
|
|
bool token0isWeth;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Actors
|
|
|
|
|
address trader = makeAddr("trader");
|
2026-02-04 20:58:30 +00:00
|
|
|
address staker = makeAddr("staker");
|
2025-08-23 22:32:41 +02:00
|
|
|
address fees = makeAddr("fees");
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Staking tracking
|
2026-02-04 20:58:30 +00:00
|
|
|
uint256[] public activePositionIds;
|
2025-08-23 22:32:41 +02:00
|
|
|
uint256 totalStakesAttempted;
|
|
|
|
|
uint256 totalStakesSucceeded;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// CSV filename for current run
|
|
|
|
|
string csvFilename;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Track cumulative fees
|
|
|
|
|
uint256 totalFees0;
|
|
|
|
|
uint256 totalFees1;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Track recentering
|
|
|
|
|
uint256 lastRecenterBlock;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
// Config
|
|
|
|
|
uint256 cfgBuyBias;
|
|
|
|
|
uint256 cfgMinBuy;
|
|
|
|
|
uint256 cfgMaxBuy;
|
|
|
|
|
bool cfgUncapped;
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
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);
|
2026-02-04 20:58:30 +00:00
|
|
|
cfgBuyBias = vm.envOr("BUY_BIAS", uint256(50));
|
2025-08-23 22:32:41 +02:00
|
|
|
uint256 stakingBias = vm.envOr("STAKING_BIAS", uint256(80));
|
|
|
|
|
string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
|
2026-02-04 20:58:30 +00:00
|
|
|
cfgUncapped = vm.envOr("UNCAPPED_SWAPS", true);
|
|
|
|
|
cfgMinBuy = vm.envOr("MIN_BUY_ETH", uint256(20));
|
|
|
|
|
cfgMaxBuy = vm.envOr("MAX_BUY_ETH", uint256(80));
|
|
|
|
|
|
2025-09-16 22:46:43 +02:00
|
|
|
console2.log("=== Streamlined Fuzzing Analysis ===");
|
|
|
|
|
console2.log("Optimizer:", optimizerClass);
|
2026-02-04 20:58:30 +00:00
|
|
|
console2.log("Uncapped swaps:", cfgUncapped);
|
|
|
|
|
console2.log("Trade range (ETH):", cfgMinBuy, cfgMaxBuy);
|
|
|
|
|
|
|
|
|
|
// Deploy factory once
|
2025-08-23 22:32:41 +02:00
|
|
|
testEnv = new TestEnvironment(fees);
|
|
|
|
|
factory = UniswapHelpers.deployUniswapFactory();
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Generate unique 4-character scenario ID
|
|
|
|
|
string memory scenarioCode = _generateScenarioId();
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
// Setup environment ONCE — all runs share it so VWAP accumulates
|
|
|
|
|
_setupEnvironment(optimizerClass, true);
|
|
|
|
|
|
|
|
|
|
// Track LM ETH across the entire session
|
|
|
|
|
uint256 lmEthStart = address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool));
|
|
|
|
|
console2.log("LM starting ETH:", lmEthStart / 1e18, "ETH");
|
|
|
|
|
|
|
|
|
|
// Run fuzzing scenarios — same environment, VWAP carries over
|
2025-08-23 22:32:41 +02:00
|
|
|
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);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Initialize CSV file for this run
|
|
|
|
|
csvFilename = string(abi.encodePacked("analysis/fuzz-", runId, ".csv"));
|
2026-02-04 20:58:30 +00:00
|
|
|
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";
|
2025-08-23 22:32:41 +02:00
|
|
|
vm.writeFile(csvFilename, header);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
// Reset tracking for CSV
|
2025-08-23 22:32:41 +02:00
|
|
|
totalFees0 = 0;
|
|
|
|
|
totalFees1 = 0;
|
|
|
|
|
lastRecenterBlock = block.number;
|
2025-09-16 22:46:43 +02:00
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
// Reset trader — burn any leftover WETH/ETH from previous run
|
|
|
|
|
uint256 leftoverWeth = weth.balanceOf(trader);
|
|
|
|
|
if (leftoverWeth > 0) {
|
|
|
|
|
vm.prank(trader);
|
|
|
|
|
weth.transfer(address(0xdead), leftoverWeth);
|
|
|
|
|
}
|
|
|
|
|
uint256 leftoverKraiken = kraiken.balanceOf(trader);
|
|
|
|
|
if (leftoverKraiken > 0) {
|
|
|
|
|
vm.prank(trader);
|
|
|
|
|
kraiken.transfer(address(0xdead), leftoverKraiken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fund trader fresh for this run
|
|
|
|
|
uint256 traderFund = 200 ether;
|
|
|
|
|
vm.deal(trader, traderFund);
|
2025-08-23 22:32:41 +02:00
|
|
|
vm.prank(trader);
|
2026-02-04 20:58:30 +00:00
|
|
|
weth.deposit{ value: traderFund }();
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Initial state
|
|
|
|
|
_recordState("INIT", 0);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// 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();
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Determine trade based on bias
|
|
|
|
|
uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, i))) % 100;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
if (rand < cfgBuyBias) {
|
2025-08-23 22:32:41 +02:00
|
|
|
_executeBuy(runIndex, i);
|
|
|
|
|
} else {
|
|
|
|
|
_executeSell(runIndex, i);
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Staking operations if enabled
|
2026-02-04 20:58:30 +00:00
|
|
|
if (enableStaking && i % 5 == 0) {
|
2025-08-23 22:32:41 +02:00
|
|
|
_executeStakingOperation(runIndex, i, stakingBias);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-16 22:46:43 +02:00
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
// Final recenter + liquidate
|
|
|
|
|
_tryRecenter();
|
2025-09-16 22:46:43 +02:00
|
|
|
_liquidateTraderHoldings();
|
2025-08-23 22:32:41 +02:00
|
|
|
_recordState("FINAL", 0);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
// Report per-run PnL
|
|
|
|
|
uint256 traderEthNow = weth.balanceOf(trader);
|
|
|
|
|
uint256 lmEthNow = address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool));
|
|
|
|
|
if (traderEthNow > traderFund) {
|
|
|
|
|
console2.log(" TRADER PROFIT:", (traderEthNow - traderFund) / 1e15, "finney");
|
|
|
|
|
}
|
|
|
|
|
console2.log(" LM ETH (wei):", lmEthNow, _signedDelta(lmEthNow, lmEthStart));
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2025-09-16 22:46:43 +02:00
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
uint256 lmEthEnd = address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool));
|
2025-09-16 22:46:43 +02:00
|
|
|
console2.log("\n=== Analysis Complete ===");
|
2026-02-04 20:58:30 +00:00
|
|
|
console2.log("LM ETH start:", lmEthStart / 1e18, "final (ETH):", lmEthEnd / 1e18);
|
|
|
|
|
if (lmEthEnd < lmEthStart) {
|
|
|
|
|
console2.log("LM LOST ETH:", (lmEthStart - lmEthEnd) / 1e15, "finney");
|
|
|
|
|
}
|
2025-09-16 22:46:43 +02:00
|
|
|
console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
function _signedDelta(uint256 current, uint256 start) internal pure returns (string memory) {
|
|
|
|
|
if (current >= start) {
|
|
|
|
|
return string(abi.encodePacked("+", vm.toString((current - start) / 1e15), " finney"));
|
|
|
|
|
} else {
|
|
|
|
|
return string(abi.encodePacked("-", vm.toString((start - current) / 1e15), " finney"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _setupEnvironment(string memory optimizerClass, bool wethIsToken0) internal {
|
|
|
|
|
address optimizer = _deployOptimizer(optimizerClass);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
|
|
|
|
|
|
|
|
|
|
// Deploy swap executor — uncapped by default for exploit testing
|
|
|
|
|
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, cfgUncapped);
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
// Fund liquidity manager
|
|
|
|
|
vm.deal(address(lm), 200 ether);
|
|
|
|
|
vm.prank(address(lm));
|
2026-02-04 20:58:30 +00:00
|
|
|
weth.deposit{ value: 100 ether }();
|
|
|
|
|
|
|
|
|
|
// Initial recenter to set positions
|
2025-08-23 22:32:41 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
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 {
|
|
|
|
|
return address(new BullMarketOptimizer());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _executeBuy(uint256 runIndex, uint256 tradeIndex) internal {
|
2026-02-04 20:58:30 +00:00
|
|
|
uint256 range = cfgMaxBuy - cfgMinBuy;
|
|
|
|
|
uint256 amount = (cfgMinBuy * 1 ether) + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex, "buy"))) % (range * 1 ether));
|
2025-08-23 22:32:41 +02:00
|
|
|
if (amount == 0 || weth.balanceOf(trader) < amount) return;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
vm.startPrank(trader);
|
|
|
|
|
weth.transfer(address(swapExecutor), amount);
|
|
|
|
|
try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
|
|
|
|
|
if (actualAmount == 0) {
|
2026-02-04 20:58:30 +00:00
|
|
|
console2.log("Buy returned 0, requested:", amount / 1e18);
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
|
|
|
|
_recordState("BUY", actualAmount);
|
2026-02-04 20:58:30 +00:00
|
|
|
} catch {
|
2025-08-23 22:32:41 +02:00
|
|
|
_recordState("BUY_FAIL", amount);
|
|
|
|
|
}
|
|
|
|
|
vm.stopPrank();
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _executeSell(uint256 runIndex, uint256 tradeIndex) internal {
|
2026-02-04 20:58:30 +00:00
|
|
|
uint256 kraikenBal = kraiken.balanceOf(trader);
|
|
|
|
|
if (kraikenBal == 0) return;
|
|
|
|
|
|
|
|
|
|
// Sell 20-80% of holdings
|
|
|
|
|
uint256 pct = 20 + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex, "sell"))) % 60);
|
|
|
|
|
uint256 amount = kraikenBal * pct / 100;
|
|
|
|
|
if (amount == 0) return;
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
function _executeStakingOperation(uint256 runIndex, uint256 tradeIndex, uint256 stakingBias) internal {
|
|
|
|
|
// Need KRAIKEN to stake — use the staker address, not the trader
|
|
|
|
|
uint256 stakerKraiken = kraiken.balanceOf(staker);
|
|
|
|
|
|
|
|
|
|
// If staker has no KRAIKEN and trader has some, transfer a small amount
|
|
|
|
|
if (stakerKraiken == 0) {
|
|
|
|
|
uint256 traderBal = kraiken.balanceOf(trader);
|
|
|
|
|
if (traderBal == 0) return;
|
|
|
|
|
uint256 toTransfer = traderBal / 10; // 10% of trader holdings
|
|
|
|
|
if (toTransfer == 0) return;
|
|
|
|
|
vm.prank(trader);
|
|
|
|
|
kraiken.transfer(staker, toTransfer);
|
|
|
|
|
stakerKraiken = toTransfer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, tradeIndex, "staking"))) % 100;
|
|
|
|
|
|
|
|
|
|
if (rand < stakingBias && activePositionIds.length == 0) {
|
|
|
|
|
// Stake: pick a tax rate (0-15 range, modest)
|
|
|
|
|
uint32 taxRate = uint32(uint256(keccak256(abi.encodePacked(runIndex, tradeIndex, "taxrate"))) % 16);
|
|
|
|
|
uint256 stakeAmount = stakerKraiken / 2;
|
|
|
|
|
|
|
|
|
|
// Check minStake
|
|
|
|
|
try kraiken.minStake() returns (uint256 minStake) {
|
|
|
|
|
if (stakeAmount < minStake) {
|
|
|
|
|
if (stakerKraiken >= minStake) {
|
|
|
|
|
stakeAmount = minStake;
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalStakesAttempted++;
|
|
|
|
|
vm.startPrank(staker);
|
|
|
|
|
kraiken.approve(address(stake), stakeAmount);
|
|
|
|
|
uint256[] memory empty = new uint256[](0);
|
|
|
|
|
try stake.snatch(stakeAmount, staker, taxRate, empty) returns (uint256 positionId) {
|
|
|
|
|
activePositionIds.push(positionId);
|
|
|
|
|
totalStakesSucceeded++;
|
|
|
|
|
} catch { }
|
|
|
|
|
vm.stopPrank();
|
|
|
|
|
} else if (activePositionIds.length > 0) {
|
|
|
|
|
// Exit a random position
|
|
|
|
|
uint256 idx = uint256(keccak256(abi.encodePacked(runIndex, tradeIndex, "exit"))) % activePositionIds.length;
|
|
|
|
|
uint256 posId = activePositionIds[idx];
|
|
|
|
|
vm.prank(staker);
|
|
|
|
|
try stake.exitPosition(posId) {
|
|
|
|
|
// Remove from tracking
|
|
|
|
|
activePositionIds[idx] = activePositionIds[activePositionIds.length - 1];
|
|
|
|
|
activePositionIds.pop();
|
|
|
|
|
} catch { }
|
|
|
|
|
}
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _tryRecenter() internal {
|
2025-08-24 18:38:48 +02:00
|
|
|
vm.warp(block.timestamp + 1 hours);
|
2026-02-04 20:58:30 +00:00
|
|
|
vm.roll(block.number + 1);
|
2025-08-24 18:38:48 +02:00
|
|
|
vm.prank(fees);
|
2026-02-04 20:58:30 +00:00
|
|
|
try lm.recenter{ gas: 50_000_000 }() {
|
2025-08-24 18:38:48 +02:00
|
|
|
lastRecenterBlock = block.number;
|
|
|
|
|
_recordState("RECENTER", 0);
|
2026-02-04 20:58:30 +00:00
|
|
|
} catch { }
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2025-09-16 22:46:43 +02:00
|
|
|
|
|
|
|
|
function _liquidateTraderHoldings() internal {
|
|
|
|
|
uint256 remaining = kraiken.balanceOf(trader);
|
|
|
|
|
uint256 attempts;
|
|
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
while (remaining > 0 && attempts < 20) {
|
2025-09-16 22:46:43 +02:00
|
|
|
uint256 prevRemaining = remaining;
|
|
|
|
|
|
|
|
|
|
vm.startPrank(trader);
|
|
|
|
|
kraiken.transfer(address(swapExecutor), remaining);
|
|
|
|
|
try swapExecutor.executeSell(remaining, trader) returns (uint256 actualAmount) {
|
|
|
|
|
if (actualAmount == 0) {
|
|
|
|
|
vm.stopPrank();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
vm.stopPrank();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
vm.stopPrank();
|
|
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
// Recenter between liquidation attempts to unlock more liquidity
|
|
|
|
|
if (attempts % 3 == 2) {
|
|
|
|
|
_tryRecenter();
|
2025-09-16 22:46:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 20:58:30 +00:00
|
|
|
remaining = kraiken.balanceOf(trader);
|
|
|
|
|
if (remaining >= prevRemaining) break;
|
|
|
|
|
|
2025-09-16 22:46:43 +02:00
|
|
|
unchecked {
|
|
|
|
|
attempts++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
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()));
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
vm.writeLine(csvFilename, row);
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _buildRowPart1(string memory action, uint256 amount) internal view returns (string memory) {
|
|
|
|
|
(, int24 tick,,,,,) = pool.slot0();
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
return string(
|
|
|
|
|
abi.encodePacked(
|
|
|
|
|
action,
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(amount),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(tick),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(floorLower),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(floorUpper),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(uint256(floorLiq)),
|
|
|
|
|
","
|
|
|
|
|
)
|
|
|
|
|
);
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
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);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
return string(
|
|
|
|
|
abi.encodePacked(
|
|
|
|
|
vm.toString(anchorLower),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(anchorUpper),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(uint256(anchorLiq)),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(discoveryLower),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(discoveryUpper),
|
|
|
|
|
",",
|
|
|
|
|
vm.toString(uint256(discoveryLiq)),
|
|
|
|
|
","
|
|
|
|
|
)
|
|
|
|
|
);
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _buildRowPart3() internal view returns (string memory) {
|
|
|
|
|
uint256 ethBalance = weth.balanceOf(trader);
|
|
|
|
|
uint256 kraikenBalance = kraiken.balanceOf(trader);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
(uint128 fees0, uint128 fees1) = pool.protocolFees();
|
|
|
|
|
uint256 deltaFees0 = fees0 > totalFees0 ? fees0 - totalFees0 : 0;
|
|
|
|
|
uint256 deltaFees1 = fees1 > totalFees1 ? fees1 - totalFees1 : 0;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
|
|
|
|
return string(
|
|
|
|
|
abi.encodePacked(
|
|
|
|
|
vm.toString(ethBalance), ",", vm.toString(kraikenBalance), ",", "0,", vm.toString(deltaFees0), ",", vm.toString(deltaFees1), ",", "0"
|
|
|
|
|
)
|
|
|
|
|
);
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
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);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
for (uint256 i = 0; i < 4; i++) {
|
|
|
|
|
result[i] = chars[rand % chars.length];
|
|
|
|
|
rand = rand / chars.length;
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
return string(result);
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
function _padNumber(uint256 num, uint256 digits) internal pure returns (string memory) {
|
|
|
|
|
string memory numStr = vm.toString(num);
|
|
|
|
|
bytes memory numBytes = bytes(numStr);
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
if (numBytes.length >= digits) {
|
|
|
|
|
return numStr;
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
bytes memory result = new bytes(digits);
|
|
|
|
|
uint256 padding = digits - numBytes.length;
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
for (uint256 i = 0; i < padding; i++) {
|
2026-02-04 20:58:30 +00:00
|
|
|
result[i] = "0";
|
2025-08-23 22:32:41 +02:00
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
for (uint256 i = 0; i < numBytes.length; i++) {
|
|
|
|
|
result[padding + i] = numBytes[i];
|
|
|
|
|
}
|
2026-02-04 20:58:30 +00:00
|
|
|
|
2025-08-23 22:32:41 +02:00
|
|
|
return string(result);
|
|
|
|
|
}
|
2025-09-16 22:46:43 +02:00
|
|
|
}
|