From ab127336c823e4913090df57e819cf5b66fd5807 Mon Sep 17 00:00:00 2001 From: giteadmin Date: Sun, 6 Jul 2025 11:45:25 +0200 Subject: [PATCH] better tests --- onchain/test/LiquidityManager.t.sol | 322 +++++++++++++++++++--------- 1 file changed, 216 insertions(+), 106 deletions(-) diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 5136f3e..46dc07e 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -24,25 +24,36 @@ import {Harberg} from "../src/Harberg.sol"; import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; import {LiquidityManager} from "../src/LiquidityManager.sol"; import "../src/helpers/UniswapHelpers.sol"; -import {CSVHelper} from "./helpers/CSVHelper.sol"; -import {CSVManager} from "./helpers/CSVManager.sol"; import {UniswapTestBase} from "./helpers/UniswapTestBase.sol"; import "../src/Optimizer.sol"; import "../test/mocks/MockOptimizer.sol"; // Test constants -address constant TAX_POOL = address(2); uint24 constant FEE = uint24(10_000); // 1% fee int24 constant TICK_SPACING = 200; int24 constant ANCHOR_SPACING = 5 * TICK_SPACING; int24 constant EXTREME_PRICE_MARGIN = 12000; // Safety margin from tick boundaries +// Time constants +uint256 constant ORACLE_UPDATE_INTERVAL = 5 hours; + +// Trading constants +uint256 constant NORMALIZATION_SELL_PERCENTAGE = 100; // 1% +uint256 constant NORMALIZATION_BUY_AMOUNT = 0.01 ether; +uint256 constant BALANCE_DIVISOR = 2; +uint256 constant MIN_TRADE_AMOUNT = 1 ether; + +// Error handling constants +bytes32 constant AMPLITUDE_ERROR = keccak256("amplitude not reached."); +bytes32 constant EXPENSIVE_HARB_ERROR = keccak256("HARB extremely expensive: perform swap to normalize price before recenter"); +bytes32 constant PROTOCOL_DEATH_ERROR = keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices"); + // Dummy.sol contract Dummy { // This contract can be empty as it is only used to affect the nonce } -contract LiquidityManagerTest is UniswapTestBase, CSVManager { +contract LiquidityManagerTest is UniswapTestBase { // Setup configuration bool constant DEFAULT_TOKEN0_IS_WETH = false; @@ -51,7 +62,6 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { // Flag to skip automatic setUp for tests that need custom setup bool private _skipAutoSetup; using UniswapHelpers for IUniswapV3Pool; - using CSVHelper for *; IUniswapV3Factory factory; Stake stake; @@ -67,18 +77,36 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { uint256 harbergDiscovery; } - // Utility to deploy dummy contracts + /// @notice Utility to deploy dummy contracts for address manipulation + /// @param count Number of dummy contracts to deploy + /// @dev Used to manipulate contract deployment addresses for token ordering function deployDummies(uint count) internal { for (uint i = 0; i < count; i++) { new Dummy(); // Just increment the nonce } } + /// @notice Main setup function with custom token order + /// @param token0shouldBeWeth Whether token0 should be WETH (affects pool pair ordering) function setUpCustomToken0(bool token0shouldBeWeth) public { + _deployFactory(); + _deployTokensWithOrder(token0shouldBeWeth); + _createAndInitializePool(); + _deployProtocolContracts(); + _configurePermissions(); + } + + /// @notice Deploys the Uniswap factory + function _deployFactory() internal { factory = UniswapHelpers.deployUniswapFactory(); - + } + + /// @notice Deploys tokens in the specified order + /// @param token0shouldBeWeth Whether token0 should be WETH + function _deployTokensWithOrder(bool token0shouldBeWeth) internal { bool setupComplete = false; uint retryCount = 0; + while (!setupComplete && retryCount < 5) { // Clean slate if retrying if (retryCount > 0) { @@ -99,72 +127,113 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } } require(setupComplete, "Setup failed to meet the condition after several retries"); - + } + + /// @notice Creates and initializes the Uniswap pool + function _createAndInitializePool() internal { pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); - token0isWeth = address(weth) < address(harberg); pool.initializePoolFor1Cent(token0isWeth); - + } + + /// @notice Deploys protocol contracts (Stake, Optimizer, LiquidityManager) + function _deployProtocolContracts() internal { stake = new Stake(address(harberg), feeDestination); - harberg.setStakingPool(address(stake)); - Optimizer optimizer = Optimizer(address(new MockOptimizer())); - optimizer.initialize(address(harberg), address(stake)); + Optimizer optimizer = Optimizer(address(new MockOptimizer())); + optimizer.initialize(address(harberg), address(stake)); lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(optimizer)); lm.setFeeDestination(feeDestination); + } + + /// @notice Configures permissions and initial funding + function _configurePermissions() internal { + harberg.setStakingPool(address(stake)); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); vm.deal(address(lm), 10 ether); - initializePositionsCSV(); // Set up the CSV header } /// @notice Intelligent recenter function that handles extreme price conditions /// @param last Whether this is the last attempt (affects error handling) function recenter(bool last) internal { - // have some time pass to record prices in uni oracle + _updateOracleTime(); + _handleExtremePrice(); + _attemptRecenter(last); + } + + /// @notice Updates oracle time to ensure accurate price data + function _updateOracleTime() internal { uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + (60 * 60 * 5)); - - // Check current price before attempting recenter + vm.warp(timeBefore + ORACLE_UPDATE_INTERVAL); + } + + /// @notice Handles extreme price conditions with normalizing swaps + function _handleExtremePrice() internal { (, int24 currentTick, , , , , ) = pool.slot0(); - // Handle extreme expensive HARB (near MAX_TICK) - perform swap first - if (currentTick >= TickMath.MAX_TICK - EXTREME_PRICE_MARGIN) { + if (_isExtremelyExpensive(currentTick)) { console.log("Detected extremely expensive HARB, performing normalizing swap..."); - _performNormalizingSwap(currentTick, true); // true = expensive HARB - } - - // Handle extreme cheap HARB (near MIN_TICK) - perform swap first - if (currentTick <= TickMath.MIN_TICK + EXTREME_PRICE_MARGIN) { + _performNormalizingSwap(currentTick, true); + } else if (_isExtremelyCheap(currentTick)) { console.log("Detected extremely cheap HARB, performing normalizing swap..."); - _performNormalizingSwap(currentTick, false); // false = cheap HARB + _performNormalizingSwap(currentTick, false); } - + } + + /// @notice Attempts the recenter operation with proper error handling + /// @param last Whether this is the last attempt (affects error handling) + function _attemptRecenter(bool last) internal { try lm.recenter() returns (bool isUp) { - - // Check liquidity positions after slide - Response memory rsp; - rsp = checkLiquidity(isUp ? "shift" : "slide"); - assertGt(rsp.ethFloor, rsp.ethAnchor, "slide - Floor should hold more ETH than Anchor"); - assertGt(rsp.harbergDiscovery, rsp.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor"); - assertEq(rsp.harbergFloor, 0, "slide - Floor should have no HARB"); - assertEq(rsp.ethDiscovery, 0, "slide - Discovery should have no ETH"); - + _validateRecenterResult(isUp); } catch Error(string memory reason) { - if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("amplitude not reached."))) { - console.log("slide failed on amplitude"); - } else if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("HARB extremely expensive: perform swap to normalize price before recenter"))) { - console.log("[SUCCESS] LiquidityManager correctly detected expensive HARB and provided clear guidance"); - console.log("This demonstrates proper error handling when client-side normalization fails"); - // This is success - the protocol is working as designed - } else if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices"))) { - console.log("Protocol death detected - insufficient ETH reserves"); - if (!last) { - revert(reason); - } - } else { - if (!last) { - revert(reason); // Rethrow the error if it's not the expected message - } + _handleRecenterError(reason, last); + } + } + + /// @notice Checks if HARB price is extremely expensive + /// @param currentTick The current price tick + /// @return True if HARB is extremely expensive + function _isExtremelyExpensive(int24 currentTick) internal pure returns (bool) { + return currentTick >= TickMath.MAX_TICK - EXTREME_PRICE_MARGIN; + } + + /// @notice Checks if HARB price is extremely cheap + /// @param currentTick The current price tick + /// @return True if HARB is extremely cheap + function _isExtremelyCheap(int24 currentTick) internal pure returns (bool) { + return currentTick <= TickMath.MIN_TICK + EXTREME_PRICE_MARGIN; + } + + /// @notice Validates recenter operation results + /// @param isUp Whether the recenter moved positions up or down + function _validateRecenterResult(bool isUp) internal view { + Response memory liquidityResponse = checkLiquidity(isUp ? "shift" : "slide"); + assertGt(liquidityResponse.ethFloor, liquidityResponse.ethAnchor, "slide - Floor should hold more ETH than Anchor"); + assertGt(liquidityResponse.harbergDiscovery, liquidityResponse.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor"); + assertEq(liquidityResponse.harbergFloor, 0, "slide - Floor should have no HARB"); + assertEq(liquidityResponse.ethDiscovery, 0, "slide - Discovery should have no ETH"); + } + + /// @notice Handles recenter operation errors + /// @param reason The error reason string + /// @param last Whether this is the last attempt + function _handleRecenterError(string memory reason, bool last) internal view { + bytes32 errorHash = keccak256(abi.encodePacked(reason)); + + if (errorHash == AMPLITUDE_ERROR) { + console.log("slide failed on amplitude"); + } else if (errorHash == EXPENSIVE_HARB_ERROR) { + console.log("[SUCCESS] LiquidityManager correctly detected expensive HARB and provided clear guidance"); + console.log("This demonstrates proper error handling when client-side normalization fails"); + // This is success - the protocol is working as designed + } else if (errorHash == PROTOCOL_DEATH_ERROR) { + console.log("Protocol death detected - insufficient ETH reserves"); + if (!last) { + revert(reason); + } + } else { + if (!last) { + revert(reason); // Rethrow the error if it's not the expected message } } } @@ -182,7 +251,7 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { // Get HARB balance from account (who has been buying) to use for normalization uint256 accountHarbBalance = harberg.balanceOf(account); if (accountHarbBalance > 0) { - uint256 harbToSell = accountHarbBalance / 100; // Sell 1% of account's HARB balance + uint256 harbToSell = accountHarbBalance / NORMALIZATION_SELL_PERCENTAGE; // Sell 1% of account's HARB balance if (harbToSell == 0) harbToSell = 1; // Minimum 1 wei vm.prank(account); @@ -202,7 +271,7 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } else { // HARB is extremely cheap - we need to bring the price UP // This means we need to BUY HARB with ETH (not sell HARB) - uint256 ethToBuy = 0.01 ether; // Small amount for price normalization + uint256 ethToBuy = NORMALIZATION_BUY_AMOUNT; // Small amount for price normalization // Ensure we have enough ETH if (weth.balanceOf(address(this)) < ethToBuy) { @@ -220,6 +289,14 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { console.log("Price change:", vm.toString(newTick - currentTick), "ticks"); } + /// @notice Retrieves liquidity position information for a specific stage + /// @param s The liquidity stage (FLOOR, ANCHOR, DISCOVERY) + /// @return currentTick Current price tick of the pool + /// @return tickLower Lower bound of the position's price range + /// @return tickUpper Upper bound of the position's price range + /// @return ethAmount Amount of ETH in the position + /// @return harbergAmount Amount of HARB in the position + /// @dev Calculates actual token amounts based on current pool price and position liquidity function getBalancesPool(LiquidityManager.Stage s) internal view returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbergAmount) { (,tickLower, tickUpper) = lm.positions(s); (uint128 liquidity, , , ,) = pool.positions(keccak256(abi.encodePacked(address(lm), tickLower, tickUpper))); @@ -262,12 +339,13 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } } - function checkLiquidity(string memory eventName) internal returns (Response memory) { - Response memory rsp; + /// @notice Checks and validates current liquidity positions across all stages + /// @return liquidityResponse Structure containing ETH and HARB amounts for each position + /// @dev Aggregates position data from FLOOR, ANCHOR, and DISCOVERY stages + function checkLiquidity(string memory /* eventName */) internal view returns (Response memory) { + Response memory liquidityResponse; int24 currentTick; - string memory floorData; - string memory anchorData; - string memory discoveryData; + { int24 tickLower; int24 tickUpper; @@ -275,48 +353,50 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { uint256 harb; { (currentTick, tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.FLOOR); - floorData = string(abi.encodePacked(CSVHelper.intToStr(tickLower), ",", CSVHelper.intToStr(tickUpper), ",", CSVHelper.uintToStr(eth), ",", CSVHelper.uintToStr(harb), ",")); - rsp.ethFloor = eth; - rsp.harbergFloor = harb; + liquidityResponse.ethFloor = eth; + liquidityResponse.harbergFloor = harb; } { (,tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.ANCHOR); - anchorData = string(abi.encodePacked(CSVHelper.intToStr(tickLower), ",", CSVHelper.intToStr(tickUpper), ",", CSVHelper.uintToStr(eth), ",", CSVHelper.uintToStr(harb), ",")); - rsp.ethAnchor = eth; - rsp.harbergAnchor = harb; + liquidityResponse.ethAnchor = eth; + liquidityResponse.harbergAnchor = harb; } { (,tickLower, tickUpper, eth, harb) = getBalancesPool(LiquidityManager.Stage.DISCOVERY); - discoveryData = string(abi.encodePacked(CSVHelper.intToStr(tickLower), ",", CSVHelper.intToStr(tickUpper), ",", CSVHelper.uintToStr(eth), ",", CSVHelper.uintToStr(harb), ",")); - rsp.ethDiscovery = eth; - rsp.harbergDiscovery = harb; + liquidityResponse.ethDiscovery = eth; + liquidityResponse.harbergDiscovery = harb; } } - string memory newRow = string(abi.encodePacked(eventName, ",", CSVHelper.intToStr(currentTick), ",", floorData, anchorData, discoveryData)); - appendCSVRow(newRow); // Append the new row to the CSV - return rsp; + return liquidityResponse; } + /// @notice Executes a buy operation (ETH -> HARB) + /// @param amountEth Amount of ETH to spend buying HARB + /// @dev Wrapper around performSwap with liquidity validation function buy(uint256 amountEth) internal { performSwap(amountEth, true); - checkLiquidity(string.concat("buy ", CSVHelper.uintToStr(amountEth))); + checkLiquidity("buy"); } + /// @notice Executes a sell operation (HARB -> ETH) + /// @param amountHarb Amount of HARB to sell for ETH + /// @dev Wrapper around performSwap with liquidity validation function sell(uint256 amountHarb) internal { performSwap(amountHarb, false); - checkLiquidity(string.concat("sell ", CSVHelper.uintToStr(amountHarb))); + checkLiquidity("sell"); } + /// @notice Allows contract to receive ETH directly + /// @dev Required for WETH unwrapping operations during testing receive() external payable {} - /// @notice Write CSV data - moved to ScenarioAnalysis for research use - /// @dev This function remains for backward compatibility but should not be used in unit tests - function writeCsv() public { - writeCSVToFile("./out/positions.csv"); - } + // ======================================== + // OVERFLOW AND ARITHMETIC TESTS + // ======================================== + /// @notice Tests overflow handling in cumulative calculations /// @dev Simulates extreme values that could cause arithmetic overflow function testHandleCumulativeOverflow() public { @@ -397,6 +477,10 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { recenter(false); } + // ======================================== + // EXTREME PRICE HANDLING TESTS + // ======================================== + /// @notice Tests handling of extremely expensive HARB prices near MAX_TICK /// @dev Validates client-side price detection and normalization swaps function testExtremeExpensiveHarbHandling() public { @@ -422,8 +506,8 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { console.log("! Price not extreme enough, pushing further..."); // Try to push further if needed uint256 remainingEth = weth.balanceOf(account); - if (remainingEth > 1 ether) { - buy(remainingEth / 2); + if (remainingEth > MIN_TRADE_AMOUNT) { + buy(remainingEth / BALANCE_DIVISOR); (, postBuyTick, , , , , ) = pool.slot0(); console.log("Tick after additional buy:", vm.toString(postBuyTick)); } @@ -521,6 +605,10 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { return abi.decode(data, (string)); } + // ======================================== + // EDGE CASE AND FAILURE CLASSIFICATION TESTS + // ======================================== + /// @notice Tests systematic classification of different failure modes /// @dev Performs multiple trading cycles to trigger various edge cases function testEdgeCaseClassification() public { @@ -533,19 +621,19 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { // Perform a series of trades that might push to different edge cases for (uint i = 0; i < 30; i++) { - uint256 amount = (i * 1 ether / 10) + 1 ether; + uint256 amount = (i * MIN_TRADE_AMOUNT / 10) + MIN_TRADE_AMOUNT; uint256 harbergBal = harberg.balanceOf(account); // Trading logic if (harbergBal == 0) { - amount = amount % (weth.balanceOf(account) / 2); + amount = amount % (weth.balanceOf(account) / BALANCE_DIVISOR); amount = amount == 0 ? weth.balanceOf(account) / 10 : amount; if (amount > 0) buy(amount); } else if (weth.balanceOf(account) == 0) { if (harbergBal > 0) sell(amount % harbergBal); } else { if (i % 2 == 0) { - amount = amount % (weth.balanceOf(account) / 2); + amount = amount % (weth.balanceOf(account) / BALANCE_DIVISOR); amount = amount == 0 ? weth.balanceOf(account) / 10 : amount; if (amount > 0) buy(amount); } else { @@ -597,6 +685,10 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { // Test passes if we reach here without reverting } + // ======================================== + // PROTOCOL DEATH AND SCENARIO ANALYSIS TESTS + // ======================================== + /// @notice Tests distinction between protocol death and recoverable edge cases /// @dev Analyzes ETH reserves vs outstanding HARB to diagnose scenario type function testProtocolDeathVsEdgeCase() public { @@ -658,8 +750,43 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { // Test passes if we reach here without reverting } + + /// @notice Executes a single random trade based on available balances + /// @param amount Base amount for trade calculations + /// @param harbergBal Current HARB balance of the account + /// @dev Uses balance-based logic to determine trade type and amount + function _executeRandomTrade(uint256 amount, uint256 harbergBal) internal { + if (harbergBal == 0) { + // Only WETH available - buy HARB + amount = _calculateBuyAmount(amount); + if (amount > 0) buy(amount); + } else if (weth.balanceOf(account) == 0) { + // Only HARB available - sell HARB + sell(amount % harbergBal); + } else { + // Both tokens available - decide randomly + if (amount % 2 == 0) { + amount = _calculateBuyAmount(amount); + if (amount > 0) buy(amount); + } else { + sell(amount % harbergBal); + } + } + } + + /// @notice Calculates appropriate buy amount based on available WETH + /// @param baseAmount Base amount for calculation + /// @return Calculated buy amount bounded by available WETH + function _calculateBuyAmount(uint256 baseAmount) internal view returns (uint256) { + uint256 wethBalance = weth.balanceOf(account); + uint256 amount = baseAmount % (wethBalance / BALANCE_DIVISOR); + return amount == 0 ? wethBalance / 10 : amount; + } - + // ======================================== + // ROBUSTNESS AND FUZZ TESTS + // ======================================== + /// @notice Fuzz test to ensure protocol robustness under random trading sequences /// @dev Validates that traders cannot extract value through arbitrary trading patterns /// This is a pure unit test with no CSV recording or scenario analysis @@ -687,31 +814,14 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { /// @notice Helper to execute a sequence of random trades and recentering /// @dev Extracted for reuse in both unit tests and scenario analysis function _executeRandomTradingSequence(uint8 numActions, uint8 frequency, uint8[] calldata amounts) internal { - uint8 f = 0; + uint8 recenterFrequencyCounter = 0; for (uint i = 0; i < numActions; i++) { - uint256 amount = (uint256(amounts[i]) * 1 ether) + 1 ether; + uint256 amount = (uint256(amounts[i]) * MIN_TRADE_AMOUNT) + MIN_TRADE_AMOUNT; uint256 harbergBal = harberg.balanceOf(account); // Execute trade based on current balances and random input - if (harbergBal == 0) { - // Only WETH available - buy HARB - amount = amount % (weth.balanceOf(account) / 2); - amount = amount == 0 ? weth.balanceOf(account) / 10 : amount; - if (amount > 0) buy(amount); - } else if (weth.balanceOf(account) == 0) { - // Only HARB available - sell HARB - sell(amount % harbergBal); - } else { - // Both tokens available - decide randomly - if (amount % 2 == 0) { - amount = amount % (weth.balanceOf(account) / 2); - amount = amount == 0 ? weth.balanceOf(account) / 10 : amount; - if (amount > 0) buy(amount); - } else { - sell(amount % harbergBal); - } - } + _executeRandomTrade(amount, harbergBal); // Handle extreme price conditions to prevent test failures (, int24 currentTick, , , , , ) = pool.slot0(); @@ -727,11 +837,11 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } // Periodic recentering based on frequency - if (f >= frequency) { + if (recenterFrequencyCounter >= frequency) { recenter(false); - f = 0; + recenterFrequencyCounter = 0; } else { - f++; + recenterFrequencyCounter++; } }