harb/onchain/analysis/helpers/FuzzingBase.sol
openhands b7260b2eaf chore: analysis tooling, research artifacts, and code quality
- Analysis: parameter sweep scripts, adversarial testing, 2D frontier maps
- Research: KRAIKEN_RESEARCH_REPORT, SECURITY_REVIEW, STORAGE_LAYOUT
- FuzzingBase: consolidated fuzzing helper, BackgroundLP simulation
- Sweep results: CSV data for full 4D sweep (1050 combos), bull-bear,
  AS sweep, VWAP fix validation
- Code quality: .gitignore for fuzz CSVs, gas snapshot, updated docs
- Remove dead analysis helpers (CSVHelper, CSVManager, ScenarioRecorder)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:22:03 +00:00

433 lines
20 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { Kraiken } from "../../src/Kraiken.sol";
import { LiquidityManager } from "../../src/LiquidityManager.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 { BackgroundLP } from "./BackgroundLP.sol";
import { SwapExecutor } from "./SwapExecutor.sol";
import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "forge-std/Script.sol";
import "forge-std/console2.sol";
/// @title FuzzingBase
/// @notice Shared infrastructure for all fuzzing/analysis scripts.
/// @dev Provides environment setup, trade execution, liquidation, token recovery,
/// and CSV parsing. Concrete scripts inherit this and implement their own run() logic.
abstract contract FuzzingBase is Script {
// ── Constants ──────────────────────────────────────────────────────────
uint256 internal constant LM_FUNDING_ETH = 200 ether;
uint256 internal constant LM_INITIAL_WETH = 100 ether;
uint256 internal constant RECENTER_GAS_LIMIT = 50_000_000;
uint256 internal constant RECENTER_TIME_ADVANCE = 1 hours;
uint256 internal constant LIQUIDATION_MAX_ATTEMPTS = 20;
uint256 internal constant LIQUIDATION_RECENTER_INTERVAL = 3;
address internal constant DEAD_ADDRESS = address(0xdead);
uint256 internal constant SELL_PCT_MIN = 20;
uint256 internal constant SELL_PCT_RANGE = 60;
// ── Environment ───────────────────────────────────────────────────────
TestEnvironment internal testEnv;
IUniswapV3Factory internal factory;
IUniswapV3Pool internal pool;
IWETH9 internal weth;
Kraiken internal kraiken;
Stake internal stake;
LiquidityManager internal lm;
SwapExecutor internal swapExecutor;
bool internal token0isWeth;
// ── Actors ────────────────────────────────────────────────────────────
address internal trader = makeAddr("trader");
address internal fees = makeAddr("fees");
BackgroundLP internal backgroundLP;
// ══════════════════════════════════════════════════════════════════════
// Environment Setup
// ══════════════════════════════════════════════════════════════════════
/// @notice Deploy TestEnvironment and Uniswap factory (call once per script)
function _initInfrastructure() internal {
testEnv = new TestEnvironment(fees);
factory = UniswapHelpers.deployUniswapFactory();
}
/// @notice Deploy a fresh pool/LM/optimizer environment
/// @param optimizer Address of the optimizer contract to use
/// @param wethIsToken0 Token ordering for the pool
/// @param uncapped Whether to deploy SwapExecutor in uncapped mode
function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped) internal {
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped);
vm.deal(address(lm), LM_FUNDING_ETH);
vm.prank(address(lm));
weth.deposit{ value: LM_INITIAL_WETH }();
vm.prank(fees);
try lm.recenter() { } catch { }
}
/// @notice Overload: setup with custom LM funding
function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped, uint256 lmFundingEth) internal {
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped);
vm.deal(address(lm), lmFundingEth);
vm.prank(address(lm));
weth.deposit{ value: lmFundingEth / 2 }();
vm.prank(fees);
try lm.recenter() { } catch { }
}
// ══════════════════════════════════════════════════════════════════════
// Background Liquidity (Gaussian competition)
// ══════════════════════════════════════════════════════════════════════
/// @notice Deploy background LP with Gaussian-approximating stacked positions.
/// KRK is acquired by buying from the pool (realistic — no free minting).
/// @param ethPerLayer ETH per layer (5 layers total). Half goes to buying KRK,
/// half stays as WETH for the liquidity positions.
function _deployBackgroundLP(uint256 ethPerLayer) internal {
backgroundLP = new BackgroundLP(pool, weth, kraiken, token0isWeth);
// Fund with ETH (2× total: half to buy KRK, half for LP positions)
uint256 totalEth = ethPerLayer * 5;
vm.deal(address(backgroundLP), totalEth * 4);
vm.prank(address(backgroundLP));
weth.deposit{ value: totalEth * 2 }();
// Buy KRK from the pool — just like a real LP would.
// Buy in small chunks with recenters to avoid overshooting thin anchors.
address bgAddr = address(backgroundLP);
uint256 chunkSize = totalEth / 5;
for (uint256 i = 0; i < 5; i++) {
uint256 buyAmount = chunkSize;
uint256 wethBal = weth.balanceOf(bgAddr);
if (buyAmount > wethBal / 2) buyAmount = wethBal / 2; // keep some WETH for LP
if (buyAmount == 0) break;
vm.startPrank(bgAddr);
weth.transfer(address(swapExecutor), buyAmount);
vm.stopPrank();
vm.prank(bgAddr);
try swapExecutor.executeBuy(buyAmount, bgAddr) { } catch { }
// Recenter between chunks to rebuild positions
vm.warp(block.timestamp + 1 hours);
vm.roll(block.number + 1);
vm.prank(fees);
try lm.recenter() { } catch { }
}
backgroundLP.rebalance(ethPerLayer);
}
/// @notice Rebalance background LP positions to track current tick.
/// Uses existing token balances — no new ETH or KRK injected.
function _rebalanceBackgroundLP(uint256 ethPerLayer) internal {
if (address(backgroundLP) == address(0)) return;
backgroundLP.rebalance(ethPerLayer);
}
/// @notice Get fee revenue collected by feeDestination
/// @return feeWeth WETH fees, feeKrk KRK fees
function _getFeeRevenue() internal view returns (uint256 feeWeth, uint256 feeKrk) {
feeWeth = weth.balanceOf(fees);
feeKrk = kraiken.balanceOf(fees);
}
// ══════════════════════════════════════════════════════════════════════
// Staking Setup
// ══════════════════════════════════════════════════════════════════════
/// @notice Pre-seed staking to simulate a given sentiment level.
/// Buys KRK from the pool and stakes it at the given tax rate.
/// @param stakingPct Target % of authorized stake to fill (0-100, in whole %)
/// @param taxRateIdx Tax rate index (0-29, lower = cheaper = more bullish)
function _seedStaking(uint256 stakingPct, uint32 taxRateIdx) internal {
if (stakingPct == 0) return;
address staker = makeAddr("staker");
// Buy KRK for staking — use 50 ETH per attempt, loop until target met
uint256 maxAttempts = 20;
for (uint256 attempt = 0; attempt < maxAttempts; attempt++) {
// Check current staking level
uint256 currentPct = stake.getPercentageStaked() * 100 / 1e18;
if (currentPct >= stakingPct) break;
// Fund staker with ETH
vm.deal(staker, 100 ether);
vm.prank(staker);
weth.deposit{ value: 50 ether }();
// Buy KRK
vm.startPrank(staker);
weth.transfer(address(swapExecutor), 50 ether);
vm.stopPrank();
vm.prank(staker);
try swapExecutor.executeBuy(50 ether, staker) { }
catch {
break;
}
// Recenter to rebuild positions
vm.warp(block.timestamp + 1 hours);
vm.roll(block.number + 1);
vm.prank(fees);
try lm.recenter() { } catch { }
// Stake all KRK
uint256 krkBal = kraiken.balanceOf(staker);
if (krkBal == 0) break;
vm.startPrank(staker);
kraiken.approve(address(stake), krkBal);
uint256[] memory empty = new uint256[](0);
try stake.snatch(krkBal, staker, taxRateIdx, empty) { }
catch {
vm.stopPrank();
break;
}
vm.stopPrank();
}
// Final recenter to pick up new optimizer params
vm.warp(block.timestamp + 1 hours);
vm.roll(block.number + 1);
vm.prank(fees);
try lm.recenter() { } catch { }
}
// ══════════════════════════════════════════════════════════════════════
// Trade Execution
// ══════════════════════════════════════════════════════════════════════
/// @notice Execute a buy trade via the SwapExecutor
/// @return success Whether the buy produced a non-zero result
function _executeBuy(uint256 amount) internal returns (bool success) {
if (amount == 0 || weth.balanceOf(trader) < amount) return false;
vm.startPrank(trader);
weth.transfer(address(swapExecutor), amount);
try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
vm.stopPrank();
_recoverStuckTokens();
return actualAmount > 0;
} catch {
vm.stopPrank();
_recoverStuckTokens();
return false;
}
}
/// @notice Execute a sell trade via the SwapExecutor
/// @return success Whether the sell produced a non-zero result
function _executeSell(uint256 amount) internal returns (bool success) {
if (amount == 0 || kraiken.balanceOf(trader) < amount) return false;
vm.startPrank(trader);
kraiken.transfer(address(swapExecutor), amount);
try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) {
vm.stopPrank();
_recoverStuckTokens();
return actualAmount > 0;
} catch {
vm.stopPrank();
_recoverStuckTokens();
return false;
}
}
// ══════════════════════════════════════════════════════════════════════
// Recenter
// ══════════════════════════════════════════════════════════════════════
/// @notice Advance time and attempt a recenter
function _tryRecenter() internal returns (bool) {
vm.warp(block.timestamp + RECENTER_TIME_ADVANCE);
vm.roll(block.number + 1);
vm.prank(fees);
try lm.recenter{ gas: RECENTER_GAS_LIMIT }() {
return true;
} catch {
return false;
}
}
// ══════════════════════════════════════════════════════════════════════
// Liquidation
// ══════════════════════════════════════════════════════════════════════
/// @notice Sell all trader's KRAIKEN, retrying with recenters between attempts
function _liquidateTraderHoldings() internal {
uint256 remaining = kraiken.balanceOf(trader);
uint256 attempts;
while (remaining > 0 && attempts < LIQUIDATION_MAX_ATTEMPTS) {
uint256 prev = remaining;
uint256 wethBefore = weth.balanceOf(trader);
vm.startPrank(trader);
kraiken.transfer(address(swapExecutor), remaining);
vm.stopPrank();
try swapExecutor.executeSell(remaining, trader) {
if (weth.balanceOf(trader) <= wethBefore) {
_recoverStuckTokens();
break;
}
} catch {
_recoverStuckTokens();
break;
}
_recoverStuckTokens();
if (attempts % LIQUIDATION_RECENTER_INTERVAL == LIQUIDATION_RECENTER_INTERVAL - 1) {
_tryRecenter();
}
remaining = kraiken.balanceOf(trader);
if (remaining >= prev) break;
unchecked {
attempts++;
}
}
_recoverStuckTokens();
}
// ══════════════════════════════════════════════════════════════════════
// Token Recovery & Cleanup
// ══════════════════════════════════════════════════════════════════════
/// @notice Recover any tokens stuck in SwapExecutor back to the trader
function _recoverStuckTokens() internal {
uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor));
if (stuckKraiken > 0) {
vm.prank(address(swapExecutor));
kraiken.transfer(trader, stuckKraiken);
}
uint256 stuckWeth = weth.balanceOf(address(swapExecutor));
if (stuckWeth > 0) {
vm.prank(address(swapExecutor));
weth.transfer(trader, stuckWeth);
}
}
/// @notice Burn all trader + SwapExecutor tokens to prevent PnL leakage between runs
function _cleanupTraderTokens() internal {
uint256 leftoverWeth = weth.balanceOf(trader);
if (leftoverWeth > 0) {
vm.prank(trader);
weth.transfer(DEAD_ADDRESS, leftoverWeth);
}
uint256 leftoverKraiken = kraiken.balanceOf(trader);
if (leftoverKraiken > 0) {
vm.prank(trader);
kraiken.transfer(DEAD_ADDRESS, leftoverKraiken);
}
uint256 stuckWeth = weth.balanceOf(address(swapExecutor));
if (stuckWeth > 0) {
vm.prank(address(swapExecutor));
weth.transfer(DEAD_ADDRESS, stuckWeth);
}
uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor));
if (stuckKraiken > 0) {
vm.prank(address(swapExecutor));
kraiken.transfer(DEAD_ADDRESS, stuckKraiken);
}
}
// ══════════════════════════════════════════════════════════════════════
// LM ETH Measurement
// ══════════════════════════════════════════════════════════════════════
/// @notice Approximate total ETH attributable to the LM (direct balance + pool WETH)
/// @dev Pool WETH includes both LM position ETH and trader buy ETH in transit.
/// This is a rough proxy — trader PnL is the reliable safety metric.
function _getLmSystemEth() internal view returns (uint256) {
return address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool));
}
// ══════════════════════════════════════════════════════════════════════
// CSV Parsing Utilities
// ══════════════════════════════════════════════════════════════════════
/// @notice Parse a comma-separated string of uint256 values
function _parseUintArray(string memory csv) internal pure returns (uint256[] memory) {
bytes memory b = bytes(csv);
uint256 count = 1;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == ",") count++;
}
uint256[] memory result = new uint256[](count);
uint256 idx;
uint256 start;
for (uint256 i = 0; i <= b.length; i++) {
if (i == b.length || b[i] == ",") {
uint256 num;
for (uint256 j = start; j < i; j++) {
require(b[j] >= "0" && b[j] <= "9", "Invalid number in CSV");
num = num * 10 + (uint256(uint8(b[j])) - 48);
}
result[idx] = num;
idx++;
start = i + 1;
}
}
return result;
}
// ══════════════════════════════════════════════════════════════════════
// String Helpers
// ══════════════════════════════════════════════════════════════════════
/// @notice Generate a random 4-character alphanumeric ID
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);
}
/// @notice Pad a number with leading zeros
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);
}
}