fix: Test coverage: Kraiken + VWAPTracker to 100% (#283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2baa913435
commit
e9370c143e
2 changed files with 162 additions and 0 deletions
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue