chore: RLN contract unit test expansion 2 (#39)

* test: mass registration and erasure
- warning cleanup

* test: large pagination query

* test: empty range pagination query

* test: impact of duration changes

* test: upgrade with invalid implementation

* test: unauthorized merkle tree modifications

* test: owner configuration updates

* fix: update gas-snapshot
This commit is contained in:
Roman Zajic 2025-09-30 09:15:30 +10:00 committed by GitHub
parent a1d97fcad9
commit e75ac913e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 312 additions and 53 deletions

View File

@ -1,51 +1,70 @@
TestStableTokenTest:test__CannotAddAlreadyMinterRole() (gas: 46015)
TestStableTokenTest:test__CannotRemoveNonMinterRole() (gas: 22633)
TestStableTokenTest:test__CheckMinterRoleMapping() (gas: 69942)
TestStableTokenTest:test__ERC20BasicFunctionality() (gas: 128100)
TestStableTokenTest:test__MinterAddedEventEmitted() (gas: 44860)
TestStableTokenTest:test__MinterRemovedEventEmitted() (gas: 34564)
TestStableTokenTest:test__MinterRoleCanMint() (gas: 95547)
TestStableTokenTest:test__MultipleMinterRolesCanMint() (gas: 125690)
TestStableTokenTest:test__NonMinterNonOwnerAccountCannotMint() (gas: 22562)
TestStableTokenTest:test__NonOwnerCannotAddMinterRole() (gas: 18154)
TestStableTokenTest:test__NonOwnerCannotRemoveMinterRole() (gas: 45632)
TestStableTokenTest:test__OwnerCanAddMinterRole() (gas: 47069)
TestStableTokenTest:test__OwnerCanAlwaysMintEvenWithoutMinterRole() (gas: 71856)
TestStableTokenTest:test__OwnerCanMintWithoutMinterRole() (gas: 67951)
TestStableTokenTest:test__OwnerCanRemoveMinterRole() (gas: 36328)
TestStableTokenTest:test__RemovedMinterRoleCannotMint() (gas: 37100)
WakuRlnV2Test:test__ErasingNonExistentMembership() (gas: 46033)
WakuRlnV2Test:test__FullCleanUpErasure() (gas: 1016600)
WakuRlnV2Test:test__GracePeriodExtensionEdgeCases() (gas: 327838)
WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 25380)
WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTNextFreeIndex() (gas: 18365)
WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16235)
WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 305899)
WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 56414)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 43985)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 42716)
WakuRlnV2Test:test__InvalidRegistration__InvalidMembershipRateLimit__MinMax() (gas: 55485)
WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1000, μ: 191559, ~: 191559)
WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1000, μ: 26091, ~: 26091)
WakuRlnV2Test:test__MaxTotalRateLimitEdgeCases() (gas: 21815151)
WakuRlnV2Test:test__MerkleTreeUpdateAfterErasureAndReuse() (gas: 2426423)
WakuRlnV2Test:test__PriceCalculatorReconfiguration() (gas: 669694)
WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 594536)
WakuRlnV2Test:test__ReinitializationProtection() (gas: 79848)
WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1000, μ: 5031235, ~: 2443747)
WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1000, μ: 1146012, ~: 1146012)
WakuRlnV2Test:test__TokenTransferFailures() (gas: 4092129)
WakuRlnV2Test:test__Upgrade() (gas: 6702686)
WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1000, μ: 385132, ~: 134408)
WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 301131)
WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1000, μ: 307480, ~: 307480)
WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1000, μ: 288428, ~: 288428)
WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1000, μ: 534572, ~: 534572)
WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1000, μ: 296089, ~: 296089)
WakuRlnV2Test:test__ValidRegistrationNoGracePeriod(uint32) (runs: 1000, μ: 292083, ~: 292083)
WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1302532)
WakuRlnV2Test:test__ValidRegistration__kats() (gas: 277468)
WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1000, μ: 277715, ~: 277715)
WakuRlnV2Test:test__ZeroGracePeriodDuration() (gas: 8156213)
WakuRlnV2Test:test__ZeroPriceEdgeCase() (gas: 791477)
WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1000, μ: 4230350, ~: 1420233)
TestStableTokenTest:test__CannotAddAlreadyMinterRole() (gas: 46286)
TestStableTokenTest:test__CannotMintExceedingMaxSupply() (gas: 26202)
TestStableTokenTest:test__CannotMintWithETHExceedingMaxSupply() (gas: 31164)
TestStableTokenTest:test__CannotMintWithZeroETH() (gas: 18282)
TestStableTokenTest:test__CannotRemoveNonMinterRole() (gas: 22638)
TestStableTokenTest:test__CannotSetMaxSupplyBelowTotalSupply() (gas: 71114)
TestStableTokenTest:test__CheckMinterRoleMapping() (gas: 70651)
TestStableTokenTest:test__ContractDoesNotHoldETHAfterMint() (gas: 110700)
TestStableTokenTest:test__ERC20BasicFunctionality() (gas: 146240)
TestStableTokenTest:test__ETHBurnedEventEmitted() (gas: 112584)
TestStableTokenTest:test__ETHIsBurnedToZeroAddress() (gas: 110545)
TestStableTokenTest:test__MaxSupplyIsSetCorrectly() (gas: 15409)
TestStableTokenTest:test__MintRequiresETH() (gas: 18254)
TestStableTokenTest:test__MintWithDifferentETHAmounts() (gas: 209672)
TestStableTokenTest:test__MinterAddedEventEmitted() (gas: 44991)
TestStableTokenTest:test__MinterRemovedEventEmitted() (gas: 34697)
TestStableTokenTest:test__MinterRoleCanMint() (gas: 98026)
TestStableTokenTest:test__MultipleMinterRolesCanMint() (gas: 128734)
TestStableTokenTest:test__NonMinterNonOwnerAccountCannotMint() (gas: 22444)
TestStableTokenTest:test__NonOwnerCannotAddMinterRole() (gas: 18239)
TestStableTokenTest:test__NonOwnerCannotRemoveMinterRole() (gas: 45775)
TestStableTokenTest:test__NonOwnerCannotSetMaxSupply() (gas: 18070)
TestStableTokenTest:test__OwnerCanAddMinterRole() (gas: 47336)
TestStableTokenTest:test__OwnerCanMintWithoutMinterRole() (gas: 74316)
TestStableTokenTest:test__OwnerCanRemoveMinterRole() (gas: 36544)
TestStableTokenTest:test__OwnerCanSetMaxSupply() (gas: 30683)
TestStableTokenTest:test__RemovedMinterRoleCannotMint() (gas: 37086)
WakuRlnV2Test:test__EmptyRangePagination() (gas: 307693)
WakuRlnV2Test:test__ErasingNonExistentMembership() (gas: 46131)
WakuRlnV2Test:test__FullCleanUpErasure() (gas: 1016790)
WakuRlnV2Test:test__GracePeriodExtensionEdgeCases() (gas: 328094)
WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 25425)
WakuRlnV2Test:test__ImpactOfDurationChangesOnExistingMemberships() (gas: 540089)
WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTNextFreeIndex() (gas: 18353)
WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16223)
WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 306169)
WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 56506)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 44077)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 42830)
WakuRlnV2Test:test__InvalidRegistration__InvalidMembershipRateLimit__MinMax() (gas: 55598)
WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1000, μ: 191620, ~: 191620)
WakuRlnV2Test:test__LargePaginationQuery() (gas: 237941853)
WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1000, μ: 26069, ~: 26069)
WakuRlnV2Test:test__MassRegistrationAndErasure() (gas: 2714587)
WakuRlnV2Test:test__MaxTotalRateLimitEdgeCases() (gas: 21832122)
WakuRlnV2Test:test__MerkleTreeUpdateAfterErasureAndReuse() (gas: 2426716)
WakuRlnV2Test:test__NonMinterCanMintWithETHAndRegister() (gas: 373178)
WakuRlnV2Test:test__OwnerConfigurationUpdates() (gas: 53177)
WakuRlnV2Test:test__PriceCalculatorReconfiguration() (gas: 669789)
WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 595140)
WakuRlnV2Test:test__ReinitializationProtection() (gas: 80197)
WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1000, μ: 5020828, ~: 2445573)
WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1000, μ: 1146896, ~: 1146896)
WakuRlnV2Test:test__TokenTransferFailures() (gas: 4114224)
WakuRlnV2Test:test__UnauthorizedMerkleTreeModifications() (gas: 1113852)
WakuRlnV2Test:test__Upgrade() (gas: 6702671)
WakuRlnV2Test:test__UpgradeWithInvalidImplementation() (gas: 51496)
WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1000, μ: 393970, ~: 134452)
WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 301276)
WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1000, μ: 307585, ~: 307585)
WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1000, μ: 288640, ~: 288640)
WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1000, μ: 534996, ~: 534996)
WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1000, μ: 296279, ~: 296279)
WakuRlnV2Test:test__ValidRegistrationNoGracePeriod(uint32) (runs: 1000, μ: 292251, ~: 292251)
WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1303567)
WakuRlnV2Test:test__ValidRegistration__kats() (gas: 277614)
WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1000, μ: 277708, ~: 277708)
WakuRlnV2Test:test__ZeroGracePeriodDuration() (gas: 8156320)
WakuRlnV2Test:test__ZeroPriceEdgeCase() (gas: 791578)
WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1000, μ: 4235383, ~: 1628509)

