// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "../src/VWAPTracker.sol"; import "./mocks/MockVWAPTracker.sol"; import "forge-std/Test.sol"; /** * @title VWAPTracker Test Suite * @notice Comprehensive tests for the VWAPTracker contract including: * - Basic VWAP calculation functionality * - Overflow handling in cumulative calculations * - Adjusted VWAP with capital inefficiency * - Volume weighted price accumulation */ contract VWAPTrackerTest is Test { MockVWAPTracker vwapTracker; // Test constants uint256 constant SAMPLE_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format uint256 constant SAMPLE_FEE = 1 ether; uint256 constant CAPITAL_INEFFICIENCY = 5 * 10 ** 17; // 50% function setUp() public { vwapTracker = new MockVWAPTracker(); } // ======================================== // BASIC VWAP FUNCTIONALITY TESTS // ======================================== function testInitialState() public { assertEq(vwapTracker.cumulativeVolumeWeightedPriceX96(), 0, "Initial cumulative VWAP should be zero"); assertEq(vwapTracker.cumulativeVolume(), 0, "Initial cumulative volume should be zero"); assertEq(vwapTracker.getVWAP(), 0, "Initial VWAP should be zero"); assertEq(vwapTracker.getAdjustedVWAP(CAPITAL_INEFFICIENCY), 0, "Initial adjusted VWAP should be zero"); } function testSinglePriceRecording() public { vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 expectedVolume = SAMPLE_FEE * 100; // Fee is 1% of volume uint256 expectedVWAP = SAMPLE_PRICE_X96; assertEq(vwapTracker.cumulativeVolume(), expectedVolume, "Volume should be recorded correctly"); assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should equal the single price"); uint256 adjustedVWAP = vwapTracker.getAdjustedVWAP(CAPITAL_INEFFICIENCY); uint256 expectedAdjustedVWAP = (7 * expectedVWAP / 10) + (expectedVWAP * CAPITAL_INEFFICIENCY / 10 ** 18); assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP should be calculated correctly"); } function testMultiplePriceRecording() public { // Record first price uint256 price1 = SAMPLE_PRICE_X96; uint256 fee1 = 1 ether; vwapTracker.recordVolumeAndPrice(price1, fee1); // Record second price (double the first) uint256 price2 = SAMPLE_PRICE_X96 * 2; uint256 fee2 = 2 ether; vwapTracker.recordVolumeAndPrice(price2, fee2); uint256 volume1 = fee1 * 100; uint256 volume2 = fee2 * 100; uint256 expectedTotalVolume = volume1 + volume2; uint256 expectedVWAP = (price1 * volume1 + price2 * volume2) / expectedTotalVolume; assertEq(vwapTracker.cumulativeVolume(), expectedTotalVolume, "Total volume should be sum of individual volumes"); assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should be correctly weighted average"); } function testVWAPReset() public { vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); assertGt(vwapTracker.getVWAP(), 0, "VWAP should be non-zero after recording"); vwapTracker.resetVWAP(); assertEq(vwapTracker.cumulativeVolumeWeightedPriceX96(), 0, "Cumulative VWAP should be reset to zero"); assertEq(vwapTracker.cumulativeVolume(), 0, "Cumulative volume should be reset to zero"); assertEq(vwapTracker.getVWAP(), 0, "VWAP should be zero after reset"); } // ======================================== // OVERFLOW HANDLING TESTS // ======================================== function testOverflowHandling() public { // Set cumulative values to near overflow vm.store( address(vwapTracker), bytes32(uint256(0)), // cumulativeVolumeWeightedPriceX96 storage slot bytes32(uint256(10 ** 70 + 1)) ); vm.store( address(vwapTracker), bytes32(uint256(1)), // cumulativeVolume storage slot bytes32(uint256(10 ** 40)) ); uint256 beforeVWAP = vwapTracker.cumulativeVolumeWeightedPriceX96(); uint256 beforeVolume = vwapTracker.cumulativeVolume(); assertGt(beforeVWAP, 10 ** 70, "Initial cumulative VWAP should be above overflow threshold"); // Record a price that should trigger overflow handling vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 afterVWAP = vwapTracker.cumulativeVolumeWeightedPriceX96(); uint256 afterVolume = vwapTracker.cumulativeVolume(); // Values should be compressed (smaller than before) assertLt(afterVWAP, beforeVWAP, "VWAP should be compressed after overflow"); assertLt(afterVolume, beforeVolume, "Volume should be compressed after overflow"); // But still maintain reasonable ratio uint256 calculatedVWAP = afterVWAP / afterVolume; assertGt(calculatedVWAP, 0, "Calculated VWAP should be positive after overflow handling"); assertLt(calculatedVWAP, 10 ** 40, "Calculated VWAP should be within reasonable bounds"); } function testOverflowCompressionRatio() public { // Test that compression preserves historical significance (eternal memory for dormant whale protection) uint256 initialVWAP = 10 ** 70 + 1; uint256 initialVolume = 10 ** 40; vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(initialVWAP)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(initialVolume)); uint256 expectedRatioBefore = initialVWAP / initialVolume; // ≈ 10^30 vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 finalVWAP = vwapTracker.cumulativeVolumeWeightedPriceX96(); uint256 finalVolume = vwapTracker.cumulativeVolume(); uint256 actualRatio = finalVWAP / finalVolume; // CRITICAL: The fixed compression algorithm should preserve historical significance // Maximum compression factor is 1000x, so historical data should still dominate // This is essential for dormant whale protection - historical prices must retain weight assertGt(actualRatio, 0, "Compression should maintain positive ratio"); // Historical data should still dominate after compression (not the new price) // With 1000x max compression, historical ratio should be preserved within reasonable bounds uint256 tolerance = expectedRatioBefore / 2; // 50% tolerance for new data influence assertGt(actualRatio, expectedRatioBefore - tolerance, "Historical data should still dominate after compression"); assertLt(actualRatio, expectedRatioBefore + tolerance, "Historical data should still dominate after compression"); // Verify the ratio is NOT close to the new price (which would indicate broken dormant whale protection) uint256 newPriceRatio = SAMPLE_PRICE_X96; // ≈ 7.9 * 10^28, much smaller than historical ratio assertGt(actualRatio, newPriceRatio * 2, "VWAP should not be dominated by new price - dormant whale protection"); } function testDormantWhaleProtection() public { // Test that VWAP maintains historical memory to prevent dormant whale attacks // Phase 1: Establish historical low prices with significant volume uint256 cheapPrice = SAMPLE_PRICE_X96 / 10; // 10x cheaper than sample uint256 historicalVolume = 100 ether; // Large volume to establish strong historical weight // Build up significant historical data at cheap prices for (uint256 i = 0; i < 10; i++) { vwapTracker.recordVolumeAndPrice(cheapPrice, historicalVolume); } uint256 earlyVWAP = vwapTracker.getVWAP(); assertEq(earlyVWAP, cheapPrice, "Early VWAP should equal the cheap price"); // Phase 2: Simulate large historical data that maintains the cheap price ratio // Set values that will trigger compression while preserving the cheap price VWAP uint256 historicalVWAPValue = 10 ** 70 + 1; // Trigger compression threshold uint256 adjustedVolume = historicalVWAPValue / cheapPrice; // Maintain cheap price ratio vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(historicalVWAPValue)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(adjustedVolume)); // Verify historical cheap price is preserved uint256 preWhaleVWAP = vwapTracker.getVWAP(); assertApproxEqRel(preWhaleVWAP, cheapPrice, 0.01e18, "Historical cheap price should be preserved"); // 1% tolerance // Phase 3: Whale tries to sell at high price (this should trigger compression) uint256 expensivePrice = SAMPLE_PRICE_X96 * 10; // 10x more expensive uint256 whaleVolume = 10 ether; // Whale's volume vwapTracker.recordVolumeAndPrice(expensivePrice, whaleVolume); uint256 finalVWAP = vwapTracker.getVWAP(); // CRITICAL: Final VWAP should still be much closer to historical cheap price // Even after compression, historical data should provide protection assertLt(finalVWAP, cheapPrice * 2, "VWAP should remain close to historical prices despite expensive whale trade"); // The whale's expensive price should not dominate the VWAP uint256 whaleInfluenceRatio = (finalVWAP * 100) / cheapPrice; // How much did whale inflate the price? assertLt(whaleInfluenceRatio, 300, "Whale should not be able to inflate VWAP by more than 3x from historical levels"); console.log("Historical cheap price:", cheapPrice); console.log("Whale expensive price:", expensivePrice); console.log("Final VWAP:", finalVWAP); console.log("VWAP inflation from whale:", whaleInfluenceRatio, "% (should be limited)"); } // ======================================== // ADJUSTED VWAP TESTS // ======================================== function testAdjustedVWAPCalculation() public { vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 baseVWAP = vwapTracker.getVWAP(); uint256 adjustedVWAP = vwapTracker.getAdjustedVWAP(CAPITAL_INEFFICIENCY); uint256 expectedAdjustedVWAP = (7 * baseVWAP / 10) + (baseVWAP * CAPITAL_INEFFICIENCY / 10 ** 18); assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP should match expected calculation"); // With 50% capital inefficiency: 70% + 50% = 120% of base VWAP assertGt(adjustedVWAP, baseVWAP, "Adjusted VWAP should be greater than base VWAP with 50% capital inefficiency"); } function testAdjustedVWAPWithZeroCapitalInefficiency() public { vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 baseVWAP = vwapTracker.getVWAP(); uint256 adjustedVWAP = vwapTracker.getAdjustedVWAP(0); uint256 expectedAdjustedVWAP = 7 * baseVWAP / 10; assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with zero capital inefficiency should be 70% of base"); } function testAdjustedVWAPWithMaxCapitalInefficiency() public { vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 baseVWAP = vwapTracker.getVWAP(); uint256 adjustedVWAP = vwapTracker.getAdjustedVWAP(10 ** 18); // 100% capital inefficiency uint256 expectedAdjustedVWAP = (7 * baseVWAP / 10) + baseVWAP; assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with max capital inefficiency should be 170% of base"); } // ======================================== // FUZZ TESTS // ======================================== function testFuzzVWAPCalculation(uint256 price, uint256 fee) public { // Bound inputs to reasonable ranges price = bound(price, 1000, type(uint128).max); fee = bound(fee, 1000, type(uint64).max); vwapTracker.recordVolumeAndPrice(price, fee); uint256 expectedVolume = fee * 100; uint256 expectedVWAP = price; assertEq(vwapTracker.cumulativeVolume(), expectedVolume, "Volume should be recorded correctly"); assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should equal the single price"); // Test that adjusted VWAP is within reasonable bounds uint256 adjustedVWAP = vwapTracker.getAdjustedVWAP(CAPITAL_INEFFICIENCY); assertGt(adjustedVWAP, 0, "Adjusted VWAP should be positive"); assertLt(adjustedVWAP, price * 2, "Adjusted VWAP should be less than twice the base price"); } function testConcreteMultipleRecordings() public { // Test with concrete values to ensure deterministic behavior uint256[] memory prices = new uint256[](3); uint256[] memory fees = new uint256[](3); prices[0] = 100_000; prices[1] = 200_000; prices[2] = 150_000; fees[0] = 1000; fees[1] = 2000; fees[2] = 1500; uint256 totalVWAP = 0; uint256 totalVolume = 0; for (uint256 i = 0; i < prices.length; i++) { uint256 volume = fees[i] * 100; totalVWAP += prices[i] * volume; totalVolume += volume; vwapTracker.recordVolumeAndPrice(prices[i], fees[i]); } uint256 expectedVWAP = totalVWAP / totalVolume; uint256 actualVWAP = vwapTracker.getVWAP(); assertEq(actualVWAP, expectedVWAP, "VWAP should be correctly calculated across multiple recordings"); assertEq(vwapTracker.cumulativeVolume(), totalVolume, "Total volume should be sum of all volumes"); } // ======================================== // EDGE CASE TESTS // ======================================== function testZeroVolumeHandling() public { // Don't record any prices assertEq(vwapTracker.getVWAP(), 0, "VWAP should be zero with no volume"); assertEq(vwapTracker.getAdjustedVWAP(CAPITAL_INEFFICIENCY), 0, "Adjusted VWAP should be zero with no volume"); } function testMinimalValues() public { // Test with minimal non-zero values vwapTracker.recordVolumeAndPrice(1, 1); uint256 expectedVolume = 100; // 1 * 100 uint256 expectedVWAP = 1; assertEq(vwapTracker.cumulativeVolume(), expectedVolume, "Volume should handle minimal values"); assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should handle minimal values"); } function testLargeButSafeValues() public { // Test with large but safe values (below overflow threshold) uint256 largePrice = type(uint128).max; uint256 largeFee = type(uint64).max; vwapTracker.recordVolumeAndPrice(largePrice, largeFee); uint256 expectedVolume = largeFee * 100; uint256 expectedVWAP = largePrice; assertEq(vwapTracker.cumulativeVolume(), expectedVolume, "Volume should handle large values"); assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should handle large values"); } // ======================================== // DOUBLE OVERFLOW PROTECTION TESTS // ======================================== /** * @notice Test double overflow protection under extreme ETH price scenario * @dev Simulates ETH at $1M, HARB at $1 - validates that unrealistic fees are required for double overflow */ function testDoubleOverflowExtremeEthPriceScenario() public { // Set up post-compression state (simulate 1000x compression already occurred) uint256 maxSafeValue = type(uint256).max / 10 ** 6; // Compression trigger point uint256 compressedValue = maxSafeValue; // Near threshold after compression // Manually set post-compression state vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30))); // Calculate space available before next overflow uint256 availableSpace = type(uint256).max - compressedValue; uint256 minProductForOverflow = availableSpace / 100 + 1; // price * fee * 100 > availableSpace // Extreme ETH price scenario: ETH = $1M, HARB = $1 uint256 extremeEthPriceUSD = 1_000_000; uint256 harbPriceUSD = 1; uint256 realisticPriceX96 = (uint256(harbPriceUSD) << 96) / extremeEthPriceUSD; // Calculate required fee for double overflow uint256 requiredFee = minProductForOverflow / realisticPriceX96; // ASSERTIONS: Verify double overflow requires unrealistic conditions assertGt(requiredFee, 1000 ether, "Double overflow requires unrealistic fee > 1000 ETH"); assertGt(requiredFee * extremeEthPriceUSD / 10 ** 18, 1_000_000_000, "Required fee exceeds $1B USD"); // Verify the mathematical relationship assertEq(minProductForOverflow, availableSpace / 100 + 1, "Overflow threshold calculation correct"); // Verify compression provides adequate protection assertGt(minProductForOverflow, 10 ** 50, "Product threshold astronomically high"); } /** * @notice Test double overflow protection under hyperinflated HARB price scenario * @dev Simulates HARB at $1M, ETH at $3k - validates that unrealistic fees are required for double overflow */ function testDoubleOverflowHyperinflatedHarbScenario() public { // Set up post-compression state (simulate 1000x compression already occurred) uint256 maxSafeValue = type(uint256).max / 10 ** 6; uint256 compressedValue = maxSafeValue; // Manually set post-compression state vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30))); // Calculate overflow requirements uint256 availableSpace = type(uint256).max - compressedValue; uint256 minProductForOverflow = availableSpace / 100 + 1; // Hyperinflated HARB scenario: HARB = $1M, ETH = $3k uint256 normalEthPrice = 3000; uint256 hyperInflatedHarbPrice = 1_000_000; uint256 hyperInflatedPriceX96 = (uint256(hyperInflatedHarbPrice) << 96) / normalEthPrice; // Calculate required fee for double overflow uint256 requiredFee = minProductForOverflow / hyperInflatedPriceX96; // ASSERTIONS: Verify double overflow requires unrealistic conditions assertGt(requiredFee, 100 ether, "Double overflow requires unrealistic fee > 100 ETH"); assertGt(requiredFee * normalEthPrice / 10 ** 18, 300_000, "Required fee exceeds $300k USD"); // Verify HARB price assumption is unrealistic assertGt(hyperInflatedHarbPrice, 100_000, "HARB price > $100k is unrealistic"); // Verify overflow protection holds assertGt(minProductForOverflow, 10 ** 50, "Product threshold astronomically high"); } /** * @notice Test double overflow protection under maximum transaction scenario * @dev Simulates maximum reasonable transaction size - validates required token prices are unrealistic */ function testDoubleOverflowMaximumTransactionScenario() public { // Set up post-compression state (simulate 1000x compression already occurred) uint256 maxSafeValue = type(uint256).max / 10 ** 6; uint256 compressedValue = maxSafeValue; // Manually set post-compression state vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30))); // Calculate overflow requirements uint256 availableSpace = type(uint256).max - compressedValue; uint256 minProductForOverflow = availableSpace / 100 + 1; // Maximum reasonable transaction scenario: 10,000 ETH (unrealistically large) uint256 maxReasonableFee = 10_000 ether; uint256 minPriceForOverflow = minProductForOverflow / maxReasonableFee; // Convert to USD equivalent (assuming $3k ETH) uint256 minHarbPriceInEth = minPriceForOverflow >> 96; uint256 minHarbPriceUSD = minHarbPriceInEth * 3000; // ASSERTIONS: Verify double overflow requires unrealistic token prices assertGt(minHarbPriceUSD, 1_000_000_000, "Required HARB price > $1B (exceeds global wealth)"); assertGt(minPriceForOverflow, 10 ** 30, "Required price X96 astronomically high"); // Verify transaction size assumption is already unrealistic assertGt(maxReasonableFee, 1000 ether, "10k ETH transaction is unrealistic"); // Verify the 1000x compression limit provides adequate protection assertGt(minProductForOverflow, 10 ** 50, "Product threshold provides adequate protection"); // Verify mathematical consistency assertEq(minPriceForOverflow, minProductForOverflow / maxReasonableFee, "Price calculation correct"); } // ======================================== // SINGLE-TRANSACTION OVERFLOW PROTECTION // ======================================== /** * @notice Test the ultra-rare single-transaction overflow protection (lines 36-41 of VWAPTracker) * @dev Uses a price so large that price * volume exceeds type(uint256).max / 2 without * itself overflowing uint256. This exercises the cap-and-recalculate branch. */ function testSingleTransactionOverflowProtection() public { // Choose price such that price * (fee * 100) > type(uint256).max / 2 // but the multiplication itself does NOT overflow uint256. // // price = type(uint256).max / 200, fee = 2 // volume = fee * 100 = 200 // volumeWeightedPrice = (type(uint256).max / 200) * 200 // = type(uint256).max - (type(uint256).max % 200) ← safely below max // >> type(uint256).max / 2 ← triggers the guard uint256 extremePrice = type(uint256).max / 200; uint256 largeFee = 2; vwapTracker.recordVolumeAndPrice(extremePrice, largeFee); // After the cap: volumeWeightedPrice = type(uint256).max / 2 // volume = (type(uint256).max / 2) / extremePrice uint256 cappedVWP = type(uint256).max / 2; uint256 expectedVolume = cappedVWP / extremePrice; assertEq(vwapTracker.cumulativeVolumeWeightedPriceX96(), cappedVWP, "Single-tx overflow: cumulative VWAP should be capped"); assertEq(vwapTracker.cumulativeVolume(), expectedVolume, "Single-tx overflow: volume should be recalculated from cap"); // VWAP should equal the extreme price (capped numerator / recalculated denominator) assertEq(vwapTracker.getVWAP(), extremePrice, "VWAP should equal the extreme price after cap"); } // ======================================== // MAXIMUM COMPRESSION FACTOR (>1000x) TEST // ======================================== /** * @notice Test that compressionFactor is capped at 1000 when historical data is very large * @dev Sets cumulativeVolumeWeightedPriceX96 to type(uint256).max / 100 so that * compressionFactor = (max/100) / (max/10^6) + 1 = 10000 + 1 > 1000 → capped to 1000. */ function testMaxCompressionFactorCapped() public { // maxSafeValue = type(uint256).max / 10^6 // compressionFactor = largeVWAP / maxSafeValue + 1 = (max/100)/(max/10^6) + 1 = 10^4 + 1 // Since 10^4 + 1 > 1000, it must be capped to 1000. uint256 largeVWAP = type(uint256).max / 100; uint256 largeVolume = 10 ** 20; vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(largeVWAP)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(largeVolume)); vwapTracker.recordVolumeAndPrice(SAMPLE_PRICE_X96, SAMPLE_FEE); uint256 compressionFactor = 1000; // capped from 10001 uint256 newVolume = SAMPLE_FEE * 100; uint256 newVWP = SAMPLE_PRICE_X96 * newVolume; uint256 expectedCumulativeVWAP = largeVWAP / compressionFactor + newVWP; uint256 expectedCumulativeVolume = largeVolume / compressionFactor + newVolume; assertEq( vwapTracker.cumulativeVolumeWeightedPriceX96(), expectedCumulativeVWAP, "Max compression: cumulative VWAP should be compressed by exactly 1000" ); assertEq(vwapTracker.cumulativeVolume(), expectedCumulativeVolume, "Max compression: cumulative volume should be compressed by exactly 1000"); } }