349 lines
15 KiB
Solidity
349 lines
15 KiB
Solidity
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
pragma solidity ^0.8.19;
|
||
|
|
|
||
|
|
import "forge-std/Test.sol";
|
||
|
|
import "../src/VWAPTracker.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 MockVWAPTracker is VWAPTracker {
|
||
|
|
function recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) external {
|
||
|
|
_recordVolumeAndPrice(currentPriceX96, fee);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetVWAP() external {
|
||
|
|
_resetVWAP();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
contract VWAPTrackerTest is Test {
|
||
|
|
MockVWAPTracker vwapTracker;
|
||
|
|
|
||
|
|
// Test constants
|
||
|
|
uint256 constant SAMPLE_PRICE_X96 = 79228162514264337593543950336; // 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 (uint 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] = 100000;
|
||
|
|
prices[1] = 200000;
|
||
|
|
prices[2] = 150000;
|
||
|
|
|
||
|
|
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");
|
||
|
|
}
|
||
|
|
}
|