diff --git a/onchain/src/abstracts/ThreePositionStrategy.sol b/onchain/src/abstracts/ThreePositionStrategy.sol index d3e2c0b..902f285 100644 --- a/onchain/src/abstracts/ThreePositionStrategy.sol +++ b/onchain/src/abstracts/ThreePositionStrategy.sol @@ -259,11 +259,17 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { { uint256 vwapX96 = getAdjustedVWAP(params.capitalInefficiency); if (vwapX96 > 0) { - int24 rawVwapTick = _tickAtPriceRatio(int128(int256(vwapX96 >> 32))); - rawVwapTick = token0isWeth ? -rawVwapTick : rawVwapTick; - int24 vwapDistance = currentTick - rawVwapTick; - if (vwapDistance < 0) vwapDistance = -vwapDistance; - mirrorTick = token0isWeth ? currentTick + vwapDistance : currentTick - vwapDistance; + // vwapX96 >> 32 converts Q96 → Q64 (ABDKMath64x64) for _tickAtPriceRatio. + // Skip mirror tick if the shifted value exceeds int128 range to prevent + // overflow in the int128 cast (#622). mirrorTick stays at currentTick. + uint256 priceRatioX64 = vwapX96 >> 32; + if (priceRatioX64 <= uint256(uint128(type(int128).max))) { + int24 rawVwapTick = _tickAtPriceRatio(int128(int256(priceRatioX64))); + rawVwapTick = token0isWeth ? -rawVwapTick : rawVwapTick; + int24 vwapDistance = currentTick - rawVwapTick; + if (vwapDistance < 0) vwapDistance = -vwapDistance; + mirrorTick = token0isWeth ? currentTick + vwapDistance : currentTick - vwapDistance; + } } } diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol index c88bf2b..18e0728 100644 --- a/onchain/test/Optimizer.t.sol +++ b/onchain/test/Optimizer.t.sol @@ -704,3 +704,4 @@ contract OptimizerNormalizedInputsTest is Test { import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; import { Math } from "@openzeppelin/utils/math/Math.sol"; + diff --git a/onchain/test/abstracts/ThreePositionStrategy.t.sol b/onchain/test/abstracts/ThreePositionStrategy.t.sol index 1292d9a..ead0d2f 100644 --- a/onchain/test/abstracts/ThreePositionStrategy.t.sol +++ b/onchain/test/abstracts/ThreePositionStrategy.t.sol @@ -502,4 +502,72 @@ contract ThreePositionStrategyTest is TestConstants { vm.expectRevert(); strategy.setAnchorPosition(CURRENT_TICK, 20 ether, params); } + + // ======================================== + // VWAP INT128 OVERFLOW GUARD (#622) + // ======================================== + + /// @notice VWAP values that would overflow int128 in _tickAtPriceRatio are handled + /// gracefully — floor is placed using scarcity/clamp signals instead of reverting (#622). + function testFloorPositionLargeVWAPNoOverflow() public { + ThreePositionStrategy.PositionParams memory params = getDefaultParams(); + params.capitalInefficiency = 0; // adjustedVWAP = 7*rawVwap/10 + + // Set VWAP so that getAdjustedVWAP(0) >> 32 exceeds int128.max (2^127-1). + // rawVwap = 2^160 → adjustedVwap ≈ 7*2^160/10 ≈ 2^159.1 → shifted ≈ 2^127.1 > int128.max + uint256 hugeVwap = uint256(1) << 160; + strategy.setVWAP(hugeVwap, 1); + + uint256 floorEthBalance = 80 ether; + uint256 pulledHarb = 1000 ether; + uint256 discoveryAmount = 500 ether; + + // Must not revert — mirror tick is skipped, floor uses scarcity/clamp fallback + strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params); + + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); + assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.FLOOR), "Should be floor position"); + assertTrue(pos.liquidity > 0, "Floor should have liquidity"); + } + + /// @notice VWAP at uint256.max >> 32 boundary — must not revert. + function testFloorPositionMaxVWAPNoOverflow() public { + ThreePositionStrategy.PositionParams memory params = getDefaultParams(); + params.capitalInefficiency = 0; + + // Use a very large VWAP near uint256 limits + // getAdjustedVWAP(0) = 7 * rawVwap / 10 — stays within uint256 + uint256 maxSafeVwap = type(uint256).max / 7; // avoids overflow in 7*rawVwap + strategy.setVWAP(maxSafeVwap, 1); + + uint256 floorEthBalance = 80 ether; + uint256 pulledHarb = 1000 ether; + uint256 discoveryAmount = 500 ether; + + // Must not revert — int128 guard skips mirror tick + strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params); + + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); + assertTrue(pos.liquidity > 0, "Floor should have liquidity even with extreme VWAP"); + } + + /// @notice Normal VWAP values (below int128 threshold) still compute mirror tick correctly. + function testFloorPositionNormalVWAPStillUsesMirror() public { + ThreePositionStrategy.PositionParams memory params = getDefaultParams(); + params.capitalInefficiency = 0; + + // Set a normal VWAP (1.0 in X96 format) — well below int128.max after >> 32 + uint256 normalVwap = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 + strategy.setVWAP(normalVwap, 1000 ether); + + uint256 floorEthBalance = 80 ether; + uint256 pulledHarb = 1000 ether; + uint256 discoveryAmount = 500 ether; + + strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params); + + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); + assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.FLOOR), "Should be floor position"); + assertTrue(pos.liquidity > 0, "Floor should have liquidity"); + } }