- 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>
216 lines
9.1 KiB
Solidity
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);
|
|
}
|
|
}
|