refactor: do not keep track of membership registration order

* unify terminology for mdetails
* more DRY in LinearPriceCalculator
* improve terminology consistency (#16)
* refactor, minor fixes
* do not reset ongoing grace period on extension (cf. spec change)
* minor renaming, comments
* refactor, group functions by funcitonality
* extract membership expiration calculation to internal function
* save active duration per membership (must carry over after extension)
* optional lazy erasure from membership set
* minor fixes in tests
* fix: off-by-one: end index can't be equal to next free index
* minor refactoring, comments
* define period boundaries: start inclusive, end exclusive
* separate eraseMembership functions to user-focused (lazy) and admin-focused (tree cleanup)
* minor fix to maintain line lengths
* unify membership-related events
* add test for  zero grace period
* fix typo in comment

Co-authored-by: richΛrd <info@richardramos.me>

---------

Co-authored-by: richΛrd <info@richardramos.me>
This commit is contained in:
Richard Ramos 2024-09-18 16:36:32 -04:00
parent 522599c704
commit 6db38e367f
No known key found for this signature in database
GPG Key ID: 1CE87DB518195760
5 changed files with 752 additions and 1060 deletions

View File

@ -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)
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)

View File

@ -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 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) external onlyOwner {
_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) 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`

View File

@ -6,21 +6,24 @@ 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 ExceededMaxTotalRateLimit();
// This membership is not in grace period yet
// This membership is not in its grace period
error NotInGracePeriod(uint256 idCommitment);
// The sender is not the holder of the membership
error NotHolder(uint256 idCommitment);
// The sender is not the holder of this membership
error AttemptedExtensionByNonHolder(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);
// This membership cannot be erased
error CannotEraseMembership(uint256 idCommitment);
// This membership does not exist
error MembershipDoesNotExist(uint256 idCommitment);
abstract contract MembershipUpgradeable is Initializable {
using SafeERC20 for IERC20;
@ -28,415 +31,318 @@ 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;
uint32 public minMembershipRateLimit;
/// @notice Membership billing period
uint32 public expirationTerm;
/// @notice Membership active period duration (A in the spec)
uint32 public activeDurationForNewMemberships;
/// @notice Membership grace period
uint32 public gracePeriod;
/// @notice Membership grace period duration (G in the spec)
uint32 public gracePeriodDurationForNewMemberships;
/// @notice balances available to withdraw
mapping(address holder => mapping(address token => uint256 balance)) public balancesToWithdraw;
/// @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 Total rate limit of all memberships in the tree
uint256 public totalRateLimitPerEpoch;
/// @notice Current total rate limit of all memberships in the membership set (messages per epoch)
uint256 public currentTotalRateLimit;
/// @notice List of registered memberships
mapping(uint256 idCommitment => MembershipInfo member) public members;
/// @notice List of memberships in the membership set
mapping(uint256 idCommitment => MembershipInfo membership) public memberships;
/// @notice The index on the merkle tree for the next member to be registered
uint32 public nextCommitmentIndex;
/// @notice The index in the membership set for the next membership to be registered
uint32 public nextFreeIndex;
/// @notice track available indices that are available due to expired memberships being removed
uint32[] public availableExpiredIndices;
/// @dev Oldest membership
uint256 public head = 0;
/// @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);
/// 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 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);
/// @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;
maxTotalRateLimit = _maxTotalRateLimit;
minMembershipRateLimit = _minMembershipRateLimit;
maxMembershipRateLimit = _maxMembershipRateLimit;
activeDurationForNewMemberships = _activeDurationForNewMemberships;
gracePeriodDurationForNewMemberships = _gracePeriodDurationForNewMemberships;
}
/// @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;
}
/// @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 trasnfer 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
uint32 _rateLimit
)
internal
returns (uint32 index, bool reusedIndex)
returns (uint32 index, bool indexReused)
{
(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
)
internal
returns (uint32 index, bool reusedIndex)
{
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 ExceededMaxTotalRateLimit();
}
if (_tail != 0) {
members[_tail].next = _idCommitment;
} else {
// First item
_head = _idCommitment;
}
(address token, uint256 depositAmount) = priceCalculator.calculate(_rateLimit);
// Adding the rate limit of the new registration
_totalRateLimitPerEpoch += _rateLimit;
// Possibly reuse an index of an erased membership
(index, indexReused) = _getFreeIndex();
// Reuse available slots from previously removed expired memberships
(index, reusedIndex) = _nextIndex();
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)) {
if (!_isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration)) {
revert NotInGracePeriod(_idCommitment);
}
if (_sender != mdetails.holder) revert NotHolder(_idCommitment);
if (_sender != membership.holder) revert AttemptedExtensionByNonHolder(_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;
}
if (next != 0) {
members[next].prev = prev;
} else {
_tail = prev;
}
// Move membership to the end (since it will be the newest)
mdetails.next = 0;
mdetails.prev = _tail;
mdetails.gracePeriodStartDate = gracePeriodStartDate;
mdetails.gracePeriod = gracePeriod;
// 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;
}
head = _head;
tail = _idCommitment;
emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, gracePeriodStartDate);
emit MembershipExtended(
_idCommitment, membership.rateLimit, membership.index, membership.gracePeriodStartTimestamp
);
}
/// @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);
/// @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];
if (membership.rateLimit == 0) revert MembershipDoesNotExist(_idCommitment);
bool membershipExpired = _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
bool membershipIsInGracePeriod =
_isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
bool isHolder = (membership.holder == _sender);
if (!membershipExpired && !(membershipIsInGracePeriod && isHolder)) {
revert CannotEraseMembership(_idCommitment);
}
// Move deposit balance from the membership to be erased to holder deposit balance
depositsToWithdraw[membership.holder][membership.token] += membership.depositAmount;
// Deduct the rate limit of this membership from the total rate limit
currentTotalRateLimit -= membership.rateLimit;
// Mark this membership as reusable
indicesOfLazilyErasedMemberships.push(membership.index);
// 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 Determine if a membership is expired (has exceeded the grace period)
/// @notice Determine if a membership is in its grace period
/// @param _idCommitment the idCommitment of the membership
function isInGracePeriod(uint256 _idCommitment) public view returns (bool) {
MembershipInfo memory membership = memberships[_idCommitment];
return _isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
}
/// @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 expired
/// @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);
MembershipInfo memory membership = memberships[_idCommitment];
return _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
}
/// @notice Returns the timestamp on which a membership can be considered expired
/// @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);
}
/// @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 expirationDate(uint256 _idCommitment) public view returns (uint256) {
MembershipInfo memory m = members[_idCommitment];
return m.gracePeriodStartDate + uint256(m.gracePeriod) + 1;
function membershipExpirationTimestamp(uint256 _idCommitment) public view returns (uint256) {
MembershipInfo memory membership = memberships[_idCommitment];
return _timestampAfterPeriod(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 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);
}
/// @notice Determine if a membership is in grace period
/// @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);
}
/// @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;
}
availableExpiredIndices.push(_mdetails.index);
delete members[_idCommitment];
}
/// @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);
}

View File

@ -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,52 +25,56 @@ 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;
/// @notice The Merkle tree that stores rate commitments of memberships
LazyIMTData public merkleTree;
/// 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 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");
_;
}
constructor() {
_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 +84,196 @@ 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 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) {
return (0, 0, 0);
}
return (member.userMessageLimit, member.index, indexToCommitment(member.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);
/// @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 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
)
external
onlyValidIdCommitment(idCommitment)
noDuplicateMembers(idCommitment)
{
uint32 index;
bool reusedIndex;
(index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, true);
_register(idCommitment, userMessageLimit, index, reusedIndex);
}
/// @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
function register(
uint256 idCommitment,
uint32 userMessageLimit,
uint256[] calldata membershipsToErase
)
external
onlyValidIdCommitment(idCommitment)
noDuplicateMembers(idCommitment)
{
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);
/// @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);
}
uint32 index;
bool reusedIndex;
(index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, false);
_register(idCommitment, userMessageLimit, index, reusedIndex);
return (membership.rateLimit, membership.index, _getRateCommmitment(membership.index));
}
/// @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);
} else {
LazyIMT.insert(imtData, rateCommitment);
nextCommitmentIndex += 1;
}
emit MemberRegistered(rateCommitment, 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) {
/// @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
)
public
view
returns (uint256[] memory)
{
if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex);
if (endIndex > nextCommitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex);
if (endIndex >= nextFreeIndex) revert InvalidPaginationQuery(startIndex, endIndex);
uint256[] memory commitments = new uint256[](endIndex - startIndex + 1);
uint256[] memory rateCommitments = new uint256[](endIndex - startIndex + 1);
for (uint32 i = startIndex; i <= endIndex; i++) {
commitments[i - startIndex] = indexToCommitment(i);
rateCommitments[i - startIndex] = _getRateCommmitment(i);
}
return commitments;
return rateCommitments;
}
/// @notice Returns the root of the IMT
/// @return The root of the IMT
/// @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 Register a membership
/// @param idCommitment The idCommitment of the new membership
/// @param rateLimit The rate limit of the new membership
function register(
uint256 idCommitment,
uint32 rateLimit
)
external
onlyValidIdCommitment(idCommitment)
noDuplicateMembership(idCommitment)
membershipSetNotFull
{
_register(idCommitment, rateLimit);
}
/// @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 rateLimit,
uint256[] calldata idCommitmentsToErase
)
external
onlyValidIdCommitment(idCommitment)
noDuplicateMembership(idCommitment)
membershipSetNotFull
{
// erase memberships without overwriting membership set data to zero (save gas)
_eraseMemberships(idCommitmentsToErase, false);
_register(idCommitment, rateLimit);
}
/// @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(merkleTree, rateCommitment);
nextFreeIndex += 1;
}
emit MembershipRegistered(idCommitment, rateLimit, index);
}
/// @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 +284,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;
}
}

File diff suppressed because it is too large Load Diff