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.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn.");
|
||||||
assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected 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
|
// Verify mathematical consistency
|
||||||
assertEq(minPriceForOverflow, minProductForOverflow / maxReasonableFee, "Price calculation correct");
|
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