harb/onchain/analysis/BullBearSweep.s.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

216 lines
9.1 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol";
import { FuzzingBase } from "./helpers/FuzzingBase.sol";
import "forge-std/console2.sol";
/// @title BullBearSweep
/// @notice Measures LM resilience during bull->bear sentiment transitions.
/// @dev For each parameter combo: runs a deterministic bull phase (buys + recenters),
/// then a bear phase (sell all + recenters). Tracks enhanced metrics:
/// - Trader PnL (proxy for LM ETH loss)
/// - Bull retention (how much of trader ETH the LM absorbs during buys)
/// - Bear drawdown (worst LM ETH drop during sell-off)
/// - Sell attempts needed to liquidate (liquidity depth indicator)
contract BullBearSweep is FuzzingBase {
ConfigurableOptimizer optimizer;
string summaryFile;
// Scenario config
uint256 cfgBullBuys;
uint256 cfgBuySize;
uint256 cfgLmFunding;
struct ComboResult {
uint256 lmEthStart;
uint256 lmEthAfterBull;
uint256 lmEthEnd;
uint256 lmEthTrough; // lowest LM ETH during bear sell-off
uint256 actualSpent;
int256 traderPnl;
uint256 retentionPct;
uint256 drawdownBps; // (peak - trough) / peak * 10000
uint256 sellAttempts; // how many sell rounds needed to liquidate
int256 lmNetGain; // lmEthEnd - lmEthStart
bool floorMoved;
uint256 feeWethBull; // WETH fees accumulated during bull phase
uint256 feeKrkBull; // KRK fees accumulated during bull phase
uint256 feeWethTotal; // WETH fees accumulated during full cycle
uint256 feeKrkTotal; // KRK fees accumulated during full cycle
}
function run() public {
cfgBullBuys = vm.envOr("BULL_BUYS", uint256(10));
cfgBuySize = vm.envOr("BUY_SIZE_ETH", uint256(15));
cfgLmFunding = vm.envOr("LM_FUNDING_ETH", uint256(200));
string memory tag = vm.envOr("SWEEP_TAG", string("BULLBEAR"));
uint256[] memory ciValues =
_parseUintArray(vm.envOr("CI_VALUES", string("0,300000000000000000,500000000000000000,800000000000000000,1000000000000000000")));
uint256[] memory asValues =
_parseUintArray(vm.envOr("AS_VALUES", string("100000000000000000,300000000000000000,500000000000000000,700000000000000000,1000000000000000000")));
uint256[] memory awValues = _parseUintArray(vm.envOr("AW_VALUES", string("20,50,80")));
uint256[] memory ddValues = _parseUintArray(vm.envOr("DD_VALUES", string("200000000000000000,500000000000000000,1000000000000000000")));
uint256 totalCombos = ciValues.length * asValues.length * awValues.length * ddValues.length;
console2.log("=== Bull-Bear Sweep ===");
console2.log("Combinations:", totalCombos);
console2.log("Bull buys:", cfgBullBuys);
console2.log("Buy size (ETH):", cfgBuySize);
console2.log("LM funding (ETH):", cfgLmFunding);
_initInfrastructure();
summaryFile = string(abi.encodePacked("analysis/sweep-", tag, "-summary.csv"));
vm.writeFile(
summaryFile,
"ci,anchor_share,anchor_width,discovery_depth,trader_pnl,bull_spent,lm_eth_start,lm_eth_after_bull,lm_eth_end,lm_retention_pct,floor_moved,lm_eth_trough,drawdown_bps,sell_attempts,lm_net_gain,fee_weth_bull,fee_krk_bull,fee_weth_total,fee_krk_total\n"
);
uint256 comboIndex;
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++) {
comboIndex++;
_runCombo(ciValues[ci_i], asValues[as_i], uint24(awValues[aw_i] > 100 ? 100 : awValues[aw_i]), ddValues[dd_i], comboIndex);
}
}
}
}
console2.log("=== Sweep Complete ===");
console2.log("Combos:", comboIndex);
console2.log("Results:", summaryFile);
}
function _runCombo(uint256 ci, uint256 as_, uint24 aw, uint256 dd, uint256 comboIdx) internal {
optimizer = new ConfigurableOptimizer(ci, as_, aw, dd);
_setupEnvironment(address(optimizer), false, true, cfgLmFunding * 1 ether);
ComboResult memory r;
r.lmEthStart = _getLmSystemEth();
// Record initial floor position
(, int24 floorTickInit,) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
// Fund trader
uint256 totalBullEth = cfgBullBuys * cfgBuySize * 1 ether;
vm.deal(trader, totalBullEth);
vm.prank(trader);
weth.deposit{ value: totalBullEth }();
uint256 traderInit = weth.balanceOf(trader);
// === BULL PHASE: deterministic buys with periodic recenters ===
for (uint256 i = 0; i < cfgBullBuys; i++) {
uint256 buyAmt = cfgBuySize * 1 ether;
if (weth.balanceOf(trader) < buyAmt) break;
vm.startPrank(trader);
weth.transfer(address(swapExecutor), buyAmt);
vm.stopPrank();
try swapExecutor.executeBuy(buyAmt, trader) {
r.actualSpent += buyAmt;
} catch { }
_recoverStuckTokens();
if ((i + 1) % LIQUIDATION_RECENTER_INTERVAL == 0) {
_tryRecenter();
}
}
_tryRecenter();
r.lmEthAfterBull = _getLmSystemEth();
r.feeWethBull = weth.balanceOf(fees);
r.feeKrkBull = kraiken.balanceOf(fees);
// === BEAR PHASE: sell everything with drawdown tracking ===
r.lmEthTrough = r.lmEthAfterBull;
_tryRecenter();
// Instrumented liquidation: sell in chunks, track trough at each step
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();
attempts++;
// Measure LM ETH after this sell — track trough
uint256 lmNow = _getLmSystemEth();
if (lmNow < r.lmEthTrough) {
r.lmEthTrough = lmNow;
}
if (attempts % LIQUIDATION_RECENTER_INTERVAL == 0) {
_tryRecenter();
// Re-measure after recenter
lmNow = _getLmSystemEth();
if (lmNow < r.lmEthTrough) {
r.lmEthTrough = lmNow;
}
}
remaining = kraiken.balanceOf(trader);
if (remaining >= prev) break;
}
_recoverStuckTokens();
r.sellAttempts = attempts;
r.lmEthEnd = _getLmSystemEth();
r.feeWethTotal = weth.balanceOf(fees);
r.feeKrkTotal = kraiken.balanceOf(fees);
r.traderPnl = int256(weth.balanceOf(trader)) - int256(traderInit);
r.lmNetGain = int256(r.lmEthEnd) - int256(r.lmEthStart);
(, int24 floorTickEnd,) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
r.floorMoved = (floorTickEnd != floorTickInit);
if (r.lmEthAfterBull > 0) {
r.retentionPct = r.lmEthEnd * 10_000 / r.lmEthAfterBull;
r.drawdownBps = (r.lmEthAfterBull - r.lmEthTrough) * 10_000 / r.lmEthAfterBull;
}
_writeRow(ci, as_, aw, dd, r);
if (comboIdx % 10 == 0) {
console2.log("Progress:", comboIdx);
console2.log(" traderPnl:", r.traderPnl);
console2.log(" drawdown:", r.drawdownBps);
}
}
function _writeRow(uint256 ci, uint256 as_, uint24 aw, uint256 dd, ComboResult memory r) internal {
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(r.traderPnl), ",", vm.toString(r.actualSpent), ","));
row = string(abi.encodePacked(row, vm.toString(r.lmEthStart), ",", vm.toString(r.lmEthAfterBull), ","));
row = string(abi.encodePacked(row, vm.toString(r.lmEthEnd), ",", vm.toString(r.retentionPct), ",", r.floorMoved ? "true" : "false", ","));
row = string(abi.encodePacked(row, vm.toString(r.lmEthTrough), ",", vm.toString(r.drawdownBps), ","));
row = string(abi.encodePacked(row, vm.toString(r.sellAttempts), ",", vm.toString(r.lmNetGain), ","));
row = string(abi.encodePacked(row, vm.toString(r.feeWethBull), ",", vm.toString(r.feeKrkBull), ","));
row = string(abi.encodePacked(row, vm.toString(r.feeWethTotal), ",", vm.toString(r.feeKrkTotal)));
vm.writeLine(summaryFile, row);
}
}