diff --git a/onchain/test/Kraiken.t.sol b/onchain/test/Kraiken.t.sol index 29219d2..93c27d7 100644 --- a/onchain/test/Kraiken.t.sol +++ b/onchain/test/Kraiken.t.sol @@ -170,4 +170,89 @@ contract KraikenTest is Test { assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn."); assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn."); } + + // ======================================== + // SETTER VALIDATION TESTS + // ======================================== + + function testSetLiquidityManagerZeroAddress() public { + Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); + vm.expectRevert(Kraiken.ZeroAddressInSetter.selector); + freshKraiken.setLiquidityManager(address(0)); + } + + function testSetLiquidityManagerAlreadySet() public { + // liquidityManager is already set in setUp — calling again must revert + vm.expectRevert(Kraiken.AddressAlreadySet.selector); + kraiken.setLiquidityManager(makeAddr("anotherLiquidityManager")); + } + + function testSetLiquidityManagerOnlyDeployer() public { + Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); + address nonDeployer = makeAddr("nonDeployer"); + vm.prank(nonDeployer); + vm.expectRevert("only deployer"); + freshKraiken.setLiquidityManager(makeAddr("someManager")); + } + + function testSetStakingPoolZeroAddress() public { + Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); + vm.expectRevert(Kraiken.ZeroAddressInSetter.selector); + freshKraiken.setStakingPool(address(0)); + } + + function testSetStakingPoolAlreadySet() public { + // stakingPool is already set in setUp — calling again must revert + vm.expectRevert(Kraiken.AddressAlreadySet.selector); + kraiken.setStakingPool(makeAddr("anotherStakingPool")); + } + + function testSetStakingPoolOnlyDeployer() public { + Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); + address nonDeployer = makeAddr("nonDeployer"); + vm.prank(nonDeployer); + vm.expectRevert("only deployer"); + freshKraiken.setStakingPool(makeAddr("somePool")); + } + + function testOnlyLiquidityManagerModifier() public { + address nonLM = makeAddr("notLiquidityManager"); + vm.prank(nonLM); + vm.expectRevert("only liquidity manager"); + kraiken.mint(1000 * 1e18); + } + + // ======================================== + // ZERO AMOUNT TESTS + // ======================================== + + function testMintZeroAmount() public { + // Mint a positive amount first so previousTotalSupply gets set + vm.prank(address(liquidityManager)); + kraiken.mint(1000 * 1e18); + + uint256 totalSupplyBefore = kraiken.totalSupply(); + assertGt(kraiken.previousTotalSupply(), 0, "previousTotalSupply should be non-zero after first mint"); + + // Mint zero: should skip the entire amount > 0 block + // AND should skip the previousTotalSupply == 0 block (it is already set) + vm.prank(address(liquidityManager)); + kraiken.mint(0); + + assertEq(kraiken.totalSupply(), totalSupplyBefore, "Total supply must not change when minting zero"); + assertEq(kraiken.balanceOf(stakingPool), 0, "Staking pool balance must not change when minting zero"); + } + + function testBurnZeroAmount() public { + vm.prank(address(liquidityManager)); + kraiken.mint(1000 * 1e18); + + uint256 totalSupplyBefore = kraiken.totalSupply(); + + // Burn zero: should skip the entire amount > 0 block + vm.prank(address(liquidityManager)); + kraiken.burn(0); + + assertEq(kraiken.totalSupply(), totalSupplyBefore, "Total supply must not change when burning zero"); + } } diff --git a/onchain/test/VWAPTracker.t.sol b/onchain/test/VWAPTracker.t.sol index 04cfb4d..e3da654 100644 --- a/onchain/test/VWAPTracker.t.sol +++ b/onchain/test/VWAPTracker.t.sol @@ -445,4 +445,81 @@ contract VWAPTrackerTest is Test { // 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" + ); + } }