// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; import "../../src/libraries/UniswapMath.sol"; /** * @title UniswapMath Test Suite * @notice Unit tests for mathematical utilities used in Uniswap V3 calculations */ contract MockUniswapMath is UniswapMath { // Expose internal functions for testing function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) external pure returns (int24) { return _tickAtPrice(t0isWeth, tokenAmount, ethAmount); } function tickAtPriceRatio(int128 priceRatioX64) external pure returns (int24) { return _tickAtPriceRatio(priceRatioX64); } function priceAtTick(int24 tick) external pure returns (uint256) { return _priceAtTick(tick); } function clampToTickSpacing(int24 tick, int24 spacing) external pure returns (int24) { return _clampToTickSpacing(tick, spacing); } } contract UniswapMathTest is Test { MockUniswapMath uniswapMath; int24 constant TICK_SPACING = 200; function setUp() public { uniswapMath = new MockUniswapMath(); } // ======================================== // TICK AT PRICE TESTS // ======================================== function testTickAtPriceBasic() public { // Test 1:1 ratio (equal amounts) uint256 tokenAmount = 1 ether; uint256 ethAmount = 1 ether; int24 tickWethToken0 = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); int24 tickTokenToken0 = uniswapMath.tickAtPrice(false, tokenAmount, ethAmount); // Ticks should be opposite signs for different token orderings assertEq(tickWethToken0, -tickTokenToken0, "Ticks should be negatives of each other"); assertGt(tickWethToken0, -1000, "Tick should be reasonable for 1:1 ratio"); assertLt(tickWethToken0, 1000, "Tick should be reasonable for 1:1 ratio"); } function testTickAtPriceZeroToken() public { // When token amount is 0, should return MAX_TICK int24 tick = uniswapMath.tickAtPrice(true, 0, 1 ether); assertEq(tick, TickMath.MAX_TICK, "Zero token amount should return MAX_TICK"); } function testTickAtPriceZeroEthReverts() public { // When ETH amount is 0, should revert vm.expectRevert("ETH amount cannot be zero"); uniswapMath.tickAtPrice(true, 1 ether, 0); } function testTickAtPriceHighRatio() public { // Test when token is much more expensive than ETH uint256 tokenAmount = 1 ether; uint256 ethAmount = 1000 ether; // Token is cheap relative to ETH int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); // Should be a large negative tick (cheap token) assertLt(tick, -10000, "Cheap token should result in large negative tick"); assertGt(tick, TickMath.MIN_TICK, "Tick should be within valid range"); } function testTickAtPriceLowRatio() public { // Test when token is much cheaper than ETH uint256 tokenAmount = 1000 ether; // Token is expensive relative to ETH uint256 ethAmount = 1 ether; int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); // Should be a large positive tick (expensive token) assertGt(tick, 10000, "Expensive token should result in large positive tick"); assertLt(tick, TickMath.MAX_TICK, "Tick should be within valid range"); } // ======================================== // PRICE AT TICK TESTS // ======================================== function testPriceAtTickZero() public { // Tick 0 should give price ratio of 1 (in X96 format) uint256 price = uniswapMath.priceAtTick(0); uint256 expectedPrice = 1 << 96; // 1.0 in X96 format assertEq(price, expectedPrice, "Tick 0 should give price ratio of 1"); } function testPriceAtTickPositive() public { // Positive tick should give price > 1 uint256 price = uniswapMath.priceAtTick(1000); uint256 basePrice = 1 << 96; assertGt(price, basePrice, "Positive tick should give price > 1"); } function testPriceAtTickNegative() public { // Negative tick should give price < 1 uint256 price = uniswapMath.priceAtTick(-1000); uint256 basePrice = 1 << 96; assertLt(price, basePrice, "Negative tick should give price < 1"); } function testPriceAtTickSymmetry() public { // Test that positive and negative ticks are reciprocals int24 tick = 5000; uint256 pricePositive = uniswapMath.priceAtTick(tick); uint256 priceNegative = uniswapMath.priceAtTick(-tick); // pricePositive * priceNegative should approximately equal (1 << 96)^2 uint256 product = (pricePositive >> 48) * (priceNegative >> 48); // Scale down to prevent overflow uint256 expected = 1 << 96; // Allow small tolerance for rounding errors assertApproxEqRel(product, expected, 0.01e18, "Positive and negative ticks should be reciprocals"); } // ======================================== // CLAMP TO TICK SPACING TESTS // ======================================== function testClampToTickSpacingExact() public { // Test tick that's already aligned int24 alignedTick = 1000; // Already multiple of 200 int24 result = uniswapMath.clampToTickSpacing(alignedTick, TICK_SPACING); assertEq(result, alignedTick, "Already aligned tick should remain unchanged"); } function testClampToTickSpacingRoundDown() public { // Test tick that needs rounding down int24 unalignedTick = 1150; // Should round down to 1000 int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING); assertEq(result, 1000, "Tick should round down to nearest multiple"); } function testClampToTickSpacingRoundUp() public { // Test negative tick that needs rounding int24 unalignedTick = -1150; // Should round to -1000 (towards zero) int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING); assertEq(result, -1000, "Negative tick should round towards zero"); } function testClampToTickSpacingMinBound() public { // Test tick below minimum int24 result = uniswapMath.clampToTickSpacing(TickMath.MIN_TICK - 1000, TICK_SPACING); assertEq(result, TickMath.MIN_TICK, "Tick below minimum should clamp to MIN_TICK"); } function testClampToTickSpacingMaxBound() public { // Test tick above maximum int24 result = uniswapMath.clampToTickSpacing(TickMath.MAX_TICK + 1000, TICK_SPACING); assertEq(result, TickMath.MAX_TICK, "Tick above maximum should clamp to MAX_TICK"); } // ======================================== // ROUND-TRIP CONVERSION TESTS // ======================================== function testTickPriceRoundTrip() public { // Test that tick → price → tick preserves the original value int24 originalTick = 12345; originalTick = uniswapMath.clampToTickSpacing(originalTick, TICK_SPACING); // Align to spacing uint256 price = uniswapMath.priceAtTick(originalTick); // Note: Direct round-trip through tickAtPriceRatio isn't possible since // priceAtTick returns uint256 while tickAtPriceRatio expects int128 // This test validates that the price calculation is reasonable assertGt(price, 0, "Price should be positive"); assertLt(price, type(uint128).max, "Price should be within reasonable bounds"); } // ======================================== // FUZZ TESTS // ======================================== function testFuzzTickAtPrice(uint256 tokenAmount, uint256 ethAmount) public { // Bound inputs to reasonable ranges to avoid overflow in ABDKMath64x64 conversions // int128 max is ~1.7e38, but we need to be more conservative for price ratios tokenAmount = bound(tokenAmount, 1, 1e18); ethAmount = bound(ethAmount, 1, 1e18); int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); // Tick should be within valid bounds assertGe(tick, TickMath.MIN_TICK, "Tick should be >= MIN_TICK"); assertLe(tick, TickMath.MAX_TICK, "Tick should be <= MAX_TICK"); } function testFuzzPriceAtTick(int24 tick) public { // Bound tick to reasonable range to avoid extreme prices // Further restrict to prevent overflow in price calculations tick = int24(bound(int256(tick), -200000, 200000)); uint256 price = uniswapMath.priceAtTick(tick); // Price should be positive and within reasonable bounds assertGt(price, 0, "Price should be positive"); assertLt(price, type(uint128).max, "Price should be within reasonable bounds"); } function testFuzzClampToTickSpacing(int24 tick, int24 spacing) public { // Bound spacing to reasonable positive values spacing = int24(bound(int256(spacing), 1, 1000)); int24 clampedTick = uniswapMath.clampToTickSpacing(tick, spacing); // Result should be within valid bounds assertGe(clampedTick, TickMath.MIN_TICK, "Clamped tick should be >= MIN_TICK"); assertLe(clampedTick, TickMath.MAX_TICK, "Clamped tick should be <= MAX_TICK"); // Result should be aligned to spacing (unless at boundaries) if (clampedTick != TickMath.MIN_TICK && clampedTick != TickMath.MAX_TICK) { assertEq(clampedTick % spacing, 0, "Clamped tick should be aligned to spacing"); } } }