diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index 7c596e7..b8ae8a7 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -414,6 +414,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer { // ---- Slot 2: pricePosition (also needs VWAP) ---- if (vwapTracker != address(0)) { uint256 vwapX96 = IVWAPTracker(vwapTracker).getVWAP(); + require(vwapX96 <= uint256(type(int256).max), "VWAP exceeds int256 range"); if (vwapX96 > 0) { // Convert pool tick to KRK-price space: higher tick = more expensive KRK. // Uniswap convention: tick ↑ → token1 more expensive relative to token0. diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol index c88bf2b..ea09bdd 100644 --- a/onchain/test/Optimizer.t.sol +++ b/onchain/test/Optimizer.t.sol @@ -704,3 +704,78 @@ contract OptimizerNormalizedInputsTest is Test { import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; import { Math } from "@openzeppelin/utils/math/Math.sol"; + +// ============================================================================= +// VWAP int256 overflow guard test (issue #622) +// ============================================================================= + +/// @dev Minimal mock that returns a configurable VWAP value for overflow testing. +contract ConfigurableVWAPMock { + uint256 private _vwap; + + function setVWAP(uint256 v) external { + _vwap = v; + } + + function getVWAP() external view returns (uint256) { + return _vwap; + } +} + +contract OptimizerVWAPOverflowTest is Test { + OptimizerInputCapture capture; + Optimizer optimizer; + MockStake mockStake; + MockKraiken mockKraiken; + ConfigurableVWAPMock configurableVwap; + MockPool mockPool; + + function setUp() public { + mockKraiken = new MockKraiken(); + mockStake = new MockStake(); + configurableVwap = new ConfigurableVWAPMock(); + mockPool = new MockPool(); + + OptimizerInputCapture impl = new OptimizerInputCapture(); + bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + capture = OptimizerInputCapture(address(proxy)); + optimizer = Optimizer(address(proxy)); + + optimizer.setDataSources(address(configurableVwap), address(mockPool), address(0), false); + mockPool.setCurrentTick(0); + mockPool.setRevertOnObserve(true); + } + + /// @notice VWAP value exceeding int256.max must revert with descriptive message. + function testRevertsWhenVWAPExceedsInt256Max() public { + configurableVwap.setVWAP(uint256(type(int256).max) + 1); + + vm.expectRevert("VWAP exceeds int256 range"); + optimizer.getLiquidityParams(); + } + + /// @notice VWAP at exactly int256.max should not revert. + function testAcceptsVWAPAtInt256Max() public { + configurableVwap.setVWAP(uint256(type(int256).max)); + + // Should not revert + optimizer.getLiquidityParams(); + } + + /// @notice VWAP at uint256.max must revert. + function testRevertsWhenVWAPIsUint256Max() public { + configurableVwap.setVWAP(type(uint256).max); + + vm.expectRevert("VWAP exceeds int256 range"); + optimizer.getLiquidityParams(); + } + + /// @notice VWAP of zero should not revert (no-op path). + function testAcceptsVWAPZero() public { + configurableVwap.setVWAP(0); + + // Should not revert — zero VWAP skips the pricePosition computation + optimizer.getLiquidityParams(); + } +}