diff --git a/.gas-snapshot b/.gas-snapshot index 9468adc..2bc98ef 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) \ No newline at end of file +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) \ No newline at end of file diff --git a/test/TestStableToken.t.sol b/test/TestStableToken.t.sol index 82f20db..9e445b7 100644 --- a/test/TestStableToken.t.sol +++ b/test/TestStableToken.t.sol @@ -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); diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index ea6414c..c71cb21 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -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); + } }