View File

@ -278,7 +278,7 @@ contract TestStableTokenTest is Test {
token.mintWithETH{ value: 0 }(user2);
}
function test__MaxSupplyIsSetCorrectly() external {
function test__MaxSupplyIsSetCorrectly() external view {
// maxSupply should be set to 1000000 * 10^18 by deployment script
uint256 expectedMaxSupply = 1_000_000 * 10 ** 18;
assertEq(token.maxSupply(), expectedMaxSupply);

View File

@ -63,6 +63,10 @@ contract MockPriceCalculator is IPriceCalculator {
}
}
contract NonUUPSContract {
// A mock contract that does not support UUPS (no proxiable UUID or _authorizeUpgrade)
}
contract WakuRlnV2Test is Test {
WakuRlnV2 internal w;
TestStableToken internal token;
@ -693,7 +697,6 @@ contract WakuRlnV2Test is Test {
// Calculate required token amount for membership
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
uint256 ethAmount = price; // Use same amount of ETH as token price needed
// Verify nonMinter is not a minter
assertFalse(token.isMinter(nonMinter));
@ -1302,4 +1305,241 @@ contract WakuRlnV2Test is Test {
); // expected root after insert
assertEq(token.balanceOf(address(w)), 0); // No transfer
}
function test__MassRegistrationAndErasure() external {
uint32 num = 10; // Number of memberships to register - adjust for gas limits in testing
uint32 rateLimit = 1; // Low to fit max total
vm.prank(w.owner());
w.setMinMembershipRateLimit(1);
vm.prank(w.owner());
w.setMaxMembershipRateLimit(1);
vm.prank(w.owner());
w.setMaxTotalRateLimit(num * rateLimit);
vm.prank(w.owner());
w.setActiveDuration(600); // 10 minutes
vm.prank(w.owner());
w.setGracePeriodDuration(240); // 4 minutes
(, uint256 price) = w.priceCalculator().calculate(rateLimit);
for (uint256 i = 1; i <= num; i++) {
token.approve(address(w), price);
w.register(i, rateLimit, noIdCommitmentsToErase);
}
assertEq(w.nextFreeIndex(), num);
assertEq(w.currentTotalRateLimit(), num * rateLimit);
// Warp to expire all
vm.warp(
block.timestamp + uint256(w.activeDurationForNewMemberships())
+ uint256(w.gracePeriodDurationForNewMemberships()) + 1
);
uint256[] memory toErase = new uint256[](num / 2);
for (uint256 i = 0; i < num / 2; i++) {
toErase[i] = i + 1;
}
w.eraseMemberships(toErase, false); // Lazy half
uint256[] memory toEraseFull = new uint256[](num / 2);
for (uint256 i = num / 2; i < num; i++) {
toEraseFull[i - num / 2] = i + 1;
}
w.eraseMemberships(toEraseFull, true); // Full half
assertEq(w.currentTotalRateLimit(), 0);
// Verify root and commitments
uint256[] memory actual_rcs = w.getRateCommitmentsInRangeBoundsInclusive(0, num - 1);
uint256[] memory expected_rcs = new uint256[](num);
for (uint256 i = 0; i < num; i++) {
if (i < num / 2) {
// Lazy erased: commitments remain as original
expected_rcs[i] = PoseidonT3.hash([uint256(i + 1), uint256(rateLimit)]);
} else {
// Fully erased: commitments set to 0
expected_rcs[i] = 0;
}
assertEq(actual_rcs[i], expected_rcs[i]);
}
// Verify all memberships are considered erased (rateLimit == 0)
for (uint256 i = 1; i <= num; i++) {
(uint32 rl, uint32 idx, uint256 rc) = w.getMembershipInfo(i);
assertEq(rl, 0);
assertEq(idx, 0);
assertEq(rc, 0);
assertFalse(w.isInMembershipSet(i));
}
// Optionally, verify the Merkle root is non-zero (since partial tree remains)
assertNotEq(w.root(), 0);
}
function test__LargePaginationQuery() external {
uint32 num = 1000;
uint32 rateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(rateLimit);
// Register 'num' memberships
for (uint256 i = 1; i <= num; i++) {
token.approve(address(w), price);
w.register(i, rateLimit, noIdCommitmentsToErase);
}
// Large query for the rate commitments
uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(0, num - 1);
assertEq(commitments.length, num);
// Verify each returned commitment matches the expected Poseidon hash
for (uint256 i = 0; i < num; i++) {
uint256 expected = PoseidonT3.hash([i + 1, rateLimit]);
assertEq(commitments[i], expected);
}
}
function test__EmptyRangePagination() external {
// Valid range with one element
uint32 rateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(rateLimit);
token.approve(address(w), price);
w.register(1, rateLimit, noIdCommitmentsToErase);
uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(0, 0);
assertEq(commitments.length, 1);
// Beyond nextFreeIndex
vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 1));
w.getRateCommitmentsInRangeBoundsInclusive(1, 1);
}
function test__ImpactOfDurationChangesOnExistingMemberships() external {
uint32 originalActive = w.activeDurationForNewMemberships();
uint32 originalGrace = w.gracePeriodDurationForNewMemberships();
uint32 rateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(rateLimit);
token.approve(address(w), price);
w.register(1, rateLimit, noIdCommitmentsToErase);
{
(, uint32 active1,, uint32 grace1,,,,) = w.memberships(1);
assertEq(active1, originalActive);
assertEq(grace1, originalGrace);
}
vm.prank(w.owner());
w.setActiveDuration(20 minutes);
vm.prank(w.owner());
w.setGracePeriodDuration(5 minutes);
token.approve(address(w), price);
w.register(2, rateLimit, noIdCommitmentsToErase);
// Existing unchanged
{
(, uint32 active1,, uint32 grace1,,,,) = w.memberships(1);
assertEq(active1, originalActive);
assertEq(grace1, originalGrace);
}
// New uses updated
{
(, uint32 active2,, uint32 grace2,,,,) = w.memberships(2);
assertEq(active2, 20 minutes);
assertEq(grace2, 5 minutes);
}
}
function test__UpgradeWithInvalidImplementation() external {
// Deploy an invalid (non-UUPS) implementation contract
address invalidImpl = address(new NonUUPSContract());
// Capture the current implementation address from the ERC1967 slot
bytes32 implSlot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
address originalImpl = address(uint160(uint256(vm.load(address(w), implSlot))));
// Impersonate the owner and expect the specific UUPS revert for unsupported proxiable UUID
vm.prank(w.owner());
vm.expectRevert("ERC1967Upgrade: new implementation is not UUPS");
UUPSUpgradeable(address(w)).upgradeTo(invalidImpl);
// Verify the implementation slot remains unchanged (still the original)
address currentImpl = address(uint160(uint256(vm.load(address(w), implSlot))));
assertEq(currentImpl, originalImpl);
assertNotEq(currentImpl, invalidImpl);
}
function test__UnauthorizedMerkleTreeModifications() external {
// Register a single membership to populate the Merkle tree
uint32 rateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(rateLimit);
token.approve(address(w), price);
w.register(1, rateLimit, noIdCommitmentsToErase);
// Capture the initial Merkle tree root
uint256 initialRoot = w.root();
// Attempt a low-level call to a nonexistent public function resembling LazyIMT's internal update
// Since LazyIMT's update is internal, no such function exists publicly, so the call will fail
bytes memory invalidCallData = abi.encodeWithSignature("update(uint256,uint32)", 0, 0);
(bool success,) = address(w).call(invalidCallData);
// Verify the call failed (no public function exists)
assertFalse(success);
// Verify the Merkle tree root remains unchanged
assertEq(w.root(), initialRoot);
// Verify membership data is intact
(uint32 fetchedRateLimit, uint32 index, uint256 rateCommitment) = w.getMembershipInfo(1);
assertEq(fetchedRateLimit, rateLimit);
assertEq(index, 0);
assertEq(rateCommitment, PoseidonT3.hash([uint256(1), uint256(rateLimit)]));
}
function test__OwnerConfigurationUpdates() external {
address owner = w.owner();
vm.startPrank(owner);
// Ensure maxMembershipRateLimit is compatible with maxTotalRateLimit
w.setMaxMembershipRateLimit(100);
assertEq(w.maxMembershipRateLimit(), 100);
// Set minMembershipRateLimit first to allow maxMembershipRateLimit=15
w.setMinMembershipRateLimit(2);
assertEq(w.minMembershipRateLimit(), 2);
// Valid updates
w.setMaxTotalRateLimit(200);
assertEq(w.maxTotalRateLimit(), 200);
w.setMaxMembershipRateLimit(15);
assertEq(w.maxMembershipRateLimit(), 15);
w.setActiveDuration(20 minutes);
assertEq(w.activeDurationForNewMemberships(), 20 minutes);
w.setGracePeriodDuration(5 minutes);
assertEq(w.gracePeriodDurationForNewMemberships(), 5 minutes);
// Invalid updates
vm.expectRevert(); // Generic revert for require(_minMembershipRateLimit <= maxMembershipRateLimit)
w.setMinMembershipRateLimit(20);
vm.expectRevert(); // Generic revert for require(_activeDurationForNewMembership > 0)
w.setActiveDuration(0);
vm.stopPrank();
// Non-owner
vm.prank(vm.addr(1));
vm.expectRevert("Ownable: caller is not the owner");
w.setMaxTotalRateLimit(100);
}
}