harb/onchain/analysis/ParameterSweepFuzzing.s.sol

165 lines
7 KiB
Solidity
Raw Normal View History

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol";
import { FuzzingBase } from "./helpers/FuzzingBase.sol";
import "forge-std/console2.sol";
/// @title ParameterSweepFuzzing
/// @notice Runs multiple parameter combinations in a single forge execution.
/// @dev Key design: environment is deployed ONCE per combo and reused across runs
/// so that VWAP accumulates naturally across recenters.
/// Swaps are uncapped by default (no LiquidityBoundaryHelper limits).
/// LM ETH is tracked as direct balance + pool WETH (since LM is sole LP,
/// pool WETH represents LM's deployed position ETH).
contract ParameterSweepFuzzing is FuzzingBase {
ConfigurableOptimizer optimizer;
string summaryFile;
uint256 cfgBuyBias;
uint256 cfgTradesPerRun;
uint256 cfgRunsPerCombo;
uint256 cfgMinBuy;
uint256 cfgMaxBuy;
bool cfgUncapped;
struct RunResult {
uint256 lmEthInit;
uint256 lmEthFinal;
uint256 traderInitEth;
uint256 traderFinalEth;
uint256 numRecenters;
uint256 numBuysFailed;
}
function run() public {
cfgTradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(30));
cfgRunsPerCombo = vm.envOr("RUNS_PER_COMBO", uint256(5));
cfgUncapped = vm.envOr("UNCAPPED_SWAPS", true);
cfgMinBuy = vm.envOr("MIN_BUY_ETH", uint256(20));
cfgMaxBuy = vm.envOr("MAX_BUY_ETH", uint256(80));
string memory tag = vm.envOr("SWEEP_TAG", string("SWEEP"));
uint256[] memory ciValues = _parseUintArray(vm.envOr("CI_VALUES", string("0,500000000000000000,1000000000000000000")));
uint256[] memory asValues = _parseUintArray(vm.envOr("AS_VALUES", string("100000000000000000,500000000000000000,1000000000000000000")));
uint256[] memory awValues = _parseUintArray(vm.envOr("AW_VALUES", string("30,50,80")));
uint256[] memory ddValues = _parseUintArray(vm.envOr("DD_VALUES", string("200000000000000000,1000000000000000000")));
uint256[] memory bbValues = _parseUintArray(vm.envOr("BB_VALUES", string("60,80,100")));
uint256 totalCombos = ciValues.length * asValues.length * awValues.length * ddValues.length * bbValues.length;
console2.log("=== Parameter Sweep ===");
console2.log("Combinations:", totalCombos);
console2.log("Runs/combo:", cfgRunsPerCombo);
console2.log("Trades/run:", cfgTradesPerRun);
console2.log("Uncapped:", cfgUncapped);
console2.log("Trade range (ETH):", cfgMinBuy, cfgMaxBuy);
_initInfrastructure();
summaryFile = string(abi.encodePacked("analysis/sweep-", tag, "-summary.csv"));
vm.writeFile(summaryFile, "ci,anchor_share,anchor_width,discovery_depth,buy_bias,worst_trader_pnl,best_trader_pnl,any_lm_loss,lm_eth_delta\n");
uint256 comboIndex = 0;
for (uint256 ci_i = 0; ci_i < ciValues.length; ci_i++) {
for (uint256 as_i = 0; as_i < asValues.length; as_i++) {
for (uint256 aw_i = 0; aw_i < awValues.length; aw_i++) {
for (uint256 dd_i = 0; dd_i < ddValues.length; dd_i++) {
for (uint256 bb_i = 0; bb_i < bbValues.length; bb_i++) {
comboIndex++;
_runCombo(
ciValues[ci_i], asValues[as_i], uint24(awValues[aw_i] > 100 ? 100 : awValues[aw_i]), ddValues[dd_i], bbValues[bb_i], comboIndex
);
}
}
}
}
}
console2.log("=== Sweep Complete ===");
console2.log("Results:", summaryFile);
}
function _runCombo(uint256 ci, uint256 as_, uint24 aw, uint256 dd, uint256 bb, uint256 comboIdx) internal {
optimizer = new ConfigurableOptimizer(ci, as_, aw, dd);
cfgBuyBias = bb;
// Setup environment once — VWAP accumulates across all runs
_setupEnvironment(address(optimizer), comboIdx % 2 == 0, cfgUncapped);
int256 worstPnl = type(int256).min;
int256 bestPnl = type(int256).max;
bool anyLoss = false;
uint256 lmSystemStart = _getLmSystemEth();
for (uint256 runIdx = 0; runIdx < cfgRunsPerCombo; runIdx++) {
RunResult memory result = _executeRun(runIdx);
int256 pnl = int256(result.traderFinalEth) - int256(result.traderInitEth);
if (pnl > worstPnl) worstPnl = pnl;
if (pnl < bestPnl) bestPnl = pnl;
if (pnl > 0) anyLoss = true;
}
uint256 lmSystemEnd = _getLmSystemEth();
int256 lmDelta = int256(lmSystemEnd) - int256(lmSystemStart);
// Write summary row
string memory row = string(abi.encodePacked(vm.toString(ci), ",", vm.toString(as_), ",", vm.toString(uint256(aw)), ","));
row = string(abi.encodePacked(row, vm.toString(dd), ",", vm.toString(bb), ",", vm.toString(worstPnl), ","));
row = string(abi.encodePacked(row, vm.toString(bestPnl), ",", anyLoss ? "true" : "false", ",", vm.toString(lmDelta)));
vm.writeLine(summaryFile, row);
if (anyLoss) {
console2.log("UNSAFE combo", comboIdx);
console2.log(" worstPnl:", worstPnl);
console2.log(" lmDelta:", lmDelta);
} else if (comboIdx % 5 == 0) {
console2.log("Progress:", comboIdx);
}
}
function _executeRun(uint256 runIndex) internal returns (RunResult memory result) {
// DON'T re-setup environment — reuse existing so VWAP accumulates
result.lmEthInit = _getLmSystemEth();
// Clean up leftover tokens from previous run to prevent PnL leakage
_cleanupTraderTokens();
// Fund trader fresh each run
vm.deal(trader, LM_FUNDING_ETH);
vm.prank(trader);
weth.deposit{ value: LM_FUNDING_ETH }();
result.traderInitEth = weth.balanceOf(trader);
for (uint256 i = 0; i < cfgTradesPerRun; i++) {
if (uint256(keccak256(abi.encodePacked(runIndex, i, "recenter"))) % 3 == 0) {
if (_tryRecenter()) result.numRecenters++;
}
uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, i))) % 100;
if (rand < cfgBuyBias) {
uint256 range = cfgMaxBuy - cfgMinBuy;
uint256 amount = (cfgMinBuy * 1 ether) + (uint256(keccak256(abi.encodePacked(runIndex, i, "buy"))) % (range * 1 ether));
if (!_executeBuy(amount)) result.numBuysFailed++;
} else {
uint256 krkBal = kraiken.balanceOf(trader);
if (krkBal > 0) {
uint256 pct = SELL_PCT_MIN + (uint256(keccak256(abi.encodePacked(runIndex, i, "sell"))) % SELL_PCT_RANGE);
uint256 amount = krkBal * pct / 100;
_executeSell(amount);
}
}
}
_tryRecenter();
_liquidateTraderHoldings();
_recoverStuckTokens();
result.lmEthFinal = _getLmSystemEth();
result.traderFinalEth = weth.balanceOf(trader);
}
}