mirror of
https://github.com/logos-messaging/logos-messaging-rlnv2-contract.git
synced 2026-01-07 08:23:09 +00:00
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:
parent
522599c704
commit
6db38e367f
@ -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)
|
||||
@ -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`
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user