logos-messaging-rlnv2-contract/test/EchidnaTestRaces.t.sol
Roman Zajic 65f9e58df9
chore: adversarial tests (#42)
* test: malicious upgrade drains funds

* fix: formatting

* test: show success when unauthorized upgrade after malicious

* test: offchain proof post lazy erase
- multi-user erase reuse race

* fix: line length

* fix: remove offchain lazy erase test - rate limit still applies

* test: timestamp manipulation

* fix: rename tests

* test: front running for registration

* fix: unused variables

* test: register during spam conditions

* fix: delete failing tests
- test_MaliciousUpgradeDrainsFunds
- testFrontrunning_RegistrationRevertsForVictim
- testFrontrunning_SetFillingSpam

* fix: delete MaliciousImplementation

* fix: formatting with a new Foundry version

* test: testEraseAndReuse with Echidna

* fix: remove limit check

* fix: remove test_MultiUserEraseReuseRace
- test_TimestampManipulationRaces

* fix: skip Echidna contract during forge test

* test: Echidna contract with invariants
- registerMembership
- attemptExtensionRace
- attemptErasureRace

* fix: tune config file

* fix: run and cleanup scripts for echidna

* test: Echidna test replay

* fix: Solidity version

* fix: test_attemptExtensionRace_WakuRLN

* fix: invalid commitment in test_attemptExtensionRace_WakuRLN

* fix: invalid commitments in
test_attemptErasureRace_WakuRLN

* fix: line length

* fix: skip all Echidna tests in CI

* chore: fuzz test expansion (#40)

* test: register invalid

* test: multiple registers

* fix: increase max rejects

* test: erasure with fullErase idCommitments

* fix: reduce cyclomatic complexity

* fix: reduce complexity one step less

* fix: run tests in parallel

* fix: undo run tests in parallel - default already

* test: invalid extension with extreme values

* fix: line length

* test: set MaxTotalRateLimit

* test: set ActiveDuration

* test: Merkle inserts

* test: Merkle erasures

* test: GetRateCommitmentsRange

* test: GetMerkleProof

* fix: optimized MerkleInsert MerkleErasures

* fix: update gas snapshot

* test: malicious upgrade drains funds

* fix: formatting

* test: show success when unauthorized upgrade after malicious

* test: offchain proof post lazy erase
- multi-user erase reuse race

* fix: line length

* fix: remove offchain lazy erase test - rate limit still applies

* fix: remove fuzz tests from CI run

* fix: formatting

* fix: formatting coverage

* test: timestamp manipulation

* fix: rename tests

* test: front running for registration

* fix: unused variables

* test: register during spam conditions

* fix: delete failing tests
- test_MaliciousUpgradeDrainsFunds
- testFrontrunning_RegistrationRevertsForVictim
- testFrontrunning_SetFillingSpam

* fix: delete MaliciousImplementation

* fix: formatting with a new Foundry version

* test: testEraseAndReuse with Echidna

* fix: remove limit check

* fix: remove test_MultiUserEraseReuseRace
- test_TimestampManipulationRaces

* fix: skip Echidna contract during forge test

* test: Echidna contract with invariants
- registerMembership
- attemptExtensionRace
- attemptErasureRace

* fix: tune config file

* fix: run and cleanup scripts for echidna

* test: Echidna test replay

* fix: Solidity version

* fix: test_attemptExtensionRace_WakuRLN

* fix: invalid commitment in test_attemptExtensionRace_WakuRLN

* fix: invalid commitments in
test_attemptErasureRace_WakuRLN

* fix: line length

* fix: skip all Echidna tests in CI

* test: register invalid

* test: multiple registers

* fix: increase max rejects

* test: erasure with fullErase idCommitments

* fix: reduce cyclomatic complexity

* fix: reduce complexity one step less

* test: invalid extension with extreme values

* fix: line length

* test: set MaxTotalRateLimit

* test: set ActiveDuration

* test: Merkle inserts

* test: Merkle erasures

* test: GetRateCommitmentsRange

* test: GetMerkleProof

* fix: optimized MerkleInsert MerkleErasures

* fix: update gas snapshot

* fix: formatting

* fix: remove tests with high overlap

* fix: remove all tests originally meant for fuzzing

* fix: rename merged Echidna tests

* fix: formatting

* test: fuzzing for essential invariants

* test: EchidnaTest contract

* fix: remove unnecessary imports

* fix: remove unnecessary helpers

* fix: remove bounds from invariants

* fix: change test mode to property

* fix: update run script

* fix: max_test_rejects back to the original value

* fix: remove unused local variables

* test: malicious upgrade drains funds

* fix: formatting

* test: show success when unauthorized upgrade after malicious

* test: offchain proof post lazy erase
- multi-user erase reuse race

* fix: line length

* fix: remove offchain lazy erase test - rate limit still applies

* test: timestamp manipulation

* fix: rename tests

* test: front running for registration

* fix: unused variables

* test: register during spam conditions

* fix: delete failing tests
- test_MaliciousUpgradeDrainsFunds
- testFrontrunning_RegistrationRevertsForVictim
- testFrontrunning_SetFillingSpam

* fix: delete MaliciousImplementation

* fix: remove test_MultiUserEraseReuseRace
- test_TimestampManipulationRaces

* test: Echidna test replay

* fix: Solidity version

* fix: test_attemptExtensionRace_WakuRLN

* fix: invalid commitment in test_attemptExtensionRace_WakuRLN

* fix: invalid commitments in
test_attemptErasureRace_WakuRLN

* fix: line length

* fix: cleanup after rebase

* fix: remove redundant file

* fix: formatting

* fix: formatting

* fix: adorno + archive EchidnaReplayRaces.t.sol

* test: focus on erasures with timestamps

* fix: remove isolated test

* test: Echidna tests for races
- add dynamic assertions before operation
- untrack erased IDs

* fix: remove unused replay test
2025-11-14 19:16:11 +08:00

146 lines
5.8 KiB
Solidity

// 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;
}
}
}
}
}