diff --git a/.gas-snapshot b/.gas-snapshot index 7f2222b..8c37d59 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,28 +1,24 @@ -WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 27547) -WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1005, μ: 1284765, ~: 577056) -WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() (gas: 18290) -WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16181) -WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 322752) -WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 239810) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 36509) -WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 35220) -WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 63679) -WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1004, μ: 207863, ~: 207863) -WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 26049, ~: 26049) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 638698) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() (gas: 906196) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() (gas: 461585) -WakuRlnV2Test:test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() (gas: 869622) -WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1005, μ: 7359251, ~: 1585880) -WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1248285, ~: 1248286) -WakuRlnV2Test:test__Upgrade() (gas: 7482875) -WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1006, μ: 227626, ~: 52991) -WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 319333) -WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 325571, ~: 325571) -WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 1456074, ~: 1456074) -WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 1276008, ~: 1276008) -WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 314390, ~: 314390) -WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1601871) -WakuRlnV2Test:test__ValidRegistration__kats() (gas: 295683) -WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 301025, ~: 301027) -WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1005, μ: 2702739, ~: 1165772) \ No newline at end of file +WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 23299) +WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() (gas: 18307) +WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16131) +WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 272654) +WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 190004) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 36492) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 35192) +WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__MinMax() (gas: 55026) +WakuRlnV2Test:test__InvalidTokenAmount(uint256,uint32) (runs: 1006, μ: 158053, ~: 158053) +WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1015, μ: 26026, ~: 26026) +WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 527384) +WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1004, μ: 3577547, ~: 653139) +WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1003, μ: 1044941, ~: 1044943) +WakuRlnV2Test:test__Upgrade() (gas: 6932864) +WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1005, μ: 227459, ~: 52991) +WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 269528) +WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1003, μ: 275279, ~: 275279) +WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1003, μ: 256301, ~: 256301) +WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1003, μ: 474309, ~: 474309) +WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1003, μ: 263787, ~: 263787) +WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1380002) +WakuRlnV2Test:test__ValidRegistration__kats() (gas: 245878) +WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1003, μ: 260362, ~: 260364) +WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1004, μ: 2377343, ~: 975838) \ No newline at end of file diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index ed9c70e..cc8b1ba 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -6,7 +6,6 @@ import { LinearPriceCalculator } from "../src/LinearPriceCalculator.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; import { LazyIMT } from "@zk-kit/imt.sol/LazyIMT.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { BaseScript } from "./Base.s.sol"; diff --git a/src/LinearPriceCalculator.sol b/src/LinearPriceCalculator.sol index 08f2ef5..0bf6bd2 100644 --- a/src/LinearPriceCalculator.sol +++ b/src/LinearPriceCalculator.sol @@ -5,7 +5,7 @@ import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2St import { IPriceCalculator } from "./IPriceCalculator.sol"; /// Address 0x0000...0000 was used instead of an ERC20 token address -error OnlyERC20TokensAllowed(); +error OnlyTokensAllowed(); /// @title Linear Price Calculator to determine the price to acquire a membership contract LinearPriceCalculator is IPriceCalculator, Ownable2Step { @@ -16,18 +16,23 @@ contract LinearPriceCalculator is IPriceCalculator, Ownable2Step { uint256 public pricePerMessagePerEpoch; constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable2Step() { - if (_token == address(0)) revert OnlyERC20TokensAllowed(); - token = _token; - pricePerMessagePerEpoch = _pricePerMessagePerEpoch; + _setTokenAndPrice(_token, _pricePerMessagePerEpoch); + } + + /// Set accepted token and price per message per epoch per period + /// @param _token The token accepted by RLN membership management + /// @param _pricePerMessagePerEpoch Price per message per epoch + function setTokenAndPrice(address _token, uint256 _pricePerMessagePerEpoch) external onlyOwner { + _setTokenAndPrice(_token, _pricePerMessagePerEpoch); } /// Set accepted token and price per message per epoch per period - /// @param _token The token accepted by the membership management for RLN - /// @param _pricePerPeriod Price per message per epoch - function setTokenAndPrice(address _token, uint256 _pricePerPeriod) external onlyOwner { - require(_token != address(0), "only tokens can be used"); + /// @param _token The token accepted by RLN membership management + /// @param _pricePerMessagePerEpoch Price per message per epoch + function _setTokenAndPrice(address _token, uint256 _pricePerMessagePerEpoch) internal { + if (_token == address(0)) revert OnlyTokensAllowed(); token = _token; - pricePerMessagePerEpoch = _pricePerPeriod; + pricePerMessagePerEpoch = _pricePerMessagePerEpoch; } /// Returns the token and price to pay in `token` for some `_rateLimit` diff --git a/src/Membership.sol b/src/Membership.sol index 3018849..ac08bb4 100644 --- a/src/Membership.sol +++ b/src/Membership.sol @@ -6,21 +6,27 @@ import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol" import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -// The specified rate limit was not correct or within the expected limits -error InvalidRateLimit(); +// The rate limit is outside the expected limits +error InvalidMembershipRateLimit(); -// It's not possible to acquire the rate limit due to exceeding the expected limits +// Cannot acquire the rate limit for a new membership due to exceeding the expected limits // even after attempting to erase expired memberships -error ExceedAvailableMaxRateLimitPerEpoch(); +error CannotExceedMaxTotalRateLimit(); -// This membership is not in grace period yet -error NotInGracePeriod(uint256 idCommitment); +// The membership is not in its grace period (cannot be extended) +error CannotExtendNonGracePeriodMembership(uint256 idCommitment); -// The sender is not the holder of the membership -error NotHolder(uint256 idCommitment); +// The sender is not the holder of this membership (cannot extend) +error NonHolderCannotExtend(uint256 idCommitment); -// This membership cannot be erased (either it is not expired or not in grace period and/or not the owner) -error CantEraseMembership(uint256 idCommitment); +// The membership is still active (cannot be erased) +error CannotEraseActiveMembership(uint256 idCommitment); + +// The sender is not the holder of this membership (cannot erase) +error NonHolderCannotEraseGracePeriodMembership(uint256 idCommitment); + +// The membership does not exist +error MembershipDoesNotExist(uint256 idCommitment); abstract contract MembershipUpgradeable is Initializable { using SafeERC20 for IERC20; @@ -28,415 +34,320 @@ abstract contract MembershipUpgradeable is Initializable { /// @notice Address of the Price Calculator used to calculate the price of a new membership IPriceCalculator public priceCalculator; - /// @notice Maximum total rate limit of all memberships in the tree - uint32 public maxTotalRateLimitPerEpoch; + /// @notice Maximum total rate limit of all memberships in the membership set (messages per epoch) + uint32 public maxTotalRateLimit; /// @notice Maximum rate limit of one membership - uint32 public maxRateLimitPerMembership; + uint32 public maxMembershipRateLimit; /// @notice Minimum rate limit of one membership - uint32 public minRateLimitPerMembership; - - /// @notice Membership billing period - uint32 public expirationTerm; - - /// @notice Membership grace period - uint32 public gracePeriod; + uint32 public minMembershipRateLimit; - /// @notice balances available to withdraw - mapping(address holder => mapping(address token => uint256 balance)) public balancesToWithdraw; + /// @notice Membership active period duration (A in the spec) + uint32 public activeDurationForNewMemberships; - /// @notice Total rate limit of all memberships in the tree - uint256 public totalRateLimitPerEpoch; + /// @notice Membership grace period duration (G in the spec) + uint32 public gracePeriodDurationForNewMemberships; - /// @notice List of registered memberships - mapping(uint256 idCommitment => MembershipInfo member) public members; + /// @notice Deposits available for withdrawal + /// Note: amount of deposits unavailable for withdrawal are stored in MembershipInfo elements. + mapping(address holder => mapping(address token => uint256 balance)) public depositsToWithdraw; - /// @notice The index on the merkle tree for the next member to be registered - uint32 public nextCommitmentIndex; + /// @notice Current total rate limit of all memberships in the membership set (messages per epoch) + uint256 public currentTotalRateLimit; - /// @notice track available indices that are available due to expired memberships being removed - uint32[] public availableExpiredIndices; + /// @notice List of memberships in the membership set + mapping(uint256 idCommitment => MembershipInfo membership) public memberships; - /// @dev Oldest membership - uint256 public head = 0; + /// @notice The index in the membership set for the next membership to be registered + uint32 public nextFreeIndex; - /// @dev Newest membership - uint256 public tail = 0; + /// @notice Indices of erased memberships that can be reused for new registrations + uint32[] public indicesOfLazilyErasedMemberships; struct MembershipInfo { - /// @notice idCommitment of the previous membership - uint256 prev; - /// @notice idCommitment of the next membership - uint256 next; - /// @notice amount of the token used to acquire this membership - uint256 amount; - /// @notice timestamp of when the grace period starts for this membership - uint256 gracePeriodStartDate; - /// @notice duration of the grace period - uint32 gracePeriod; - /// @notice the user message limit of each member - uint32 userMessageLimit; - /// @notice the index of the member in the set + /// @notice the deposit amount (in tokens) to register this membership + uint256 depositAmount; + /// @notice the duration of the active period of this membership + uint32 activeDuration; + /// @notice the start of the grace period (= the end of the active period) + uint256 gracePeriodStartTimestamp; + /// @notice the duration of the grace period of this membership + uint32 gracePeriodDuration; + /// @notice the membership rate limit + uint32 rateLimit; + /// @notice the index of the membership in the membership set uint32 index; - /// @notice address of the owner of this membership + /// @notice the address of the holder of this membership address holder; - /// @notice token used to acquire this membership + /// @notice the token used to make the deposit to register this membership address token; } - /// @notice Emitted when a membership is erased due to having exceeded the grace period or the owner having chosen - /// to not extend it - /// @param idCommitment the idCommitment of the member - /// @param userMessageLimit the rate limit of this membership - /// @param index the index of the membership in the merkle tree - event MemberExpired(uint256 idCommitment, uint32 userMessageLimit, uint32 index); - - /// @notice Emitted when a membership in grace period is extended - /// @param idCommitment the idCommitment of the member - /// @param userMessageLimit the rate limit of this membership - /// @param index the index of the membership in the merkle tree - /// @param newExpirationDate the new expiration date of this membership - event MemberExtended(uint256 idCommitment, uint32 userMessageLimit, uint32 index, uint256 newExpirationDate); + /// Emitted when a new membership is added to the membership set + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index The index of the membership in the membership set + event MembershipRegistered(uint256 idCommitment, uint256 membershipRateLimit, uint32 index); + + /// @notice Emitted when a membership is expired (exceeded its grace period and not extended) + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index the index of the membership in the membership set + event MembershipExpired(uint256 idCommitment, uint32 membershipRateLimit, uint32 index); + + /// @notice Emitted when a membership is erased by its holder during grace period + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index the index of the membership in the membership set + event MembershipErased(uint256 idCommitment, uint32 membershipRateLimit, uint32 index); + + /// @notice Emitted when a membership in its grace period is extended (i.e., is back to Active state) + /// @param idCommitment the idCommitment of the membership + /// @param membershipRateLimit the rate limit of this membership + /// @param index the index of the membership in the membership set + /// @param newGracePeriodStartTimestamp the new grace period start timestamp of this membership + event MembershipExtended( + uint256 idCommitment, uint32 membershipRateLimit, uint32 index, uint256 newGracePeriodStartTimestamp + ); /// @dev contract initializer /// @param _priceCalculator Address of an instance of IPriceCalculator - /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree - /// @param _minRateLimitPerMembership Minimum rate limit of one membership - /// @param _maxRateLimitPerMembership Maximum rate limit of one membership - /// @param _expirationTerm Membership expiration term - /// @param _gracePeriod Membership grace period + /// @param _maxTotalRateLimit Maximum total rate limit of all memberships in the membership set + /// @param _minMembershipRateLimit Minimum rate limit of each membership + /// @param _maxMembershipRateLimit Maximum rate limit of each membership + /// @param _activeDurationForNewMemberships Active state duration of each membership + /// @param _gracePeriodDurationForNewMemberships Grace period duration of each membership function __MembershipUpgradeable_init( address _priceCalculator, - uint32 _maxTotalRateLimitPerEpoch, - uint32 _minRateLimitPerMembership, - uint32 _maxRateLimitPerMembership, - uint32 _expirationTerm, - uint32 _gracePeriod + uint32 _maxTotalRateLimit, + uint32 _minMembershipRateLimit, + uint32 _maxMembershipRateLimit, + uint32 _activeDurationForNewMemberships, + uint32 _gracePeriodDurationForNewMemberships ) internal onlyInitializing { __MembershipUpgradeable_init_unchained( _priceCalculator, - _maxTotalRateLimitPerEpoch, - _minRateLimitPerMembership, - _maxRateLimitPerMembership, - _expirationTerm, - _gracePeriod + _maxTotalRateLimit, + _minMembershipRateLimit, + _maxMembershipRateLimit, + _activeDurationForNewMemberships, + _gracePeriodDurationForNewMemberships ); } function __MembershipUpgradeable_init_unchained( address _priceCalculator, - uint32 _maxTotalRateLimitPerEpoch, - uint32 _minRateLimitPerMembership, - uint32 _maxRateLimitPerMembership, - uint32 _expirationTerm, - uint32 _gracePeriod + uint32 _maxTotalRateLimit, + uint32 _minMembershipRateLimit, + uint32 _maxMembershipRateLimit, + uint32 _activeDurationForNewMemberships, + uint32 _gracePeriodDurationForNewMemberships ) internal onlyInitializing { - require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); - require(_maxRateLimitPerMembership > minRateLimitPerMembership); - require(_minRateLimitPerMembership > 0); - require(_expirationTerm > 0); + require(0 < _minMembershipRateLimit); + require(_minMembershipRateLimit <= _maxMembershipRateLimit); + require(_maxMembershipRateLimit <= _maxTotalRateLimit); + require(_activeDurationForNewMemberships > 0); + // Note: grace period duration may be equal to zero priceCalculator = IPriceCalculator(_priceCalculator); - maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; - maxRateLimitPerMembership = _maxRateLimitPerMembership; - minRateLimitPerMembership = _minRateLimitPerMembership; - expirationTerm = _expirationTerm; - gracePeriod = _gracePeriod; - } - - /// @notice Checks if a user message limit is valid. This does not take into account whether we the total - /// memberships have reached already the `maxTotalRateLimitPerEpoch` - /// @param userMessageLimit The user message limit - /// @return true if the user message limit is valid, false otherwise - function isValidUserMessageLimit(uint32 userMessageLimit) external view returns (bool) { - return userMessageLimit >= minRateLimitPerMembership && userMessageLimit <= maxRateLimitPerMembership; + maxTotalRateLimit = _maxTotalRateLimit; + minMembershipRateLimit = _minMembershipRateLimit; + maxMembershipRateLimit = _maxMembershipRateLimit; + activeDurationForNewMemberships = _activeDurationForNewMemberships; + gracePeriodDurationForNewMemberships = _gracePeriodDurationForNewMemberships; } - /// @dev acquire a membership and trasnfer the fees to the contract - /// @param _sender address of the owner of the new membership - /// @param _idCommitment the idcommitment of the new membership - /// @param _rateLimit the user message limit - /// @param _eraseIfNeeded Erase expired memberships if the `_rateLimit` exceeds the available rate limit - /// @return index the index in the merkle tree - /// @return reusedIndex indicates whether a new leaf is being used or if using an existing leaf in the merkle tree + /// @dev acquire a membership and transfer the deposit to the contract + /// @param _sender the address of the transaction sender + /// @param _idCommitment the idCommitment of the new membership + /// @param _rateLimit the membership rate limit + /// @return index the index of the new membership in the membership set + /// @return indexReused true if the index was reused, false otherwise function _acquireMembership( address _sender, uint256 _idCommitment, - uint32 _rateLimit, - bool _eraseIfNeeded - ) - internal - returns (uint32 index, bool reusedIndex) - { - (address token, uint256 amount) = priceCalculator.calculate(_rateLimit); - (index, reusedIndex) = - _setupMembershipDetails(_sender, _idCommitment, _rateLimit, token, amount, _eraseIfNeeded); - _transferFees(_sender, token, amount); - } - - function _transferFees(address _from, address _token, uint256 _amount) internal { - IERC20(_token).safeTransferFrom(_from, address(this), _amount); - } - - /// @dev Setup a new membership. If there are not enough remaining rate limit to acquire - /// a new membership, it will attempt to erase existing memberships and reuse one of the - /// slots helds by the membership - /// @param _sender holder of the membership. Generally `msg.sender` - /// @param _idCommitment IDCommitment - /// @param _rateLimit User message limit - /// @param _token Address of the token used to acquire the membership - /// @param _amount Amount of the token used to acquire the membership - /// @param _eraseIfNeeded Erase expired memberships if the `_rateLimit` exceeds the available rate limit - /// @return index membership index on the merkle tree - /// @return reusedIndex indicates whether the index returned was a reused slot on the tree or not - function _setupMembershipDetails( - address _sender, - uint256 _idCommitment, - uint32 _rateLimit, - address _token, - uint256 _amount, - bool _eraseIfNeeded + uint32 _rateLimit ) internal - returns (uint32 index, bool reusedIndex) + returns (uint32 index, bool indexReused) { - if (_rateLimit < minRateLimitPerMembership || _rateLimit > maxRateLimitPerMembership) { - revert InvalidRateLimit(); + // Check if the rate limit is valid + if (!isValidMembershipRateLimit(_rateLimit)) { + revert InvalidMembershipRateLimit(); } - // Storing in local variable to not access the storage frequently - // And we're using/modifying these variables in each iteration - uint256 _head = head; - uint256 _tail = tail; - uint256 _totalRateLimitPerEpoch = totalRateLimitPerEpoch; - uint32 _maxTotalRateLimitPerEpoch = maxTotalRateLimitPerEpoch; + currentTotalRateLimit += _rateLimit; // Determine if we exceed the total rate limit - if (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch) { - if (_head == 0 || !_eraseIfNeeded) revert ExceedAvailableMaxRateLimitPerEpoch(); // List is empty or can't - // erase memberships automatically - - // Attempt to free expired membership slots - while (_totalRateLimitPerEpoch + _rateLimit > _maxTotalRateLimitPerEpoch && _head != 0) { - // Determine if there are any available spot in the membership map - // by looking at the oldest membership. If it's expired, we can free it - MembershipInfo memory oldestMembership = members[_head]; - if (!_isExpired(oldestMembership.gracePeriodStartDate, oldestMembership.gracePeriod)) { - revert ExceedAvailableMaxRateLimitPerEpoch(); - } - - emit MemberExpired(_head, oldestMembership.userMessageLimit, oldestMembership.index); - - // Deduct the expired membership rate limit - _totalRateLimitPerEpoch -= oldestMembership.userMessageLimit; - - // Remove the element from the list - delete members[_head]; - - // Promote the next oldest membership to oldest - _head = oldestMembership.next; - - // Move balance from expired membership to holder balance - balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount; - - availableExpiredIndices.push(oldestMembership.index); - } - - // Ensure new head and tail are pointing to the correct memberships - if (_head != 0) { - members[_head].prev = 0; - } else { - _tail = 0; - } + if (currentTotalRateLimit > maxTotalRateLimit) { + revert CannotExceedMaxTotalRateLimit(); } - if (_tail != 0) { - members[_tail].next = _idCommitment; - } else { - // First item - _head = _idCommitment; - } - - // Adding the rate limit of the new registration - _totalRateLimitPerEpoch += _rateLimit; + (address token, uint256 depositAmount) = priceCalculator.calculate(_rateLimit); - // Reuse available slots from previously removed expired memberships - (index, reusedIndex) = _nextIndex(); + // Possibly reuse an index of an erased membership + (index, indexReused) = _getFreeIndex(); - totalRateLimitPerEpoch = _totalRateLimitPerEpoch; - members[_idCommitment] = MembershipInfo({ + memberships[_idCommitment] = MembershipInfo({ holder: _sender, - gracePeriodStartDate: block.timestamp + uint256(expirationTerm), - gracePeriod: gracePeriod, - token: _token, - amount: _amount, - userMessageLimit: _rateLimit, - next: 0, // It's the newest value, so point to nowhere - prev: _tail, + activeDuration: activeDurationForNewMemberships, + gracePeriodStartTimestamp: block.timestamp + uint256(activeDurationForNewMemberships), + gracePeriodDuration: gracePeriodDurationForNewMemberships, + token: token, + depositAmount: depositAmount, + rateLimit: _rateLimit, index: index }); - head = _head; - tail = _idCommitment; + + IERC20(token).safeTransferFrom(_sender, address(this), depositAmount); } - /// @dev reuse available slots from previously removed expired memberships - /// @return index index to use - /// @return reusedIndex indicates whether it is reusing an existing index, or using a new one - function _nextIndex() internal returns (uint32 index, bool reusedIndex) { - // Reuse available slots from previously removed expired memberships - uint256 arrLen = availableExpiredIndices.length; - if (arrLen != 0) { - index = availableExpiredIndices[arrLen - 1]; - availableExpiredIndices.pop(); - reusedIndex = true; + /// @notice Checks if a rate limit is within the allowed bounds + /// @param rateLimit The rate limit + /// @return true if the rate limit is within the allowed bounds, false otherwise + function isValidMembershipRateLimit(uint32 rateLimit) public view returns (bool) { + return minMembershipRateLimit <= rateLimit && rateLimit <= maxMembershipRateLimit; + } + + /// @dev Get a free index (possibly reuse an index of an erased membership) + /// @return index index to be used for the new membership registration + /// @return indexReused indicates whether the index was reused from an erased membership + function _getFreeIndex() internal returns (uint32 index, bool indexReused) { + uint256 numIndices = indicesOfLazilyErasedMemberships.length; + if (numIndices != 0) { + // Reuse the index of the latest erased membership + index = indicesOfLazilyErasedMemberships[numIndices - 1]; + indicesOfLazilyErasedMemberships.pop(); + indexReused = true; } else { - index = nextCommitmentIndex; + index = nextFreeIndex; } } - /// @dev Extend a membership expiration date. Membership must be on grace period - /// @param _sender the address of the holder of the membership + /// @dev Extend a grace-period membership + /// @param _sender the address of the transaction sender /// @param _idCommitment the idCommitment of the membership function _extendMembership(address _sender, uint256 _idCommitment) public { - MembershipInfo storage mdetails = members[_idCommitment]; + MembershipInfo storage membership = memberships[_idCommitment]; - if (!_isGracePeriod(mdetails.gracePeriodStartDate, mdetails.gracePeriod)) { - revert NotInGracePeriod(_idCommitment); + if (!_isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration)) { + revert CannotExtendNonGracePeriodMembership(_idCommitment); } - if (_sender != mdetails.holder) revert NotHolder(_idCommitment); + if (_sender != membership.holder) revert NonHolderCannotExtend(_idCommitment); - uint256 gracePeriodStartDate = block.timestamp + uint256(expirationTerm); + // Note: we add the new active period to the end of the ongoing grace period + uint256 newGracePeriodStartTimestamp = + membership.gracePeriodStartTimestamp + membership.gracePeriodDuration + uint256(membership.activeDuration); - uint256 next = mdetails.next; - uint256 prev = mdetails.prev; - uint256 _tail = tail; - uint256 _head = head; + membership.gracePeriodStartTimestamp = newGracePeriodStartTimestamp; - // Remove current membership references - if (prev != 0) { - members[prev].next = next; - } else { - _head = next; - } + emit MembershipExtended( + _idCommitment, membership.rateLimit, membership.index, membership.gracePeriodStartTimestamp + ); + } - if (next != 0) { - members[next].prev = prev; - } else { - _tail = prev; - } + /// @dev Erase expired memberships or owned grace-period memberships. + /// @param _sender the address of the transaction sender + /// @param _idCommitment idCommitment of the membership to erase + function _eraseMembershipLazily(address _sender, uint256 _idCommitment) internal returns (uint32 index) { + MembershipInfo memory membership = memberships[_idCommitment]; - // Move membership to the end (since it will be the newest) - mdetails.next = 0; - mdetails.prev = _tail; - mdetails.gracePeriodStartDate = gracePeriodStartDate; - mdetails.gracePeriod = gracePeriod; + if (membership.rateLimit == 0) revert MembershipDoesNotExist(_idCommitment); - // Link previous tail with membership that was just extended - if (_tail != 0) { - members[_tail].next = _idCommitment; - } else { - // There are no other items in the list. - // The head will become the extended commitment - _head = _idCommitment; + bool membershipExpired = _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); + bool membershipIsInGracePeriod = + _isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); + bool isHolder = (membership.holder == _sender); + + if (!membershipExpired && !membershipIsInGracePeriod) { + revert CannotEraseActiveMembership(_idCommitment); + } else if (membershipIsInGracePeriod && !isHolder) { + revert NonHolderCannotEraseGracePeriodMembership(_idCommitment); } - head = _head; - tail = _idCommitment; + // Move deposit balance from the membership to be erased to holder deposit balance + depositsToWithdraw[membership.holder][membership.token] += membership.depositAmount; - emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, gracePeriodStartDate); - } + // Deduct the rate limit of this membership from the total rate limit + currentTotalRateLimit -= membership.rateLimit; - /// @dev Determine whether a timestamp is considered to be expired or not after exceeding the grace period - /// @param _gracePeriodStartDate timestamp in which the grace period starts - /// @param _gracePeriod duration of the grace period - function _isExpired(uint256 _gracePeriodStartDate, uint32 _gracePeriod) internal view returns (bool) { - return block.timestamp > _gracePeriodStartDate + uint256(_gracePeriod); - } + // Mark this membership as reusable + indicesOfLazilyErasedMemberships.push(membership.index); - /// @notice Determine if a membership is expired (has exceeded the grace period) - /// @param _idCommitment the idCommitment of the membership - function isExpired(uint256 _idCommitment) public view returns (bool) { - MembershipInfo memory m = members[_idCommitment]; - return _isExpired(m.gracePeriodStartDate, m.gracePeriod); + // Erase this membership from the memberships mapping + delete memberships[_idCommitment]; + + if (membershipExpired) { + emit MembershipExpired(_idCommitment, membership.rateLimit, membership.index); + } + emit MembershipErased(_idCommitment, membership.rateLimit, membership.index); + + // This index will be used to erase the data from the Merkle tree that represents the membership set + return membership.index; } - /// @notice Returns the timestamp on which a membership can be considered expired + /// @notice Determine if a membership is in its grace period /// @param _idCommitment the idCommitment of the membership - function expirationDate(uint256 _idCommitment) public view returns (uint256) { - MembershipInfo memory m = members[_idCommitment]; - return m.gracePeriodStartDate + uint256(m.gracePeriod) + 1; + function isInGracePeriod(uint256 _idCommitment) public view returns (bool) { + MembershipInfo memory membership = memberships[_idCommitment]; + return _isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); } - /// @dev Determine whether a timestamp is considered to be in grace period or not - /// @param _gracePeriodStartDate timestamp in which the grace period starts - /// @param _gracePeriod duration of the grace period - function _isGracePeriod(uint256 _gracePeriodStartDate, uint32 _gracePeriod) internal view returns (bool) { - uint256 blockTimestamp = block.timestamp; - return - blockTimestamp >= _gracePeriodStartDate && blockTimestamp <= _gracePeriodStartDate + uint256(_gracePeriod); + /// @dev Determine whether the current timestamp is within a given period + /// @param _start timestamp in which the period starts (inclusive) + /// @param _duration duration of the period (end timestamp exclusive) + function _isInPeriod(uint256 _start, uint32 _duration) internal view returns (bool) { + uint256 timeNow = block.timestamp; + return (_start <= timeNow && timeNow < _start + uint256(_duration)); } - /// @notice Determine if a membership is in grace period + /// @notice Determine if a membership is expired /// @param _idCommitment the idCommitment of the membership - function isGracePeriod(uint256 _idCommitment) public view returns (bool) { - MembershipInfo memory m = members[_idCommitment]; - return _isGracePeriod(m.gracePeriodStartDate, m.gracePeriod); + function isExpired(uint256 _idCommitment) public view returns (bool) { + MembershipInfo memory membership = memberships[_idCommitment]; + return _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); } - /// @dev Remove expired memberships or owned memberships in grace period. - /// @param _sender address of the sender of transaction (will be used to check memberships in grace period) - /// @param _idCommitment IDCommitment of the membership to erase - function _eraseMembership(address _sender, uint256 _idCommitment, MembershipInfo memory _mdetails) internal { - bool membershipExpired = _isExpired(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod); - bool isGracePeriodAndOwned = - _isGracePeriod(_mdetails.gracePeriodStartDate, _mdetails.gracePeriod) && _mdetails.holder == _sender; - - if (!membershipExpired && !isGracePeriodAndOwned) revert CantEraseMembership(_idCommitment); - - emit MemberExpired(_idCommitment, _mdetails.userMessageLimit, _mdetails.index); - - // Move balance from expired membership to holder balance - balancesToWithdraw[_mdetails.holder][_mdetails.token] += _mdetails.amount; - - // Deduct the expired membership rate limit - totalRateLimitPerEpoch -= _mdetails.userMessageLimit; - - // Remove current membership references - if (_mdetails.prev != 0) { - members[_mdetails.prev].next = _mdetails.next; - } else { - head = _mdetails.next; - } - - if (_mdetails.next != 0) { - members[_mdetails.next].prev = _mdetails.prev; - } else { - tail = _mdetails.prev; - } + /// @dev Determine whether the current timestamp is after a given period + /// @param _start timestamp in which the period starts (inclusive) + /// @param _duration duration of the period (end timestamp exclusive) + function _isAfterPeriod(uint256 _start, uint32 _duration) internal view returns (bool) { + uint256 timeNow = block.timestamp; + return (_timestampAfterPeriod(_start, _duration) <= timeNow); + } - availableExpiredIndices.push(_mdetails.index); + /// @notice Returns the timestamp on which a membership can be considered expired (i.e. when its grace period ends) + /// @param _idCommitment the idCommitment of the membership + function membershipExpirationTimestamp(uint256 _idCommitment) public view returns (uint256) { + MembershipInfo memory membership = memberships[_idCommitment]; + return _timestampAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration); + } - delete members[_idCommitment]; + /// @dev Returns the first timestamp after a specified period + /// @param _start timestamp in which the period starts (inclusive) + /// @param _duration duration of the period (exclusive) + function _timestampAfterPeriod(uint256 _start, uint32 _duration) internal pure returns (uint256) { + return _start + uint256(_duration); } - /// @dev Withdraw any available balance in tokens after a membership is erased. - /// @param _sender the address of the owner of the tokens - /// @param _token the address of the token to withdraw. + /// @dev Withdraw any available deposit balance in tokens after a membership is erased. + /// @param _sender the address of the transaction sender (who withdraws their tokens) + /// @param _token the address of the token to withdraw function _withdraw(address _sender, address _token) internal { require(_token != address(0), "ETH is not allowed"); - uint256 amount = balancesToWithdraw[_sender][_token]; - require(amount > 0, "Insufficient balance"); + uint256 amount = depositsToWithdraw[_sender][_token]; + require(amount > 0, "Insufficient deposit balance"); - balancesToWithdraw[_sender][_token] = 0; + depositsToWithdraw[_sender][_token] = 0; IERC20(_token).safeTransfer(_sender, amount); } diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 0275205..965bbaa 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -11,18 +11,12 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { MembershipUpgradeable } from "./Membership.sol"; import { IPriceCalculator } from "./IPriceCalculator.sol"; -/// The tree is full -error FullTree(); - -/// Member is already registered +/// A membership with this idCommitment is already registered error DuplicateIdCommitment(); /// Invalid idCommitment error InvalidIdCommitment(uint256 idCommitment); -/// Invalid userMessageLimit -error InvalidUserMessageLimit(uint32 messageLimit); - /// Invalid pagination query error InvalidPaginationQuery(uint256 startIndex, uint256 endIndex); @@ -31,32 +25,35 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M uint256 public constant Q = 21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617; - /// @notice The depth of the merkle tree - uint8 public constant DEPTH = 20; + /// @notice The depth of the Merkle tree that stores rate commitments of memberships + uint8 public constant MERKLE_TREE_DEPTH = 20; - /// @notice The size of the merkle tree, i.e 2^depth - uint32 public SET_SIZE; + /// @notice The maximum membership set size is the size of the Merkle tree (2 ^ depth) + uint32 public MAX_MEMBERSHIP_SET_SIZE; - /// @notice the deployed block number + /// @notice The block number at which this contract was deployed uint32 public deployedBlockNumber; - /// @notice the stored imt data - LazyIMTData public imtData; - - /// Emitted when a new member is added to the set - /// @param rateCommitment the rateCommitment of the member - /// @param index The index of the member in the set - event MemberRegistered(uint256 rateCommitment, uint32 index); + /// @notice The Merkle tree that stores rate commitments of memberships + LazyIMTData public merkleTree; - /// @notice the modifier to check if the idCommitment is valid - /// @param idCommitment The idCommitment of the member + /// @notice Сheck if the idCommitment is valid + /// @param idCommitment The idCommitment of the membership modifier onlyValidIdCommitment(uint256 idCommitment) { - if (!isValidCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment); + if (!isValidIdCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment); _; } - modifier noDuplicateMembers(uint256 idCommitment) { - if (members[idCommitment].userMessageLimit != 0) revert DuplicateIdCommitment(); + /// @notice Сheck that the membership with this idCommitment is not already in the membership set + /// @param idCommitment The idCommitment of the membership + modifier noDuplicateMembership(uint256 idCommitment) { + require(!isInMembershipSet(idCommitment), "Duplicate idCommitment: membership already exists"); + _; + } + + /// @notice Check that the membership set is not full + modifier membershipSetNotFull() { + require(nextFreeIndex < MAX_MEMBERSHIP_SET_SIZE, "Membership set is full"); _; } @@ -64,19 +61,19 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M _disableInitializers(); } - /// @dev contract initializer + /// @dev Contract initializer /// @param _priceCalculator Address of an instance of IPriceCalculator - /// @param _maxTotalRateLimitPerEpoch Maximum total rate limit of all memberships in the tree - /// @param _minRateLimitPerMembership Minimum rate limit of one membership - /// @param _maxRateLimitPerMembership Maximum rate limit of one membership - /// @param _expirationTerm Membership expiration term + /// @param _maxTotalRateLimit Maximum total rate limit of all memberships in the membership set + /// @param _minMembershipRateLimit Minimum rate limit of one membership + /// @param _maxMembershipRateLimit Maximum rate limit of one membership + /// @param _activeDuration Membership active duration /// @param _gracePeriod Membership grace period function initialize( address _priceCalculator, - uint32 _maxTotalRateLimitPerEpoch, - uint32 _minRateLimitPerMembership, - uint32 _maxRateLimitPerMembership, - uint32 _expirationTerm, + uint32 _maxTotalRateLimit, + uint32 _minMembershipRateLimit, + uint32 _maxMembershipRateLimit, + uint32 _activeDuration, uint32 _gracePeriod ) public @@ -86,181 +83,181 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M __UUPSUpgradeable_init(); __MembershipUpgradeable_init( _priceCalculator, - _maxTotalRateLimitPerEpoch, - _minRateLimitPerMembership, - _maxRateLimitPerMembership, - _expirationTerm, + _maxTotalRateLimit, + _minMembershipRateLimit, + _maxMembershipRateLimit, + _activeDuration, _gracePeriod ); - SET_SIZE = uint32(1 << DEPTH); + MAX_MEMBERSHIP_SET_SIZE = uint32(1 << MERKLE_TREE_DEPTH); deployedBlockNumber = uint32(block.number); - LazyIMT.init(imtData, DEPTH); - nextCommitmentIndex = 0; + LazyIMT.init(merkleTree, MERKLE_TREE_DEPTH); + nextFreeIndex = 0; } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } // solhint-disable-line - /// @notice Checks if a commitment is valid - /// @param idCommitment The idCommitment of the member - /// @return true if the commitment is valid, false otherwise - function isValidCommitment(uint256 idCommitment) public pure returns (bool) { - return idCommitment != 0 && idCommitment < Q; + /// @notice Checks if an idCommitment is valid (between 0 and Q, both exclusive) + /// @param idCommitment The idCommitment of the membership + /// @return true if the idCommitment is valid, false otherwise + function isValidIdCommitment(uint256 idCommitment) public pure returns (bool) { + return 0 < idCommitment && idCommitment < Q; } - /// @notice Returns the rateCommitment of a member - /// @param index The index of the member - /// @return The rateCommitment of the member - function indexToCommitment(uint32 index) internal view returns (uint256) { - return imtData.elements[LazyIMT.indexForElement(0, index)]; + /// @notice Checks if a membership is in the membership set + /// @param idCommitment The idCommitment of the membership + /// @return true if the membership is in the membership set, false otherwise + function isInMembershipSet(uint256 idCommitment) public view returns (bool) { + (,, uint256 rateCommitment) = getMembershipInfo(idCommitment); + return rateCommitment != 0; } - /// @notice Returns the metadata of a member - /// @param idCommitment The idCommitment of the member - /// @return The metadata of the member (userMessageLimit, index, rateCommitment) - function idCommitmentToMetadata(uint256 idCommitment) public view returns (uint32, uint32, uint256) { - MembershipInfo memory member = members[idCommitment]; - // we cannot call indexToCommitment for 0 index if the member doesn't exist - if (member.userMessageLimit == 0) { + /// @notice Returns the membership info (rate limit, index, rateCommitment) by its idCommitment + /// @param idCommitment The idCommitment of the membership + /// @return The membership info (rateLimit, index, rateCommitment) + function getMembershipInfo(uint256 idCommitment) public view returns (uint32, uint32, uint256) { + MembershipInfo memory membership = memberships[idCommitment]; + // we cannot call getRateCommmitment for 0 index if the membership doesn't exist + if (membership.rateLimit == 0) { return (0, 0, 0); } - return (member.userMessageLimit, member.index, indexToCommitment(member.index)); + return (membership.rateLimit, membership.index, _getRateCommmitment(membership.index)); } - /// @notice Checks if a member exists - /// @param idCommitment The idCommitment of the member - /// @return true if the member exists, false otherwise - function memberExists(uint256 idCommitment) public view returns (bool) { - (,, uint256 rateCommitment) = idCommitmentToMetadata(idCommitment); - return rateCommitment != 0; - } - - /// @notice Allows a user to register as a member - /// @param idCommitment The idCommitment of the member - /// @param userMessageLimit The message limit of the member - function register( - uint256 idCommitment, - uint32 userMessageLimit + /// @notice Returns the rateCommitments of memberships within an index range + /// @param startIndex The start index of the range (inclusive) + /// @param endIndex The end index of the range (inclusive) + /// @return The rateCommitments of the memberships + function getRateCommitmentsInRangeBoundsInclusive( + uint32 startIndex, + uint32 endIndex ) - external - onlyValidIdCommitment(idCommitment) - noDuplicateMembers(idCommitment) + public + view + returns (uint256[] memory) { - uint32 index; - bool reusedIndex; - (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, true); + if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex); + if (endIndex >= nextFreeIndex) revert InvalidPaginationQuery(startIndex, endIndex); + + uint256[] memory rateCommitments = new uint256[](endIndex - startIndex + 1); + for (uint32 i = startIndex; i <= endIndex; i++) { + rateCommitments[i - startIndex] = _getRateCommmitment(i); + } + return rateCommitments; + } - _register(idCommitment, userMessageLimit, index, reusedIndex); + /// @notice Returns the rateCommitment of a membership at a given index + /// @param index The index of the membership in the membership set + /// @return The rateCommitment of the membership + function _getRateCommmitment(uint32 index) internal view returns (uint256) { + return merkleTree.elements[LazyIMT.indexForElement(0, index)]; } - /// @notice Allows a user to register as a member - /// @param idCommitment The idCommitment of the member - /// @param userMessageLimit The message limit of the member - /// @param membershipsToErase List of expired idCommitments to erase + /// @notice Register a membership while erasing some expired memberships to reuse their rate limit + /// @param idCommitment The idCommitment of the new membership + /// @param rateLimit The rate limit of the new membership + /// @param idCommitmentsToErase The list of idCommitments of expired memberships to erase function register( uint256 idCommitment, - uint32 userMessageLimit, - uint256[] calldata membershipsToErase + uint32 rateLimit, + uint256[] calldata idCommitmentsToErase ) external onlyValidIdCommitment(idCommitment) - noDuplicateMembers(idCommitment) + noDuplicateMembership(idCommitment) + membershipSetNotFull { - for (uint256 i = 0; i < membershipsToErase.length; i++) { - uint256 idCommitmentToErase = membershipsToErase[i]; - MembershipInfo memory mdetails = members[idCommitmentToErase]; - if (mdetails.userMessageLimit == 0) revert InvalidIdCommitment(idCommitmentToErase); - _eraseMembership(_msgSender(), idCommitmentToErase, mdetails); - LazyIMT.update(imtData, 0, mdetails.index); - } - - uint32 index; - bool reusedIndex; - (index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, false); - - _register(idCommitment, userMessageLimit, index, reusedIndex); + // erase memberships without overwriting membership set data to zero (save gas) + _eraseMemberships(idCommitmentsToErase, false); + _register(idCommitment, rateLimit); } - /// @dev Registers a member - /// @param idCommitment The idCommitment of the member - /// @param userMessageLimit The message limit of the member - /// @param index Indicates the index in the merkle tree - /// @param reusedIndex indicates whether we're inserting a new element in the merkle tree or updating a existing - /// leaf - function _register(uint256 idCommitment, uint32 userMessageLimit, uint32 index, bool reusedIndex) internal { - if (nextCommitmentIndex >= SET_SIZE) revert FullTree(); - - uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); - if (reusedIndex) { - LazyIMT.update(imtData, rateCommitment, index); + /// @dev Register a membership (internal function) + /// @param idCommitment The idCommitment of the membership + /// @param rateLimit The rate limit of the membership + function _register(uint256 idCommitment, uint32 rateLimit) internal { + (uint32 index, bool indexReused) = _acquireMembership(_msgSender(), idCommitment, rateLimit); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, rateLimit]); + if (indexReused) { + LazyIMT.update(merkleTree, rateCommitment, index); } else { - LazyIMT.insert(imtData, rateCommitment); - nextCommitmentIndex += 1; + LazyIMT.insert(merkleTree, rateCommitment); + nextFreeIndex += 1; } - emit MemberRegistered(rateCommitment, index); + emit MembershipRegistered(idCommitment, rateLimit, index); } - /// @notice Returns the commitments of a range of members - /// @param startIndex The start index of the range - /// @param endIndex The end index of the range - /// @return The commitments of the members - function getCommitments(uint32 startIndex, uint32 endIndex) public view returns (uint256[] memory) { - if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex); - if (endIndex > nextCommitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex); - - uint256[] memory commitments = new uint256[](endIndex - startIndex + 1); - for (uint32 i = startIndex; i <= endIndex; i++) { - commitments[i - startIndex] = indexToCommitment(i); - } - return commitments; - } - - /// @notice Returns the root of the IMT - /// @return The root of the IMT + /// @notice Returns the root of the Merkle tree that stores rate commitments of memberships + /// @return The root of the Merkle tree that stores rate commitments of memberships function root() external view returns (uint256) { - return LazyIMT.root(imtData, DEPTH); + return LazyIMT.root(merkleTree, MERKLE_TREE_DEPTH); } - /// @notice Returns the merkle proof elements of a given membership - /// @param index The index of the member - /// @return The merkle proof elements of the member - function merkleProofElements(uint40 index) public view returns (uint256[DEPTH] memory) { - uint256[DEPTH] memory castedProof; - uint256[] memory proof = LazyIMT.merkleProofElements(imtData, index, DEPTH); - for (uint8 i = 0; i < DEPTH; i++) { - castedProof[i] = proof[i]; + /// @notice Returns the Merkle proof that a given membership is in the membership set + /// @param index The index of the membership + /// @return The Merkle proof (an array of MERKLE_TREE_DEPTH elements) + function getMerkleProof(uint40 index) public view returns (uint256[MERKLE_TREE_DEPTH] memory) { + uint256[] memory dynamicSizeProof = LazyIMT.merkleProofElements(merkleTree, index, MERKLE_TREE_DEPTH); + uint256[MERKLE_TREE_DEPTH] memory fixedSizeProof; + for (uint8 i = 0; i < MERKLE_TREE_DEPTH; i++) { + fixedSizeProof[i] = dynamicSizeProof[i]; } - return castedProof; + return fixedSizeProof; } - /// @notice Extend a membership expiration date. Memberships must be on grace period - /// @param idCommitments list of idcommitments - function extend(uint256[] calldata idCommitments) external { + /// @notice Extend a grace-period membership under the same conditions + /// @param idCommitments list of idCommitments of memberships to extend + function extendMemberships(uint256[] calldata idCommitments) external { for (uint256 i = 0; i < idCommitments.length; i++) { - uint256 idCommitment = idCommitments[i]; - _extendMembership(_msgSender(), idCommitment); + _extendMembership(_msgSender(), idCommitments[i]); } } - /// @notice Remove expired memberships or owned memberships in grace period. - /// The user can determine offchain which expired memberships slots - /// are available, and proceed to free them. - /// This is also used to erase memberships in grace period if they're - /// held by the sender. The sender can then withdraw the tokens. - /// @param idCommitments list of idcommitments of the memberships + /// @notice Erase expired memberships or owned grace-period memberships + /// The user can select expired memberships offchain, and proceed to erase them. + /// The holder can use this function to erase their own grace-period memberships. + /// The holder can then withdraw the deposited tokens. + /// @param idCommitments The list of idCommitments of the memberships to erase + /// set function eraseMemberships(uint256[] calldata idCommitments) external { - for (uint256 i = 0; i < idCommitments.length; i++) { - uint256 idCommitment = idCommitments[i]; - MembershipInfo memory mdetails = members[idCommitment]; - if (mdetails.userMessageLimit == 0) revert InvalidIdCommitment(idCommitment); - _eraseMembership(_msgSender(), idCommitment, mdetails); - LazyIMT.update(imtData, 0, mdetails.index); + _eraseMemberships(idCommitments, false); + } + + /// @notice Erase expired memberships or owned grace-period memberships + /// Optionally, also erase rate commitment data from the membership set (clean-up). + /// Compared to eraseMemberships(idCommitments), + /// this function decreases Merkle tree size and spends more gas (if eraseFromMembershipSet == true). + /// @param idCommitments The list of idCommitments of the memberships to erase + /// @param eraseFromMembershipSet Indicates whether to erase membership data from the membership set + function eraseMemberships(uint256[] calldata idCommitments, bool eraseFromMembershipSet) external { + _eraseMemberships(idCommitments, eraseFromMembershipSet); + } + + /// @dev Erase memberships from the list of idCommitments + /// @param idCommitmentsToErase The idCommitments of memberships to erase from storage + /// @param eraseFromMembershipSet Indicates whether to erase membership data from the membership set + function _eraseMemberships(uint256[] calldata idCommitmentsToErase, bool eraseFromMembershipSet) internal { + // eraseFromMembershipSet == true means full clean-up. + // Erase memberships from memberships array (free up the rate limit and index), + // and erase the rate commitment from the membership set (reduce the Merkle tree size). + // eraseFromMembershipSet == false means lazy erasure. + // Only erase memberships from the memberships array (consume less gas). + // Merkle tree data will be overwritten when the correspondind index is reused. + for (uint256 i = 0; i < idCommitmentsToErase.length; i++) { + // Erase the membership from the memberships array in contract storage + uint32 indexToErase = _eraseMembershipLazily(_msgSender(), idCommitmentsToErase[i]); + // Optionally, also erase the rate commitment data from the membership set. + // This does not affect the total rate limit control, or index reusal for new membership registrations. + if (eraseFromMembershipSet) { + LazyIMT.update(merkleTree, 0, indexToErase); + } } } - /// @notice Withdraw any available balance in tokens after a membership is erased. - /// @param token The address of the token to withdraw. Use 0x000...000 to withdraw ETH + /// @notice Withdraw any available deposit balance in tokens after a membership is erased + /// @param token The address of the token to withdraw function withdraw(address token) external { _withdraw(_msgSender(), token); } @@ -271,37 +268,39 @@ contract WakuRlnV2 is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, M priceCalculator = IPriceCalculator(_priceCalculator); } - /// @notice Set the maximum total rate limit of all memberships in the tree - /// @param _maxTotalRateLimitPerEpoch new value - function setMaxTotalRateLimitPerEpoch(uint32 _maxTotalRateLimitPerEpoch) external onlyOwner { - require(_maxTotalRateLimitPerEpoch >= maxRateLimitPerMembership); - maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; + /// @notice Set the maximum total rate limit of all memberships in the membership set + /// @param _maxTotalRateLimit new maximum total rate limit (messages per epoch) + function setMaxTotalRateLimit(uint32 _maxTotalRateLimit) external onlyOwner { + require(maxMembershipRateLimit <= _maxTotalRateLimit); + maxTotalRateLimit = _maxTotalRateLimit; } /// @notice Set the maximum rate limit of one membership - /// @param _maxRateLimitPerMembership new value - function setMaxRateLimitPerMembership(uint32 _maxRateLimitPerMembership) external onlyOwner { - require(_maxRateLimitPerMembership >= minRateLimitPerMembership); - maxRateLimitPerMembership = _maxRateLimitPerMembership; + /// @param _maxMembershipRateLimit new maximum rate limit per membership (messages per epoch) + function setMaxMembershipRateLimit(uint32 _maxMembershipRateLimit) external onlyOwner { + require(minMembershipRateLimit <= _maxMembershipRateLimit); + maxMembershipRateLimit = _maxMembershipRateLimit; } /// @notice Set the minimum rate limit of one membership - /// @param _minRateLimitPerMembership new value - function setMinRateLimitPerMembership(uint32 _minRateLimitPerMembership) external onlyOwner { - require(_minRateLimitPerMembership > 0); - minRateLimitPerMembership = _minRateLimitPerMembership; + /// @param _minMembershipRateLimit new minimum rate limit per membership (messages per epoch) + function setMinMembershipRateLimit(uint32 _minMembershipRateLimit) external onlyOwner { + require(_minMembershipRateLimit > 0); + require(_minMembershipRateLimit <= maxMembershipRateLimit); + minMembershipRateLimit = _minMembershipRateLimit; } - /// @notice Set the membership expiration term - /// @param _expirationTerm new value - function setExpirationTerm(uint32 _expirationTerm) external onlyOwner { - require(_expirationTerm > 0); - expirationTerm = _expirationTerm; + /// @notice Set the active duration for new memberships (terms of existing memberships don't change) + /// @param _activeDurationForNewMembership new active duration + function setActiveDuration(uint32 _activeDurationForNewMembership) external onlyOwner { + require(_activeDurationForNewMembership > 0); + activeDurationForNewMemberships = _activeDurationForNewMembership; } - /// @notice Set the membership grace period - /// @param _gracePeriod new value - function setGracePeriod(uint32 _gracePeriod) external onlyOwner { - gracePeriod = _gracePeriod; + /// @notice Set the grace period for new memberships (terms of existing memberships don't change) + /// @param _gracePeriodDurationForNewMembership new grace period duration + function setGracePeriodDuration(uint32 _gracePeriodDurationForNewMembership) external onlyOwner { + // Note: grace period duration may be equal to zero + gracePeriodDurationForNewMemberships = _gracePeriodDurationForNewMembership; } } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 4818887..49c7898 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -22,6 +22,8 @@ contract WakuRlnV2Test is Test { address internal deployer; + uint256[] noIdCommitmentsToErase = new uint256[](0); + function setUp() public virtual { token = new TestToken(); @@ -37,19 +39,19 @@ contract WakuRlnV2Test is Test { vm.pauseGasMetering(); // Merkle tree leaves are calculated using 2 as rateLimit vm.prank(w.owner()); - w.setMinRateLimitPerMembership(2); + w.setMinMembershipRateLimit(2); uint256 idCommitment = 2; - uint32 userMessageLimit = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 2; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); vm.pauseGasMetering(); - assertEq(w.nextCommitmentIndex(), 1); - assertEq(w.memberExists(idCommitment), true); - (,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment); - assertEq(fetchedUserMessageLimit, userMessageLimit); + assertEq(w.nextFreeIndex(), 1); + assertEq(w.isInMembershipSet(idCommitment), true); + (,,,, uint32 membershipRateLimit1, uint32 index, address holder,) = w.memberships(idCommitment); + assertEq(membershipRateLimit1, membershipRateLimit); assertEq(holder, address(this)); assertEq(index, 0); // kats from zerokit @@ -59,12 +61,11 @@ contract WakuRlnV2Test is Test { w.root(), 13_801_897_483_540_040_307_162_267_952_866_411_686_127_372_014_953_358_983_481_592_640_000_001_877_295 ); - (uint32 fetchedUserMessageLimit2, uint32 index2, uint256 rateCommitment2) = - w.idCommitmentToMetadata(idCommitment); - assertEq(fetchedUserMessageLimit2, userMessageLimit); + (uint32 membershipRateLimit2, uint32 index2, uint256 rateCommitment2) = w.getMembershipInfo(idCommitment); + assertEq(membershipRateLimit2, membershipRateLimit); assertEq(index2, 0); assertEq(rateCommitment2, rateCommitment); - uint256[20] memory proof = w.merkleProofElements(0); + uint256[20] memory proof = w.getMerkleProof(0); uint256[20] memory expectedProof = [ 0, 14_744_269_619_966_411_208_579_211_824_598_458_697_587_494_354_926_760_081_771_325_075_741_142_829_156, @@ -93,93 +94,58 @@ contract WakuRlnV2Test is Test { vm.resumeGasMetering(); } - function test__ValidRegistration(uint32 userMessageLimit) external { + function test__ValidRegistration(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); - uint256 minUserMessageLimit = w.minRateLimitPerMembership(); - uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); - vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); + uint256 minMembershipRateLimit = w.minMembershipRateLimit(); + uint256 maxMembershipRateLimit = w.maxMembershipRateLimit(); + vm.assume(minMembershipRateLimit <= membershipRateLimit && membershipRateLimit <= maxMembershipRateLimit); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); - assertEq(w.memberExists(idCommitment), false); + assertEq(w.isInMembershipSet(idCommitment), false); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, membershipRateLimit]); - (uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) = - w.idCommitmentToMetadata(idCommitment); - assertEq(fetchedUserMessageLimit, userMessageLimit); + (uint32 fetchedMembershipRateLimit, uint32 index, uint256 fetchedRateCommitment) = + w.getMembershipInfo(idCommitment); + assertEq(fetchedMembershipRateLimit, membershipRateLimit); assertEq(index, 0); assertEq(fetchedRateCommitment, rateCommitment); assertEq(token.balanceOf(address(w)), price); - assertEq(w.totalRateLimitPerEpoch(), userMessageLimit); - } - - function test__InsertionNormalOrder(uint32 idCommitmentsLength) external { - vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 50); - - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); - - // Register some commitments - for (uint256 i = 0; i < idCommitmentsLength; i++) { - uint256 idCommitment = i + 1; - token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); - // new membership will always be the tail - assertEq(next, 0); - assertEq(w.tail(), idCommitment); - // current membership prevLink will always point to previous membership - assertEq(prev, idCommitment - 1); - } - assertEq(w.head(), 1); - assertEq(w.tail(), idCommitmentsLength); - - // Ensure that prev and next are chained correctly - for (uint256 i = 0; i < idCommitmentsLength; i++) { - uint256 idCommitment = i + 1; - (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); - - assertEq(prev, idCommitment - 1); - if (i == idCommitmentsLength - 1) { - assertEq(next, 0); - } else { - assertEq(next, idCommitment + 1); - } - } + assertEq(w.currentTotalRateLimit(), membershipRateLimit); } - function test__LinearPriceCalculation(uint32 userMessageLimit) external view { + function test__LinearPriceCalculation(uint32 membershipRateLimit) external view { IPriceCalculator priceCalculator = w.priceCalculator(); uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerEpoch(); assertNotEq(pricePerMessagePerPeriod, 0); - uint256 expectedPrice = uint256(userMessageLimit) * pricePerMessagePerPeriod; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint256 expectedPrice = uint256(membershipRateLimit) * pricePerMessagePerPeriod; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); assertEq(price, expectedPrice); } - function test__InvalidTokenAmount(uint256 idCommitment, uint32 userMessageLimit) external { + function test__InvalidTokenAmount(uint256 idCommitment, uint32 membershipRateLimit) external { vm.pauseGasMetering(); - uint256 minUserMessageLimit = w.minRateLimitPerMembership(); - uint256 maxUserMessageLimit = w.maxRateLimitPerMembership(); - vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit); - vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint256 minMembershipRateLimit = w.minMembershipRateLimit(); + uint256 maxMembershipRateLimit = w.maxMembershipRateLimit(); + vm.assume(minMembershipRateLimit <= membershipRateLimit && membershipRateLimit <= maxMembershipRateLimit); + vm.assume(w.isValidIdCommitment(idCommitment) && w.isValidMembershipRateLimit(membershipRateLimit)); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price - 1); vm.expectRevert(bytes("ERC20: insufficient allowance")); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } function test__IdCommitmentToMetadata__DoesntExist() external view { uint256 idCommitment = 2; - (uint32 userMessageLimit, uint32 index, uint256 rateCommitment) = w.idCommitmentToMetadata(idCommitment); - assertEq(userMessageLimit, 0); + (uint32 membershipRateLimit, uint32 index, uint256 rateCommitment) = w.getMembershipInfo(idCommitment); + assertEq(membershipRateLimit, 0); assertEq(index, 0); assertEq(rateCommitment, 0); } @@ -187,546 +153,404 @@ contract WakuRlnV2Test is Test { function test__InvalidRegistration__InvalidIdCommitment__Zero() external { vm.pauseGasMetering(); uint256 idCommitment = 0; - uint32 userMessageLimit = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 2; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0)); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external { vm.pauseGasMetering(); - uint32 userMessageLimit = 20; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 20; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); uint256 idCommitment = w.Q() + 1; token.approve(address(w), price); vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment)); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } - function test__InvalidRegistration__InvalidUserMessageLimit__MinMax() external { + function test__InvalidRegistration__InvalidMembershipRateLimit__MinMax() external { uint256 idCommitment = 2; - uint32 invalidMin = w.minRateLimitPerMembership() - 1; - uint32 invalidMax = w.maxRateLimitPerMembership() + 1; + uint32 invalidMin = w.minMembershipRateLimit() - 1; + uint32 invalidMax = w.maxMembershipRateLimit() + 1; - vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(idCommitment, invalidMin); + vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector)); + w.register(idCommitment, invalidMin, noIdCommitmentsToErase); - vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(idCommitment, invalidMax); + vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector)); + w.register(idCommitment, invalidMax, noIdCommitmentsToErase); } - function test__ValidRegistrationExtend(uint32 userMessageLimit) external { + function test__ValidRegistrationExtend(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment); - assertFalse(w.isGracePeriod(idCommitment)); + assertFalse(w.isInGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); - vm.warp(gracePeriodStartDate); + vm.warp(gracePeriodStartTimestamp); - assertTrue(w.isGracePeriod(idCommitment)); + assertTrue(w.isInGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); - // Registering other memberships just to check linkage is correct - for (uint256 i = 1; i < 5; i++) { - token.approve(address(w), price); - w.register(idCommitment + i, userMessageLimit); - } - - assertEq(w.head(), idCommitment); - uint256[] memory commitmentsToExtend = new uint256[](1); commitmentsToExtend[0] = idCommitment; // Attempt to extend the membership (but it is not owned by us) address randomAddress = vm.addr(block.timestamp); vm.prank(randomAddress); - vm.expectRevert(abi.encodeWithSelector(NotHolder.selector, commitmentsToExtend[0])); - w.extend(commitmentsToExtend); + vm.expectRevert(abi.encodeWithSelector(NonHolderCannotExtend.selector, commitmentsToExtend[0])); + w.extendMemberships(commitmentsToExtend); // Attempt to extend the membership (but now we are the owner) vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExtended(idCommitment, 0, 0, 0); - w.extend(commitmentsToExtend); + emit MembershipUpgradeable.MembershipExtended(idCommitment, 0, 0, 0); - (,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment); + (, uint256 oldActiveDuration, uint256 oldGracePeriodStartTimestamp, uint32 oldGracePeriodDuration,,,,) = + w.memberships(idCommitment); + w.extendMemberships(commitmentsToExtend); + (, uint256 newActiveDuration, uint256 newGracePeriodStartTimestamp, uint32 newGracePeriodDuration,,,,) = + w.memberships(idCommitment); - assertEq(block.timestamp + uint256(w.expirationTerm()), newGracePeriodStartDate); - assertFalse(w.isGracePeriod(idCommitment)); + assertEq(oldActiveDuration, newActiveDuration); + assertEq(oldGracePeriodDuration, newGracePeriodDuration); + assertEq( + oldGracePeriodStartTimestamp + oldGracePeriodDuration + newActiveDuration, newGracePeriodStartTimestamp + ); + assertFalse(w.isInGracePeriod(idCommitment)); assertFalse(w.isExpired(idCommitment)); - // Verify list order is correct - assertEq(w.tail(), idCommitment); - assertEq(w.head(), idCommitment + 1); - - // Ensure that prev and next are chained correctly - for (uint256 i = 0; i < 5; i++) { - uint256 currIdCommitment = idCommitment + i; - (uint256 prev, uint256 next,,,,,,,) = w.members(currIdCommitment); - console.log("idCommitment: %s - prev: %s - next: %s", currIdCommitment, prev, next); - if (i == 0) { - // Verifying links of extended idCommitment - assertEq(next, 0); - assertEq(prev, idCommitment + 4); - } else if (i == 1) { - // The second element in the chain became the oldest - assertEq(next, currIdCommitment + 1); - assertEq(prev, 0); - } else if (i == 4) { - assertEq(prev, currIdCommitment - 1); - assertEq(next, idCommitment); - } else { - // The rest of the elements maintain their order - assertEq(prev, currIdCommitment - 1); - assertEq(next, currIdCommitment + 1); - } - } - // Attempt to extend a non grace period membership + token.approve(address(w), price); + w.register(idCommitment + 1, membershipRateLimit, noIdCommitmentsToErase); commitmentsToExtend[0] = idCommitment + 1; - vm.expectRevert(abi.encodeWithSelector(NotInGracePeriod.selector, commitmentsToExtend[0])); - w.extend(commitmentsToExtend); + vm.expectRevert(abi.encodeWithSelector(CannotExtendNonGracePeriodMembership.selector, commitmentsToExtend[0])); + w.extendMemberships(commitmentsToExtend); } - function test__ValidRegistrationExtendSingleMembership(uint32 userMessageLimit) external { + function test__ValidRegistrationNoGracePeriod(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); + + vm.startPrank(w.owner()); + w.setGracePeriodDuration(0); + vm.stopPrank(); + vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - vm.warp(gracePeriodStartDate); + (,, uint256 gracePeriodStartTimestamp, uint32 gracePeriodDuration,,,,) = w.memberships(idCommitment); + + assertEq(gracePeriodDuration, 0); + + assertFalse(w.isInGracePeriod(idCommitment)); + assertFalse(w.isExpired(idCommitment)); + + uint256 expectedExpirationTimestamp = gracePeriodStartTimestamp + uint256(gracePeriodDuration); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); + + assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp); + + vm.warp(membershipExpirationTimestamp); + + assertFalse(w.isInGracePeriod(idCommitment)); + assertTrue(w.isExpired(idCommitment)); + } + + function test__ValidRegistrationExtendSingleMembership(uint32 membershipRateLimit) external { + vm.pauseGasMetering(); + uint256 idCommitment = 2; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); + vm.assume( + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() + ); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); + vm.resumeGasMetering(); + + token.approve(address(w), price); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + uint256 ogExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment); + + vm.warp(gracePeriodStartTimestamp); uint256[] memory commitmentsToExtend = new uint256[](1); commitmentsToExtend[0] = idCommitment; // Extend the membership vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExtended(idCommitment, 0, 0, 0); - w.extend(commitmentsToExtend); - - // Verify list order is correct - assertEq(w.tail(), idCommitment); - assertEq(w.head(), idCommitment); - (uint256 prev, uint256 next,,,,,,,) = w.members(idCommitment); - assertEq(next, 0); - assertEq(prev, 0); + emit MembershipUpgradeable.MembershipExtended(idCommitment, 0, 0, 0); + w.extendMemberships(commitmentsToExtend); + + (,, uint256 newGracePeriodStartTimestamp, uint32 newGracePeriodDuration,,,,) = w.memberships(idCommitment); + uint256 expectedExpirationTimestamp = newGracePeriodStartTimestamp + uint256(newGracePeriodDuration); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); + assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp); + assertTrue(expectedExpirationTimestamp > ogExpirationTimestamp); } - function test__ValidRegistrationExpiry(uint32 userMessageLimit) external { + function test__ValidRegistrationExpiry(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - (,,, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) = w.members(idCommitment); + (,, uint256 fetchedgracePeriodStartTimestamp, uint32 fetchedGracePeriod,,,,) = w.memberships(idCommitment); - uint256 expectedExpirationDate = fetchedGracePeriodStartDate + uint256(fetchedGracePeriod) + 1; - uint256 expirationDate = w.expirationDate(idCommitment); + uint256 expectedExpirationTimestamp = fetchedgracePeriodStartTimestamp + uint256(fetchedGracePeriod); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment); - assertEq(expectedExpirationDate, expirationDate); + assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp); - vm.warp(expirationDate); + vm.warp(membershipExpirationTimestamp); - assertFalse(w.isGracePeriod(idCommitment)); + assertFalse(w.isInGracePeriod(idCommitment)); assertTrue(w.isExpired(idCommitment)); - - // Registering other memberships just to check linkage is correct - for (uint256 i = 1; i <= 5; i++) { - token.approve(address(w), price); - w.register(idCommitment + i, userMessageLimit); - } - - assertEq(w.head(), idCommitment); - assertEq(w.tail(), idCommitment + 5); } function test__ValidRegistrationWithEraseList() external { vm.pauseGasMetering(); vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(20); - w.setMaxRateLimitPerMembership(100); - w.setMaxTotalRateLimitPerEpoch(100); + w.setMinMembershipRateLimit(20); + w.setMaxMembershipRateLimit(100); + w.setMaxTotalRateLimit(100); vm.stopPrank(); vm.resumeGasMetering(); - (, uint256 price) = w.priceCalculator().calculate(20); + (, uint256 priceA) = w.priceCalculator().calculate(20); for (uint256 i = 1; i <= 5; i++) { - token.approve(address(w), price); - w.register(i, 20); + token.approve(address(w), priceA); + w.register(i, 20, noIdCommitmentsToErase); // Make sure they're expired - vm.warp(w.expirationDate(i)); + vm.warp(w.membershipExpirationTimestamp(i)); } - // Time travel to a point in which the last commitment is active - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(5); - vm.warp(gracePeriodStartDate - 1); + // Time travel to a point in which the last membership is active + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(5); + vm.warp(gracePeriodStartTimestamp - 1); // Ensure that this is the case assertTrue(w.isExpired(4)); assertFalse(w.isExpired(5)); - assertFalse(w.isGracePeriod(5)); + assertFalse(w.isInGracePeriod(5)); - (, price) = w.priceCalculator().calculate(60); - token.approve(address(w), price); + (, uint256 priceB) = w.priceCalculator().calculate(60); + token.approve(address(w), priceB); - // Attempt to expire 3 commitments including one that can't be erased (the last one) + // Should fail. There's not enough free rate limit + vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector)); + w.register(6, 60, noIdCommitmentsToErase); + + // Attempt to erase 3 memberships including one that can't be erased (the last one) uint256[] memory commitmentsToErase = new uint256[](3); commitmentsToErase[0] = 1; commitmentsToErase[1] = 2; commitmentsToErase[2] = 5; // This one is still active - token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, 5)); + token.approve(address(w), priceB); + vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, 5)); w.register(6, 60, commitmentsToErase); - // Attempt to expire 3 commitments that can be erased + // Attempt to erase 3 memberships that can be erased commitmentsToErase[2] = 4; vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(1, 0, 0); + emit MembershipUpgradeable.MembershipExpired(1, 0, 0); vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(2, 0, 0); + emit MembershipUpgradeable.MembershipExpired(2, 0, 0); vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(4, 0, 0); + emit MembershipUpgradeable.MembershipExpired(4, 0, 0); w.register(6, 60, commitmentsToErase); // Ensure that the chosen memberships were erased and others unaffected address holder; - (,,,,,,, holder,) = w.members(1); + (,,,,,, holder,) = w.memberships(1); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(2); + (,,,,,, holder,) = w.memberships(2); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(3); + (,,,,,, holder,) = w.memberships(3); assertEq(holder, address(this)); - (,,,,,,, holder,) = w.members(4); + (,,,,,, holder,) = w.memberships(4); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(5); + (,,,,,, holder,) = w.memberships(5); assertEq(holder, address(this)); - (,,,,,,, holder,) = w.members(6); + (,,,,,, holder,) = w.memberships(6); assertEq(holder, address(this)); + + // The balance available for withdrawal should match the amount of the expired membership + uint256 availableBalance = w.depositsToWithdraw(address(this), address(token)); + assertEq(availableBalance, priceA * 3); } function test__RegistrationWhenMaxRateLimitIsReached() external { vm.pauseGasMetering(); vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); + w.setMinMembershipRateLimit(1); + w.setMaxMembershipRateLimit(5); + w.setMaxTotalRateLimit(5); vm.stopPrank(); vm.resumeGasMetering(); - bool isValid = w.isValidUserMessageLimit(6); + bool isValid = w.isValidMembershipRateLimit(6); assertFalse(isValid); - // Exceeds the max rate limit per user - uint32 userMessageLimit = 10; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + // Exceeds the max rate limit per membership + uint32 membershipRateLimit = 10; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector)); - w.register(1, userMessageLimit); + vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector)); + w.register(1, membershipRateLimit, noIdCommitmentsToErase); // Should register succesfully - userMessageLimit = 4; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 4; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - w.register(2, userMessageLimit); + w.register(2, membershipRateLimit, noIdCommitmentsToErase); // Exceeds the rate limit - userMessageLimit = 2; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 2; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(3, userMessageLimit); + vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector)); + w.register(3, membershipRateLimit, noIdCommitmentsToErase); // Should register succesfully - userMessageLimit = 1; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 1; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - w.register(3, userMessageLimit); + w.register(3, membershipRateLimit, noIdCommitmentsToErase); // We ran out of rate limit again - userMessageLimit = 1; - (, price) = w.priceCalculator().calculate(userMessageLimit); + membershipRateLimit = 1; + (, price) = w.priceCalculator().calculate(membershipRateLimit); token.approve(address(w), price); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(4, userMessageLimit); - } - - function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external { - vm.pauseGasMetering(); - vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); - vm.stopPrank(); - vm.resumeGasMetering(); - - uint32 userMessageLimitA = 2; - uint32 totalUserMessageLimit = userMessageLimitA; - (, uint256 priceA) = w.priceCalculator().calculate(userMessageLimitA); - token.approve(address(w), priceA); - w.register(1, userMessageLimitA); - - (,,, uint256 gracePeriodStartDate,,, uint32 indexA,,) = w.members(1); - vm.warp(gracePeriodStartDate + 1); - - // Exceeds the rate limit, but if the first were expired, it should register - // It is in grace period so can't be erased - assertTrue(w.isGracePeriod(1)); - assertFalse(w.isExpired(1)); - uint32 userMessageLimitB = 4; - (, uint256 priceB) = w.priceCalculator().calculate(userMessageLimitB); - (, priceB) = w.priceCalculator().calculate(userMessageLimitB); - token.approve(address(w), priceB); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(2, userMessageLimitB); - - // FFW until the membership is expired so we can get rid of it - uint256 expirationDate = w.expirationDate(1); - vm.warp(expirationDate); - assertTrue(w.isExpired(1)); - - // It should succeed now - vm.expectEmit(); - emit MembershipUpgradeable.MemberExpired(1, userMessageLimitA, indexA); - w.register(2, userMessageLimitB); - - // The previous expired membership should have been erased - (,,,,,,, address holder,) = w.members(1); - assertEq(holder, address(0)); - - uint32 expectedUserMessageLimit = totalUserMessageLimit - userMessageLimitA + userMessageLimitB; - assertEq(expectedUserMessageLimit, w.totalRateLimitPerEpoch()); - - // The new commitment should be the only element in the list - assertEq(w.head(), 2); - assertEq(w.tail(), 2); - (uint256 prev, uint256 next,,,,, uint32 indexB,,) = w.members(2); - assertEq(prev, 0); - assertEq(next, 0); - - // Index should have been reused - assertEq(indexA, indexB); - - // The balance available for withdrawal should match the amount of the expired membership - uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); - assertEq(availableBalance, priceA); - } - - function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() external { - vm.pauseGasMetering(); - vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); - vm.stopPrank(); - vm.resumeGasMetering(); - - (, uint256 priceA) = w.priceCalculator().calculate(1); - token.approve(address(w), priceA); - w.register(1, 1); - vm.warp(block.timestamp + 100); - token.approve(address(w), priceA); - w.register(2, 1); - vm.warp(block.timestamp + 100); - uint256 expirationDate = w.expirationDate(2); - vm.warp(expirationDate); - token.approve(address(w), priceA); - w.register(3, 1); - - // Make sure only the first 2 memberships are expired - assertTrue(w.isExpired(1)); - assertTrue(w.isExpired(2)); - assertFalse(w.isExpired(3) || w.isGracePeriod(3)); - - (,,,,,, uint32 index1,,) = w.members(1); - (,,,,,, uint32 index2,,) = w.members(2); - - // Attempt to register a membership that will require to expire 2 memberships - // Currently there is 2 available, and we want to register 4 - // If we remove first membership, we'll have 3 available - // If we also remove the second, we'll have 4 available - (, uint256 priceB) = w.priceCalculator().calculate(4); - token.approve(address(w), priceB); - vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(1, 0, 0); - vm.expectEmit(true, false, false, false); - emit MembershipUpgradeable.MemberExpired(2, 0, 0); - w.register(4, 4); - - // idCommitment4 will use the last removed index available (since we push to an array) - (,,,,,, uint32 index4,,) = w.members(4); - assertEq(index4, index2); - - // the index of the first removed membership is still available for further registrations - assertEq(index1, w.availableExpiredIndices(0)); - - // The previous expired memberships should have been erased - (,,,,,,, address holder,) = w.members(1); - assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(2); - assertEq(holder, address(0)); - - // The total rate limit used should be those from idCommitment 3 and 4 - assertEq(5, w.totalRateLimitPerEpoch()); - - // There should only be 2 memberships, the non expired and the new one - assertEq(w.head(), 3); - assertEq(w.tail(), 4); - (uint256 prev, uint256 next,,,,,,,) = w.members(3); - assertEq(prev, 0); - assertEq(next, 4); - (prev, next,,,,,,,) = w.members(4); - assertEq(prev, 3); - assertEq(next, 0); - - // The balance available for withdrawal should match the amount of the expired membership - uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); - assertEq(availableBalance, priceA * 2); - } - - function test__RegistrationWhenMaxRateLimitReachedAndMultipleExpiredMembersAvailableWithoutEnoughRateLimit() - external - { - vm.pauseGasMetering(); - vm.startPrank(w.owner()); - w.setMinRateLimitPerMembership(1); - w.setMaxRateLimitPerMembership(5); - w.setMaxTotalRateLimitPerEpoch(5); - vm.stopPrank(); - vm.resumeGasMetering(); - - (, uint256 priceA) = w.priceCalculator().calculate(1); - token.approve(address(w), priceA); - w.register(1, 1); - vm.warp(block.timestamp + 100); - token.approve(address(w), priceA); - w.register(2, 1); - vm.warp(block.timestamp + 100); - uint256 expirationDate = w.expirationDate(2); - vm.warp(expirationDate); - token.approve(address(w), priceA); - w.register(3, 1); - - // Make sure only the first 2 memberships are expired - assertTrue(w.isExpired(1)); - assertTrue(w.isExpired(2)); - assertFalse(w.isExpired(3) || w.isGracePeriod(3)); - - // Attempt to register a membership that will require to expire 2 memberships - // Currently there is 2 available, and we want to register 5 - // If we remove first membership, we'll have 3 available - // If we also remove the second, we'll have 4 available, but it is still not enough - // for registering - (, uint256 priceB) = w.priceCalculator().calculate(5); - token.approve(address(w), priceB); - vm.expectRevert(abi.encodeWithSelector(ExceedAvailableMaxRateLimitPerEpoch.selector)); - w.register(4, 5); + vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector)); + w.register(4, membershipRateLimit, noIdCommitmentsToErase); } function test__indexReuse_eraseMemberships(uint32 idCommitmentsLength) external { - vm.assume(idCommitmentsLength > 0 && idCommitmentsLength < 50); + vm.assume(0 < idCommitmentsLength && idCommitmentsLength < 50); (, uint256 price) = w.priceCalculator().calculate(20); uint32 index; uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength); + uint256 time = block.timestamp; for (uint256 i = 1; i <= idCommitmentsLength; i++) { token.approve(address(w), price); - w.register(i, 20); - (,,,,,, index,,) = w.members(i); - assertEq(index, w.nextCommitmentIndex() - 1); + w.register(i, 20, noIdCommitmentsToErase); + (,,,,, index,,) = w.memberships(i); + assertEq(index, w.nextFreeIndex() - 1); commitmentsToErase[i - 1] = i; + time += 100; + vm.warp(time); } + // None of the commitments can be deleted because they're still active + uint256[] memory singleCommitmentToErase = new uint256[](1); + for (uint256 i = 1; i <= idCommitmentsLength; i++) { + singleCommitmentToErase[0] = i; + vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, i)); + w.eraseMemberships(singleCommitmentToErase); + } + + // Fastfwd to commitment grace period, and try to erase it without being the owner + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(1); + vm.warp(gracePeriodStartTimestamp); + assertTrue(w.isInGracePeriod(1)); + singleCommitmentToErase[0] = 1; + address randomAddress = vm.addr(block.timestamp); + vm.prank(randomAddress); + vm.expectRevert(abi.encodeWithSelector(NonHolderCannotEraseGracePeriodMembership.selector, 1)); + w.eraseMemberships(singleCommitmentToErase); + // time travel to the moment we can erase all expired memberships - uint256 expirationDate = w.expirationDate(idCommitmentsLength); - vm.warp(expirationDate); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitmentsLength); + vm.warp(membershipExpirationTimestamp); w.eraseMemberships(commitmentsToErase); // Verify that expired indices match what we expect for (uint32 i = 0; i < idCommitmentsLength; i++) { - assertEq(i, w.availableExpiredIndices(i)); + assertEq(i, w.indicesOfLazilyErasedMemberships(i)); } - uint32 currnextCommitmentIndex = w.nextCommitmentIndex(); + uint32 expectedNextFreeIndex = w.nextFreeIndex(); for (uint256 i = 1; i <= idCommitmentsLength; i++) { uint256 idCommitment = i + 10; - uint256 expectedReusedIndexPos = idCommitmentsLength - i; - uint32 expectedIndex = w.availableExpiredIndices(expectedReusedIndexPos); + uint256 expectedindexReusedPos = idCommitmentsLength - i; + uint32 expectedReusedIndex = w.indicesOfLazilyErasedMemberships(expectedindexReusedPos); token.approve(address(w), price); - w.register(idCommitment, 20); - (,,,,,, index,,) = w.members(idCommitment); - assertEq(expectedIndex, index); + w.register(idCommitment, 20, noIdCommitmentsToErase); + (,,,,, index,,) = w.memberships(idCommitment); + assertEq(expectedReusedIndex, index); // Should have been removed from the list vm.expectRevert(); - w.availableExpiredIndices(expectedReusedIndexPos); + w.indicesOfLazilyErasedMemberships(expectedindexReusedPos); // Should not have been affected - assertEq(currnextCommitmentIndex, w.nextCommitmentIndex()); + assertEq(expectedNextFreeIndex, w.nextFreeIndex()); } - // No indexes should be available for reuse + // No indices should be available for reuse vm.expectRevert(); - w.availableExpiredIndices(0); + w.indicesOfLazilyErasedMemberships(0); - // Should use a new index since we got rid of all available indexes + // Should use a new index since we got rid of all reusable indexes token.approve(address(w), price); - w.register(100, 20); - (,,,,,, index,,) = w.members(100); - assertEq(index, currnextCommitmentIndex); - assertEq(currnextCommitmentIndex + 1, w.nextCommitmentIndex()); + w.register(100, 20, noIdCommitmentsToErase); + (,,,,, index,,) = w.memberships(100); + assertEq(index, expectedNextFreeIndex); + assertEq(expectedNextFreeIndex + 1, w.nextFreeIndex()); } - function test__RemoveExpiredMemberships(uint32 userMessageLimit) external { + function test__RemoveExpiredMemberships(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); uint256 time = block.timestamp; for (uint256 i = 0; i < 5; i++) { token.approve(address(w), price); - w.register(idCommitment + i, userMessageLimit); + w.register(idCommitment + i, membershipRateLimit, noIdCommitmentsToErase); time += 100; vm.warp(time); } - // Expiring the first 3 - uint256 expirationDate = w.expirationDate(idCommitment + 2); - vm.warp(expirationDate); + // Expiring the first 3 memberships + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment + 2); + vm.warp(membershipExpirationTimestamp); for (uint256 i = 0; i < 5; i++) { if (i <= 2) { assertTrue(w.isExpired(idCommitment + i)); @@ -740,61 +564,46 @@ contract WakuRlnV2Test is Test { commitmentsToErase[1] = idCommitment + 2; vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExpired(commitmentsToErase[0], 0, 0); + emit MembershipUpgradeable.MembershipExpired(commitmentsToErase[0], 0, 0); vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExpired(commitmentsToErase[0], 0, 0); + emit MembershipUpgradeable.MembershipExpired(commitmentsToErase[0], 0, 0); w.eraseMemberships(commitmentsToErase); address holder; - (,,,,,,, holder,) = w.members(idCommitment + 1); + (,,,,,, holder,) = w.memberships(idCommitment + 1); assertEq(holder, address(0)); - (,,,,,,, holder,) = w.members(idCommitment + 2); + (,,,,,, holder,) = w.memberships(idCommitment + 2); assertEq(holder, address(0)); - // Verify list order is correct - uint256 prev; - uint256 next; - (prev, next,,,,,,,) = w.members(idCommitment); - assertEq(prev, 0); - assertEq(next, idCommitment + 3); - (prev, next,,,,,,,) = w.members(idCommitment + 3); - assertEq(prev, idCommitment); - assertEq(next, idCommitment + 4); - (prev, next,,,,,,,) = w.members(idCommitment + 4); - assertEq(prev, idCommitment + 3); - assertEq(next, 0); - assertEq(w.head(), idCommitment); - assertEq(w.tail(), idCommitment + 4); - // Attempting to call erase when some of the commitments can't be erased yet // idCommitment can be erased (in grace period), but idCommitment + 4 is still active - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment + 4); - vm.warp(gracePeriodStartDate - 1); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment + 4); + vm.warp(gracePeriodStartTimestamp - 1); commitmentsToErase[0] = idCommitment; commitmentsToErase[1] = idCommitment + 4; - vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, idCommitment + 4)); + vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, idCommitment + 4)); w.eraseMemberships(commitmentsToErase); } function test__RemoveAllExpiredMemberships(uint32 idCommitmentsLength) external { vm.pauseGasMetering(); - vm.assume(idCommitmentsLength > 1 && idCommitmentsLength <= 100); - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + vm.assume(1 < idCommitmentsLength && idCommitmentsLength <= 100); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); uint256 time = block.timestamp; for (uint256 i = 1; i <= idCommitmentsLength; i++) { token.approve(address(w), price); - w.register(i, userMessageLimit); + w.register(i, membershipRateLimit, noIdCommitmentsToErase); time += 100; vm.warp(time); } - uint256 expirationDate = w.expirationDate(idCommitmentsLength); - vm.warp(expirationDate); + uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitmentsLength); + vm.warp(membershipExpirationTimestamp); for (uint256 i = 1; i <= 5; i++) { assertTrue(w.isExpired(i)); } @@ -803,60 +612,44 @@ contract WakuRlnV2Test is Test { for (uint256 i = 0; i < idCommitmentsLength; i++) { commitmentsToErase[i] = i + 1; vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment) - emit MembershipUpgradeable.MemberExpired(i + 1, 0, 0); + emit MembershipUpgradeable.MembershipExpired(i + 1, 0, 0); } w.eraseMemberships(commitmentsToErase); - // No memberships registered - assertEq(w.head(), 0); - assertEq(w.tail(), 0); - - for (uint256 i = 10; i <= idCommitmentsLength + 10; i++) { - token.approve(address(w), price); - w.register(i, userMessageLimit); - assertEq(w.tail(), i); + // Erased memberships are gone! + for (uint256 i = 0; i < commitmentsToErase.length; i++) { + (,,,, uint32 fetchedMembershipRateLimit,,,) = w.memberships(commitmentsToErase[i]); + assertEq(fetchedMembershipRateLimit, 0); } - - // Verify list order is correct - assertEq(w.head(), 10); - assertEq(w.tail(), idCommitmentsLength + 10); - uint256 prev; - uint256 next; - (prev, next,,,,,,,) = w.members(10); - assertEq(prev, 0); - assertEq(next, 11); - (prev, next,,,,,,,) = w.members(idCommitmentsLength + 10); - assertEq(prev, idCommitmentsLength + 9); - assertEq(next, 0); } - function test__WithdrawToken(uint32 userMessageLimit) external { + function test__WithdrawToken(uint32 membershipRateLimit) external { vm.pauseGasMetering(); uint256 idCommitment = 2; LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator())); vm.prank(priceCalculator.owner()); priceCalculator.setTokenAndPrice(address(token), 5 wei); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); token.mint(address(this), price); vm.assume( - userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership() + w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit() ); - vm.assume(w.isValidUserMessageLimit(userMessageLimit)); + vm.assume(w.isValidMembershipRateLimit(membershipRateLimit)); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); - (,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment); + (,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment); - vm.warp(gracePeriodStartDate); + vm.warp(gracePeriodStartTimestamp); uint256[] memory commitmentsToErase = new uint256[](1); commitmentsToErase[0] = idCommitment; w.eraseMemberships(commitmentsToErase); - uint256 availableBalance = w.balancesToWithdraw(address(this), address(token)); + uint256 availableBalance = w.depositsToWithdraw(address(this), address(token)); assertEq(availableBalance, price); assertEq(token.balanceOf(address(w)), price); @@ -867,7 +660,7 @@ contract WakuRlnV2Test is Test { uint256 balanceAfterWithdraw = token.balanceOf(address(this)); - availableBalance = w.balancesToWithdraw(address(this), address(token)); + availableBalance = w.depositsToWithdraw(address(this), address(token)); assertEq(availableBalance, 0); assertEq(token.balanceOf(address(w)), 0); assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw); @@ -876,29 +669,29 @@ contract WakuRlnV2Test is Test { function test__InvalidRegistration__DuplicateIdCommitment() external { vm.pauseGasMetering(); uint256 idCommitment = 2; - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); token.approve(address(w), price); - vm.expectRevert(DuplicateIdCommitment.selector); - w.register(idCommitment, userMessageLimit); + vm.expectRevert(bytes("Duplicate idCommitment: membership already exists")); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); } function test__InvalidRegistration__FullTree() external { vm.pauseGasMetering(); - uint32 userMessageLimit = 20; - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = 20; + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); // we progress the tree to the last leaf /*| Name | Type | Slot | Offset | Bytes | |---------------------|-----------------------------------------------------|------|--------|-------| - | nextCommitmentIndex | uint32 | 256 | 0 | 4 | */ + | nextFreeIndex | uint32 | 256 | 0 | 4 | */ /* Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout @@ -916,55 +709,55 @@ contract WakuRlnV2Test is Test { If the storage layout changes, update the next line accordingly */ - // we set nextCommitmentIndex to 4294967295 (1 << 20) = 0x00100000 + // we set nextFreeIndex to 4294967295 (1 << 20) = 0x00100000 vm.store(address(w), bytes32(uint256(256)), 0x0000000000000000000000000000000000000000000000000000000000100000); token.approve(address(w), price); - vm.expectRevert(FullTree.selector); - w.register(1, userMessageLimit); + vm.expectRevert(bytes("Membership set is full")); + w.register(1, membershipRateLimit, noIdCommitmentsToErase); } function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0)); - w.getCommitments(1, 0); + w.getRateCommitmentsInRangeBoundsInclusive(1, 0); } - function test__InvalidPaginationQuery__EndIndexGTnextCommitmentIndex() external { + function test__InvalidPaginationQuery__EndIndexGTNextFreeIndex() external { vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 0, 2)); - w.getCommitments(0, 2); + w.getRateCommitmentsInRangeBoundsInclusive(0, 2); } function test__ValidPaginationQuery__OneElement() external { vm.pauseGasMetering(); uint256 idCommitment = 1; - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); vm.resumeGasMetering(); token.approve(address(w), price); - w.register(idCommitment, userMessageLimit); - uint256[] memory commitments = w.getCommitments(0, 0); + w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase); + uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(0, 0); assertEq(commitments.length, 1); - uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, membershipRateLimit]); assertEq(commitments[0], rateCommitment); } function test__ValidPaginationQuery(uint32 idCommitmentsLength) external { vm.pauseGasMetering(); - vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100); - uint32 userMessageLimit = w.minRateLimitPerMembership(); - (, uint256 price) = w.priceCalculator().calculate(userMessageLimit); + vm.assume(0 < idCommitmentsLength && idCommitmentsLength <= 100); + uint32 membershipRateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(membershipRateLimit); - for (uint256 i = 0; i < idCommitmentsLength; i++) { + for (uint256 i = 0; i <= idCommitmentsLength; i++) { token.approve(address(w), price); - w.register(i + 1, userMessageLimit); + w.register(i + 1, membershipRateLimit, noIdCommitmentsToErase); } vm.resumeGasMetering(); - uint256[] memory commitments = w.getCommitments(0, idCommitmentsLength); - assertEq(commitments.length, idCommitmentsLength + 1); + uint256[] memory rateCommitments = w.getRateCommitmentsInRangeBoundsInclusive(0, idCommitmentsLength - 1); + assertEq(rateCommitments.length, idCommitmentsLength); for (uint256 i = 0; i < idCommitmentsLength; i++) { - uint256 rateCommitment = PoseidonT3.hash([i + 1, userMessageLimit]); - assertEq(commitments[i], rateCommitment); + uint256 rateCommitment = PoseidonT3.hash([i + 1, membershipRateLimit]); + assertEq(rateCommitments[i], rateCommitment); } }