diff --git a/.gas-snapshot b/.gas-snapshot index 2bc98ef..328bb8c 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,30 +1,33 @@ -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) +TestStableTokenTest:test__CannotAddAlreadyMinterRole() (gas: 46248) +TestStableTokenTest:test__CannotMintExceedingMaxSupply() (gas: 26253) +TestStableTokenTest:test__CannotMintWithETHExceedingMaxSupply() (gas: 31196) +TestStableTokenTest:test__CannotMintWithZeroETH() (gas: 18269) +TestStableTokenTest:test__CannotRemoveNonMinterRole() (gas: 22686) +TestStableTokenTest:test__CannotSetMaxSupplyBelowTotalSupply() (gas: 71121) +TestStableTokenTest:test__CheckMinterRoleMapping() (gas: 70476) +TestStableTokenTest:test__ContractDoesNotHoldETHAfterMint() (gas: 110659) +TestStableTokenTest:test__ERC20BasicFunctionality() (gas: 146438) +TestStableTokenTest:test__ETHBurnedEventEmitted() (gas: 112577) +TestStableTokenTest:test__ETHIsBurnedToZeroAddress() (gas: 110526) +TestStableTokenTest:test__InitializeZeroReverts() (gas: 2558591) +TestStableTokenTest:test__MaxSupplyIsSetCorrectly() (gas: 15454) +TestStableTokenTest:test__MintRequiresETH() (gas: 18285) +TestStableTokenTest:test__MintWithDifferentETHAmounts() (gas: 209788) +TestStableTokenTest:test__MintWithETH_RevertsBelowOneETH() (gas: 25455) +TestStableTokenTest:test__MintWithETH_SucceedsAtOneETH() (gas: 110063) +TestStableTokenTest:test__MinterAddedEventEmitted() (gas: 44947) +TestStableTokenTest:test__MinterRemovedEventEmitted() (gas: 34662) +TestStableTokenTest:test__MinterRoleCanMint() (gas: 98092) +TestStableTokenTest:test__MultipleMinterRolesCanMint() (gas: 128755) +TestStableTokenTest:test__NonMinterNonOwnerAccountCannotMint() (gas: 22493) +TestStableTokenTest:test__NonOwnerCannotAddMinterRole() (gas: 18223) +TestStableTokenTest:test__NonOwnerCannotRemoveMinterRole() (gas: 45737) +TestStableTokenTest:test__NonOwnerCannotSetMaxSupply() (gas: 18054) +TestStableTokenTest:test__OwnerCanAddMinterRole() (gas: 47248) +TestStableTokenTest:test__OwnerCanMintWithoutMinterRole() (gas: 74382) +TestStableTokenTest:test__OwnerCanRemoveMinterRole() (gas: 36473) +TestStableTokenTest:test__OwnerCanSetMaxSupply() (gas: 30795) +TestStableTokenTest:test__RemovedMinterRoleCannotMint() (gas: 37073) WakuRlnV2Test:test__EmptyRangePagination() (gas: 307693) WakuRlnV2Test:test__ErasingNonExistentMembership() (gas: 46131) WakuRlnV2Test:test__FullCleanUpErasure() (gas: 1016790) @@ -44,27 +47,27 @@ WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1000, μ: 26069, ~: 26 WakuRlnV2Test:test__MassRegistrationAndErasure() (gas: 2714587) WakuRlnV2Test:test__MaxTotalRateLimitEdgeCases() (gas: 21832122) WakuRlnV2Test:test__MerkleTreeUpdateAfterErasureAndReuse() (gas: 2426716) -WakuRlnV2Test:test__NonMinterCanMintWithETHAndRegister() (gas: 373178) +WakuRlnV2Test:test__NonMinterCanMintWithETHAndRegister() (gas: 373332) WakuRlnV2Test:test__OwnerConfigurationUpdates() (gas: 53177) -WakuRlnV2Test:test__PriceCalculatorReconfiguration() (gas: 669789) +WakuRlnV2Test:test__PriceCalculatorReconfiguration() (gas: 669854) 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__RemoveAllExpiredMemberships(uint32) (runs: 1000, μ: 4516570, ~: 2259520) +WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1000, μ: 1055797, ~: 1055798) +WakuRlnV2Test:test__TokenTransferFailures() (gas: 4139092) 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(uint32) (runs: 1000, μ: 378536, ~: 134452) WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 301276) -WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1000, μ: 307585, ~: 307585) +WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1000, μ: 307650, ~: 307650) 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__ValidRegistrationWithEraseList() (gas: 1337020) WakuRlnV2Test:test__ValidRegistration__kats() (gas: 277614) -WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1000, μ: 277708, ~: 277708) +WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1000, μ: 283285, ~: 283286) 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 +WakuRlnV2Test:test__ZeroPriceEdgeCase() (gas: 791643) +WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1000, μ: 4269053, ~: 1835792) \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1695f3d..4fcebea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,7 +117,7 @@ jobs: )" >> $GITHUB_ENV - name: "Run the tests" - run: "forge test" + run: 'forge test --no-match-path "test/Echidna*.t.sol"' - name: "Add test summary" run: | @@ -151,7 +151,7 @@ jobs: run: "pnpm install" - name: "Generate the coverage report using the unit and the integration tests" - run: 'forge coverage --match-path "test/**/*.sol" --report lcov' + run: 'forge coverage --match-path "test/**/*.sol" --no-match-path "test/Echidna*.t.sol" --report lcov' - name: "Upload coverage report to Codecov" uses: "codecov/codecov-action@v3" diff --git a/echidna.config.yaml b/echidna.config.yaml new file mode 100644 index 0000000..4098935 --- /dev/null +++ b/echidna.config.yaml @@ -0,0 +1,19 @@ +solcArgs: "--via-ir --optimize --optimize-runs 1" +testMode: property +testLimit: 100000 # For ~1 hour on strong CPU; adjust +seqLen: 100 # Sequence length for stateful fuzzing +shrinkLimit: 5000 +corpusDir: corpus +deployContracts: + - ["0x0000000000000000000000000000000000001000", "PoseidonT3"] + - ["0x0000000000000000000000000000000000001001", "LazyIMT"] +cryticArgs: + - "--compile-libraries=(PoseidonT3,0x0000000000000000000000000000000000001000),(LazyIMT,0x0000000000000000000000000000000000001001)" +propMaxGas: 25000000 +testMaxGas: 25000000 +maxTimeDelay: 20000000 # ~231 days in seconds; set high to cover active (180 days / 15552000s) and grace (30 days / 2592000s) periods for expiration races +sender: ["0x10000", "0x20000", "0x30000", "0x40000"] # Multiple senders to simulate different users; expand if needed +balanceAddr: 100000000000000000000 +coverage: true +quiet: false +projectName: "WakuRlnV2" diff --git a/echidna_cleanup.sh b/echidna_cleanup.sh new file mode 100755 index 0000000..35e71ec --- /dev/null +++ b/echidna_cleanup.sh @@ -0,0 +1,5 @@ +#!/bin/sh +rm -rf corpus +rm -f covered*.txt +rm -rf .crytic/ +echo "Echidna artifacts cleaned up." diff --git a/echidna_races.config.yaml b/echidna_races.config.yaml new file mode 100644 index 0000000..eec3e9e --- /dev/null +++ b/echidna_races.config.yaml @@ -0,0 +1,19 @@ +solcArgs: "--via-ir --optimize --optimize-runs 1" +testMode: assertion +testLimit: 100000 # For ~1 hour on strong CPU; adjust +seqLen: 100 # Sequence length for stateful fuzzing +shrinkLimit: 5000 +corpusDir: corpus +deployContracts: + - ["0x0000000000000000000000000000000000001000", "PoseidonT3"] + - ["0x0000000000000000000000000000000000001001", "LazyIMT"] +cryticArgs: + - "--compile-libraries=(PoseidonT3,0x0000000000000000000000000000000000001000),(LazyIMT,0x0000000000000000000000000000000000001001)" +propMaxGas: 25000000 +testMaxGas: 25000000 +maxTimeDelay: 20000000 # ~231 days in seconds; set high to cover active (180 days / 15552000s) and grace (30 days / 2592000s) periods for expiration races +sender: ["0x10000", "0x20000", "0x30000", "0x40000"] # Multiple senders to simulate different users; expand if needed +balanceAddr: 100000000000000000000 +coverage: true +quiet: false +projectName: "WakuRlnV2" diff --git a/run_echidna_tests.sh b/run_echidna_tests.sh new file mode 100755 index 0000000..c33211e --- /dev/null +++ b/run_echidna_tests.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echidna test/EchidnaTest.t.sol --contract EchidnaTest --config echidna.config.yaml +echidna test/EchidnaTestRaces.t.sol --contract EchidnaTestRaces --config echidna_races.config.yaml diff --git a/test/EchidnaTest.t.sol b/test/EchidnaTest.t.sol new file mode 100644 index 0000000..d3208b4 --- /dev/null +++ b/test/EchidnaTest.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../src/WakuRlnV2.sol"; +import "../src/LinearPriceCalculator.sol"; +import "../src/Membership.sol"; +import "../test/TestStableToken.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract EchidnaTest { + WakuRlnV2 public w; + TestStableToken public token; + LinearPriceCalculator public priceCalculator; + + uint32 public constant MAX_TOTAL_RATELIMIT_PER_EPOCH = 160_000; + uint32 public constant MIN_RATELIMIT_PER_MEMBERSHIP = 20; + uint32 public constant MAX_RATELIMIT_PER_MEMBERSHIP = 600; + uint32 public constant ACTIVE_DURATION = 180 days; + uint32 public constant GRACE_PERIOD_DURATION = 30 days; + uint256 public constant MAX_SUPPLY = 1_000_000 * 10 ** 18; + + uint256[] public activeIdCommitments; + mapping(uint32 => uint256) public indexToId; + mapping(uint32 => uint32) public indexToRate; + mapping(uint256 => uint32) public idToExpectedActiveDuration; + + constructor() { + // Deploy TestStableToken via proxy + address tokenImpl = address(new TestStableToken()); + bytes memory tokenInitData = abi.encodeCall(TestStableToken.initialize, (MAX_SUPPLY)); + ERC1967Proxy tokenProxy = new ERC1967Proxy(tokenImpl, tokenInitData); + token = TestStableToken(address(tokenProxy)); + + // Deploy LinearPriceCalculator + priceCalculator = new LinearPriceCalculator(address(token), 0.05 ether); + + // Deploy WakuRlnV2 via proxy + address wImpl = address(new WakuRlnV2()); + bytes memory wInitData = abi.encodeCall( + WakuRlnV2.initialize, + ( + address(priceCalculator), + MAX_TOTAL_RATELIMIT_PER_EPOCH, + MIN_RATELIMIT_PER_MEMBERSHIP, + MAX_RATELIMIT_PER_MEMBERSHIP, + ACTIVE_DURATION, + GRACE_PERIOD_DURATION + ) + ); + ERC1967Proxy wProxy = new ERC1967Proxy(wImpl, wInitData); + w = WakuRlnV2(address(wProxy)); + + // Mint and approve tokens + token.mint(address(this), 1_000_000 ether); + token.approve(address(w), type(uint256).max); + } + + // Helper for proof verification + function _verifyMerkleProof( + uint256[20] memory proof, + uint256 root, + uint32 index, + uint256 leaf, + uint8 depth + ) + internal + pure + returns (bool) + { + uint256 current = leaf; + uint32 idx = index; + for (uint8 level = 0; level < depth; level++) { + bool isLeft = (idx & 1) == 0; + uint256 sibling = proof[level]; + uint256[2] memory inputs; + if (isLeft) { + inputs[0] = current; + inputs[1] = sibling; + } else { + inputs[0] = sibling; + inputs[1] = current; + } + current = PoseidonT3.hash(inputs); + idx >>= 1; + } + return current == root; + } + + // Invariants + + function echidna_rate_commitments_range_correct() public view returns (bool) { + uint32 nextFree = w.nextFreeIndex(); + if (nextFree == 0) return true; + uint32 startIndex = 0; + uint32 endIndex = nextFree - 1; + uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(startIndex, endIndex); + if (commitments.length != uint256(endIndex - startIndex + 1)) { + return false; + } + for (uint32 j = startIndex; j <= endIndex; j++) { + if (indexToRate[j] != 0) { + uint256 exp = PoseidonT3.hash([indexToId[j], uint256(indexToRate[j])]); + if (commitments[j - startIndex] != exp) { + return false; + } + } + } + return true; + } + + function echidna_merkle_proof_valid() public view returns (bool) { + uint32 nextFree = w.nextFreeIndex(); + if (nextFree == 0) return true; + for (uint32 index = 0; index < nextFree; index++) { + uint256[20] memory proof = w.getMerkleProof(index); + uint256 root = w.root(); + uint256 expectedCommitment = w.getRateCommitmentsInRangeBoundsInclusive(index, index)[0]; + if (!_verifyMerkleProof(proof, root, index, expectedCommitment, 20)) { + return false; + } + } + return true; + } + + function echidna_total_rate_limit_correct() public view returns (bool) { + uint256 computedTotal = 0; + for (uint256 i = 0; i < activeIdCommitments.length; i++) { + (,,,, uint32 rateLimitMem,,,) = w.memberships(activeIdCommitments[i]); + computedTotal += rateLimitMem; + } + return w.currentTotalRateLimit() == computedTotal; + } + + function echidna_max_total_rate_limit_valid() public view returns (bool) { + uint32 maxTotal = w.maxTotalRateLimit(); + uint256 currentTotal = w.currentTotalRateLimit(); + uint32 maxMembership = w.maxMembershipRateLimit(); + return maxTotal >= currentTotal && maxTotal >= maxMembership; + } + + function echidna_merkle_inserts_integrity() public view returns (bool) { + uint32 nextFree = w.nextFreeIndex(); + if (nextFree == 0) return true; + for (uint32 index = 0; index < nextFree; index++) { + uint256 commitment = w.getRateCommitmentsInRangeBoundsInclusive(index, index)[0]; + if (indexToRate[index] != 0) { + uint256 exp = PoseidonT3.hash([indexToId[index], uint256(indexToRate[index])]); + if (commitment != exp) { + return false; + } + } + } + return true; + } + + function echidna_merkle_erasures_integrity() public view returns (bool) { + uint32 nextFree = w.nextFreeIndex(); + if (nextFree == 0) return true; + for (uint32 index = 0; index < nextFree; index++) { + uint256 commitment = w.getRateCommitmentsInRangeBoundsInclusive(index, index)[0]; + if (indexToRate[index] == 0) { + continue; // Erased, skip + } + uint256[20] memory proof = w.getMerkleProof(index); + if (!_verifyMerkleProof(proof, w.root(), index, commitment, 20)) { + return false; + } + } + return true; + } +} diff --git a/test/EchidnaTestRaces.t.sol b/test/EchidnaTestRaces.t.sol new file mode 100644 index 0000000..64192fa --- /dev/null +++ b/test/EchidnaTestRaces.t.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../src/LinearPriceCalculator.sol"; +import "../src/WakuRlnV2.sol"; +import "../src/Membership.sol"; // Added import for MembershipUpgradeable +import "./TestStableToken.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +// Echidna invariants and assertions for WakuRlnV2 multi-user timestamp manipulation races +contract EchidnaTestRaces { + WakuRlnV2 internal w; + TestStableToken internal token; + address internal tokenOwner = address(this); + + // Storage for multi-user registrations (to track registered IDs) + uint256[] internal registeredIds; + + constructor() { + address tokenImpl = address(new TestStableToken()); + bytes memory tokenInitData = abi.encodeCall(TestStableToken.initialize, (1_000_000 * 10 ** 18)); + address tokenProxyAddr = address(new ERC1967Proxy(tokenImpl, tokenInitData)); + token = TestStableToken(tokenProxyAddr); + + LinearPriceCalculator priceCalculator = new LinearPriceCalculator(address(token), 1e18 / 20); // Example + + address impl = address(new WakuRlnV2()); + bytes memory initData = + abi.encodeCall(WakuRlnV2.initialize, (address(priceCalculator), 160_000, 20, 600, 15_552_000, 2_592_000)); + address proxyAddr = address(new ERC1967Proxy(impl, initData)); + w = WakuRlnV2(proxyAddr); + } + + // Function to register a single membership; Echidna can call this multiple times with time advances between + function registerMembership(uint256 idCommitment, uint32 rateLimit) public { + (, uint256 price) = w.priceCalculator().calculate(rateLimit); + token.mint(address(this), price); + token.approve(address(w), price); + w.register(idCommitment, rateLimit, new uint256[](0)); + + // Add to list (only if successful; reverts otherwise) + registeredIds.push(idCommitment); + } + + // Function to attempt extension on a random registered membership and assert based on current time + function attemptExtensionRace(uint256 index) public { + if (registeredIds.length == 0) return; // Skip if no registrations yet + + uint256 focusId = registeredIds[index % registeredIds.length]; + + // Query current membership info from contract to handle updates (e.g., from extensions) + MembershipUpgradeable.MembershipInfo memory info; + ( + info.depositAmount, + info.activeDuration, + info.gracePeriodStartTimestamp, + info.gracePeriodDuration, + info.rateLimit, + info.index, + info.holder, + info.token + ) = w.memberships(focusId); + + // If membership doesn't exist (e.g., erased), skip + if (info.rateLimit == 0) return; + + uint256 graceStart = info.gracePeriodStartTimestamp; + uint256 graceEnd = graceStart + uint256(info.gracePeriodDuration); + bool isInGrace = (block.timestamp >= graceStart && block.timestamp < graceEnd); + bool isExpired = (block.timestamp >= graceEnd); + + // Additional assertions: State consistency (pre-operation) + assert(w.isInGracePeriod(focusId) == isInGrace); + assert(w.isExpired(focusId) == isExpired); + + uint256[] memory ids = new uint256[](1); + ids[0] = focusId; + + bool success = false; + try w.extendMemberships(ids) { + success = true; + } catch { } + + // Assertion: Extension should succeed only if in grace period (and sender is holder, but always true here) + assert(success == isInGrace); + } + + // Function to attempt erasure on a random registered membership and assert based on current time + function attemptErasureRace(uint256 index, bool fullErase) public { + if (registeredIds.length == 0) return; // Skip if no registrations yet + + uint256 focusId = registeredIds[index % registeredIds.length]; + + // Query current membership info from contract to handle updates + MembershipUpgradeable.MembershipInfo memory info; + ( + info.depositAmount, + info.activeDuration, + info.gracePeriodStartTimestamp, + info.gracePeriodDuration, + info.rateLimit, + info.index, + info.holder, + info.token + ) = w.memberships(focusId); + + // If membership doesn't exist (e.g., already erased), skip + if (info.rateLimit == 0) return; + + uint256 graceStart = info.gracePeriodStartTimestamp; + uint256 activeEnd = graceStart; // graceStart is end of active + uint256 graceEnd = graceStart + uint256(info.gracePeriodDuration); + + bool isActive = (block.timestamp < activeEnd); + bool isExpired = (block.timestamp >= graceEnd); + + // Additional assertions: State consistency (pre-operation) + assert(w.isExpired(focusId) == isExpired); + assert(w.isInGracePeriod(focusId) == !(isExpired || isActive)); + + uint256[] memory ids = new uint256[](1); + ids[0] = focusId; + + bool success = false; + try w.eraseMemberships(ids, fullErase) { + success = true; + } catch { } + + // Assertion: Erasure should succeed only if not active (i.e., in grace or expired) + // (and for grace, sender == holder, but always true here) + assert(success == !isActive); + + // If successful erasure, remove from local registeredIds to avoid stale entries + if (success) { + // Find and remove focusId from array (swap with last and pop) + for (uint256 i = 0; i < registeredIds.length; i++) { + if (registeredIds[i] == focusId) { + registeredIds[i] = registeredIds[registeredIds.length - 1]; + registeredIds.pop(); + break; + } + } + } + } +} diff --git a/test/TestStableToken.sol b/test/TestStableToken.sol index f1ce5d9..3a09eae 100644 --- a/test/TestStableToken.sol +++ b/test/TestStableToken.sol @@ -3,8 +3,9 @@ pragma solidity >=0.8.19 <0.9.0; import { BaseScript } from "../script/Base.s.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import { ERC20PermitUpgradeable } from - "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import { + ERC20PermitUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 0ac698d..d482f1d 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -64,8 +64,9 @@ contract MockPriceCalculator is IPriceCalculator { } contract NonUUPSContract { -// A mock contract that does not support UUPS (no proxiable UUID or _authorizeUpgrade) -} + // A mock contract that does not support UUPS (no proxiable UUID or _authorizeUpgrade) + + } contract WakuRlnV2Test is Test { WakuRlnV2 internal w; @@ -911,8 +912,7 @@ contract WakuRlnV2Test is Test { w.register(idCommitment, rateLimit, noIdCommitmentsToErase); // Destructure the memberships mapping tuple, skipping unused fields - ( - , // depositAmount + (, // depositAmount uint32 activeDuration, uint256 gracePeriodStart, uint32 gracePeriodDuration, @@ -989,11 +989,10 @@ contract WakuRlnV2Test is Test { , // depositAmount , // activeDuration uint256 graceStart, - uint32 gracePeriodDuration, - , // rateLimit + uint32 gracePeriodDuration,, // rateLimit , // index , // holder - // token + // token ) = w.memberships(100); vm.warp(graceStart + gracePeriodDuration + 1); // Expire one @@ -1024,12 +1023,11 @@ contract WakuRlnV2Test is Test { ( , // depositAmount , // activeDuration - uint256 graceStart, - , // gracePeriodDuration + uint256 graceStart,, // gracePeriodDuration , // rateLimit , // index , // holder - // token + // token ) = w.memberships(idCommitment1); vm.warp(graceStart); uint256[] memory toErase = new uint256[](1); @@ -1087,12 +1085,11 @@ contract WakuRlnV2Test is Test { ( , // depositAmount , // activeDuration - uint256 gracePeriodStart, - , // gracePeriodDuration + uint256 gracePeriodStart,, // gracePeriodDuration , // rateLimit , // index , // holder - // token + // token ) = wZeroGrace.memberships(idCommitment); // Warp just after active period @@ -1128,11 +1125,10 @@ contract WakuRlnV2Test is Test { , // depositAmount , // activeDuration uint256 graceStart, - uint32 gracePeriodDuration, - , // rateLimit + uint32 gracePeriodDuration,, // rateLimit , // index , // holder - // token + // token ) = w.memberships(idCommitment); vm.warp(graceStart + gracePeriodDuration + 1); // Expire