feat: membership (#13)

This commit is contained in:
richΛrd 2024-10-23 12:22:32 -04:00 committed by GitHub
parent 1c72717bc9
commit afb8585f62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 2730 additions and 1300 deletions

View File

@ -1,14 +1,24 @@
WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 16726)
WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18249)
WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16130)
WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 99502)
WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 14328)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 15229)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 14004)
WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() (gas: 17703)
WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__Zero() (gas: 14085)
WakuRlnV2Test:test__Upgrade() (gas: 3791109)
WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1000, μ: 445155, ~: 159972)
WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 119773)
WakuRlnV2Test:test__ValidRegistration(uint256,uint32) (runs: 1000, μ: 124408, ~: 124408)
WakuRlnV2Test:test__ValidRegistration__kats() (gas: 96616)
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

@ -69,7 +69,14 @@ $ forge coverage
#### Deploy to Anvil:
```sh
$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545
$ TOKEN_ADDRESS=0x1122334455667788990011223344556677889900 forge script script/Deploy.s.sol --broadcast --rpc-url localhost --tc Deploy
```
Replace the `TOKEN_ADDRESS` value by a token address you have deployed on anvil. A `TestToken` is available in
`test/TestToken.sol` and can be deployed with
```sh
forge script test/TestToken.sol --broadcast --rpc-url localhost --tc TestTokenFactory
```
For this script to work, you need to have a `MNEMONIC` environment variable set to a valid

2453
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,21 +2,38 @@
pragma solidity >=0.8.19 <=0.9.0;
import { WakuRlnV2 } from "../src/WakuRlnV2.sol";
import { LinearPriceCalculator } from "../src/LinearPriceCalculator.sol";
import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol";
import { LazyIMT } from "@zk-kit/imt.sol/LazyIMT.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";
contract Deploy is BaseScript {
function run() public broadcast returns (WakuRlnV2 w, address impl) {
address _token = _getTokenAddress();
return deploy(_token);
}
function deploy(address _token) public returns (WakuRlnV2 w, address impl) {
address priceCalcAddr = address(new LinearPriceCalculator(_token, 0.05 ether));
impl = address(new WakuRlnV2());
bytes memory data = abi.encodeCall(WakuRlnV2.initialize, 100);
bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 180 days, 30 days));
address proxy = address(new ERC1967Proxy(impl, data));
w = WakuRlnV2(proxy);
}
function _getTokenAddress() internal view returns (address) {
try vm.envAddress("TOKEN_ADDRESS") returns (address passedAddress) {
return passedAddress;
} catch {
if (block.chainid == 1) {
return 0x6B175474E89094C44Da98b954EedeAC495271d0F; // DAI address on mainnet
} else {
revert("no TOKEN_ADDRESS was specified");
}
}
}
}
contract DeployLibs is BaseScript {

10
src/IPriceCalculator.sol Normal file
View File

@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IPriceCalculator {
/// Returns the token and price to pay in `token` for some `_rateLimit`
/// @param _rateLimit the rate limit the user wants to acquire
/// @return address of the erc20 token
/// @return uint price to pay for acquiring the specified `_rateLimit`
function calculate(uint32 _rateLimit) external view returns (address, uint256);
}

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol";
import { IPriceCalculator } from "./IPriceCalculator.sol";
/// Address 0x0000...0000 was used instead of an ERC20 token address
error OnlyTokensAllowed();
/// @title Linear Price Calculator to determine the price to acquire a membership
contract LinearPriceCalculator is IPriceCalculator, Ownable {
/// @notice Address of the ERC20 token accepted by this contract.
address public token;
/// @notice The price per message per epoch
uint256 public pricePerMessagePerEpoch;
constructor(address _token, uint256 _pricePerMessagePerEpoch) Ownable() {
_setTokenAndPrice(_token, _pricePerMessagePerEpoch);
}
/// Set accepted token and price per message per epoch per period
/// @param _token The token accepted by RLN membership management
/// @param _pricePerMessagePerEpoch Price per message per epoch
function setTokenAndPrice(address _token, uint256 _pricePerMessagePerEpoch) external onlyOwner {
_setTokenAndPrice(_token, _pricePerMessagePerEpoch);
}
/// Set accepted token and price per message per epoch per period
/// @param _token The token accepted by 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 = _pricePerMessagePerEpoch;
}
/// Returns the token and price to pay in `token` for some `_rateLimit`
/// @param _rateLimit the rate limit the user wants to acquire
/// @return address of the erc20 token
/// @return uint price to pay for acquiring the specified `_rateLimit`
function calculate(uint32 _rateLimit) external view returns (address, uint256) {
return (token, uint256(_rateLimit) * pricePerMessagePerEpoch);
}
}

360
src/Membership.sol Normal file
View File

@ -0,0 +1,360 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { IPriceCalculator } from "./IPriceCalculator.sol";
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 rate limit is outside the expected limits
error InvalidMembershipRateLimit();
// Cannot acquire the rate limit for a new membership due to exceeding the expected limits
// even after attempting to erase expired memberships
error CannotExceedMaxTotalRateLimit();
// The membership is not in its grace period (cannot be extended)
error CannotExtendNonGracePeriodMembership(uint256 idCommitment);
// The sender is not the holder of this membership (cannot extend)
error NonHolderCannotExtend(uint256 idCommitment);
// The membership is still active (cannot be erased)
error CannotEraseActiveMembership(uint256 idCommitment);
// The sender is not the holder of this membership (cannot erase)
error NonHolderCannotEraseGracePeriodMembership(uint256 idCommitment);
// The membership does not exist
error MembershipDoesNotExist(uint256 idCommitment);
abstract contract MembershipUpgradeable is Initializable {
using SafeERC20 for IERC20;
/// @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 membership set (messages per epoch)
uint32 public maxTotalRateLimit;
/// @notice Maximum rate limit of one membership
uint32 public maxMembershipRateLimit;
/// @notice Minimum rate limit of one membership
uint32 public minMembershipRateLimit;
/// @notice Membership active period duration (A in the spec)
uint32 public activeDurationForNewMemberships;
/// @notice Membership grace period duration (G in the spec)
uint32 public gracePeriodDurationForNewMemberships;
/// @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 Current total rate limit of all memberships in the membership set (messages per epoch)
uint256 public currentTotalRateLimit;
/// @notice List of memberships in the membership set
mapping(uint256 idCommitment => MembershipInfo membership) public memberships;
/// @notice The index in the membership set for the next membership to be registered
uint32 public nextFreeIndex;
/// @notice Indices of erased memberships that can be reused for new registrations
uint32[] public indicesOfLazilyErasedMemberships;
struct MembershipInfo {
/// @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 the address of the holder of this membership
address holder;
/// @notice the token used to make the deposit to register this membership
address token;
}
/// Emitted when a new membership is added to the membership set
/// @param idCommitment the idCommitment of the membership
/// @param membershipRateLimit the rate limit of this membership
/// @param index The index of the membership in the membership set
event MembershipRegistered(uint256 idCommitment, uint256 membershipRateLimit, uint32 index);
/// @notice Emitted when a membership is expired (exceeded its grace period and not extended)
/// @param idCommitment the idCommitment of the membership
/// @param membershipRateLimit the rate limit of this membership
/// @param index the index of the membership in the membership set
event MembershipExpired(uint256 idCommitment, uint32 membershipRateLimit, uint32 index);
/// @notice Emitted when a membership is erased by its holder during grace period
/// @param idCommitment the idCommitment of the membership
/// @param membershipRateLimit the rate limit of this membership
/// @param index the index of the membership in the membership set
event MembershipErased(uint256 idCommitment, uint32 membershipRateLimit, uint32 index);
/// @notice Emitted when a membership in its grace period is extended (i.e., is back to Active state)
/// @param idCommitment the idCommitment of the membership
/// @param membershipRateLimit the rate limit of this membership
/// @param index the index of the membership in the membership set
/// @param newGracePeriodStartTimestamp the new grace period start timestamp of this membership
event MembershipExtended(
uint256 idCommitment, uint32 membershipRateLimit, uint32 index, uint256 newGracePeriodStartTimestamp
);
/// @dev contract initializer
/// @param _priceCalculator Address of an instance of IPriceCalculator
/// @param _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 _maxTotalRateLimit,
uint32 _minMembershipRateLimit,
uint32 _maxMembershipRateLimit,
uint32 _activeDurationForNewMemberships,
uint32 _gracePeriodDurationForNewMemberships
)
internal
onlyInitializing
{
__MembershipUpgradeable_init_unchained(
_priceCalculator,
_maxTotalRateLimit,
_minMembershipRateLimit,
_maxMembershipRateLimit,
_activeDurationForNewMemberships,
_gracePeriodDurationForNewMemberships
);
}
function __MembershipUpgradeable_init_unchained(
address _priceCalculator,
uint32 _maxTotalRateLimit,
uint32 _minMembershipRateLimit,
uint32 _maxMembershipRateLimit,
uint32 _activeDurationForNewMemberships,
uint32 _gracePeriodDurationForNewMemberships
)
internal
onlyInitializing
{
require(0 < _minMembershipRateLimit);
require(_minMembershipRateLimit <= _maxMembershipRateLimit);
require(_maxMembershipRateLimit <= _maxTotalRateLimit);
require(_activeDurationForNewMemberships > 0);
// Note: grace period duration may be equal to zero
priceCalculator = IPriceCalculator(_priceCalculator);
maxTotalRateLimit = _maxTotalRateLimit;
minMembershipRateLimit = _minMembershipRateLimit;
maxMembershipRateLimit = _maxMembershipRateLimit;
activeDurationForNewMemberships = _activeDurationForNewMemberships;
gracePeriodDurationForNewMemberships = _gracePeriodDurationForNewMemberships;
}
/// @dev acquire a membership and transfer the deposit to the contract
/// @param _sender the address of the transaction sender
/// @param _idCommitment the idCommitment of the new membership
/// @param _rateLimit the membership rate limit
/// @return index the index of the new membership in the membership set
/// @return indexReused true if the index was reused, false otherwise
function _acquireMembership(
address _sender,
uint256 _idCommitment,
uint32 _rateLimit
)
internal
returns (uint32 index, bool indexReused)
{
// Check if the rate limit is valid
if (!isValidMembershipRateLimit(_rateLimit)) {
revert InvalidMembershipRateLimit();
}
currentTotalRateLimit += _rateLimit;
// Determine if we exceed the total rate limit
if (currentTotalRateLimit > maxTotalRateLimit) {
revert CannotExceedMaxTotalRateLimit();
}
(address token, uint256 depositAmount) = priceCalculator.calculate(_rateLimit);
// Possibly reuse an index of an erased membership
(index, indexReused) = _getFreeIndex();
memberships[_idCommitment] = MembershipInfo({
holder: _sender,
activeDuration: activeDurationForNewMemberships,
gracePeriodStartTimestamp: block.timestamp + uint256(activeDurationForNewMemberships),
gracePeriodDuration: gracePeriodDurationForNewMemberships,
token: token,
depositAmount: depositAmount,
rateLimit: _rateLimit,
index: index
});
IERC20(token).safeTransferFrom(_sender, address(this), depositAmount);
}
/// @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 = nextFreeIndex;
}
}
/// @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 membership = memberships[_idCommitment];
if (!_isInPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration)) {
revert CannotExtendNonGracePeriodMembership(_idCommitment);
}
if (_sender != membership.holder) revert NonHolderCannotExtend(_idCommitment);
// Note: we add the new active period to the end of the ongoing grace period
uint256 newGracePeriodStartTimestamp =
membership.gracePeriodStartTimestamp + membership.gracePeriodDuration + uint256(membership.activeDuration);
membership.gracePeriodStartTimestamp = newGracePeriodStartTimestamp;
emit MembershipExtended(
_idCommitment, membership.rateLimit, membership.index, membership.gracePeriodStartTimestamp
);
}
/// @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) {
revert CannotEraseActiveMembership(_idCommitment);
} else if (membershipIsInGracePeriod && !isHolder) {
revert NonHolderCannotEraseGracePeriodMembership(_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 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 membership = memberships[_idCommitment];
return _isAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
}
/// @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 membershipExpirationTimestamp(uint256 _idCommitment) public view returns (uint256) {
MembershipInfo memory membership = memberships[_idCommitment];
return _timestampAfterPeriod(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
}
/// @dev Returns the first timestamp after a specified period
/// @param _start timestamp in which the period starts (inclusive)
/// @param _duration duration of the period (exclusive)
function _timestampAfterPeriod(uint256 _start, uint32 _duration) internal pure returns (uint256) {
return _start + uint256(_duration);
}
/// @dev Withdraw any available 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 = depositsToWithdraw[_sender][_token];
require(amount > 0, "Insufficient deposit balance");
depositsToWithdraw[_sender][_token] = 0;
IERC20(_token).safeTransfer(_sender, amount);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
}

View File

@ -8,71 +8,52 @@ import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/O
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
/// The tree is full
error FullTree();
import { MembershipUpgradeable } from "./Membership.sol";
import { IPriceCalculator } from "./IPriceCalculator.sol";
/// 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);
contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, MembershipUpgradeable {
/// @notice The Field
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 max message limit per epoch
uint32 public MAX_MESSAGE_LIMIT;
/// @notice The depth of the Merkle tree that stores rate commitments of memberships
uint8 public constant MERKLE_TREE_DEPTH = 20;
/// @notice The depth of the merkle tree
uint8 public constant DEPTH = 20;
/// @notice The maximum membership set size is the size of the Merkle tree (2 ^ depth)
uint32 public MAX_MEMBERSHIP_SET_SIZE;
/// @notice The size of the merkle tree, i.e 2^depth
uint32 public SET_SIZE;
/// @notice The index of the next member to be registered
uint32 public commitmentIndex;
/// @notice the membership metadata of the member
struct MembershipInfo {
/// @notice the user message limit of each member
uint32 userMessageLimit;
/// @notice the index of the member in the set
uint32 index;
}
/// @notice the member metadata
mapping(uint256 => MembershipInfo) public memberInfo;
/// @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);
_;
}
/// @notice the modifier to check if the userMessageLimit is valid
/// @param userMessageLimit The user message limit
modifier onlyValidUserMessageLimit(uint32 userMessageLimit) {
if (!isValidUserMessageLimit(userMessageLimit)) revert InvalidUserMessageLimit(userMessageLimit);
/// @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");
_;
}
@ -80,119 +61,246 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
_disableInitializers();
}
function initialize(uint32 maxMessageLimit) public initializer {
/// @dev Contract initializer
/// @param _priceCalculator Address of an instance of IPriceCalculator
/// @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 _maxTotalRateLimit,
uint32 _minMembershipRateLimit,
uint32 _maxMembershipRateLimit,
uint32 _activeDuration,
uint32 _gracePeriod
)
public
initializer
{
__Ownable_init();
__UUPSUpgradeable_init();
MAX_MESSAGE_LIMIT = maxMessageLimit;
SET_SIZE = uint32(1 << DEPTH);
__MembershipUpgradeable_init(
_priceCalculator,
_maxTotalRateLimit,
_minMembershipRateLimit,
_maxMembershipRateLimit,
_activeDuration,
_gracePeriod
);
MAX_MEMBERSHIP_SET_SIZE = uint32(1 << MERKLE_TREE_DEPTH);
deployedBlockNumber = uint32(block.number);
LazyIMT.init(imtData, DEPTH);
commitmentIndex = 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 Checks if a user message limit is valid
/// @param userMessageLimit The user message limit
/// @return true if the user message limit is valid, false otherwise
function isValidUserMessageLimit(uint32 userMessageLimit) public view returns (bool) {
return userMessageLimit > 0 && userMessageLimit <= MAX_MESSAGE_LIMIT;
}
/// @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 = memberInfo[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;
}
/// Allows a user to register as a member
/// @param idCommitment The idCommitment of the member
/// @param userMessageLimit The message limit of the member
/// @notice Returns the membership info (rate limit, index, rateCommitment) by its idCommitment
/// @param idCommitment The idCommitment of the membership
/// @return The membership info (rateLimit, index, rateCommitment)
function getMembershipInfo(uint256 idCommitment) public view returns (uint32, uint32, uint256) {
MembershipInfo memory membership = memberships[idCommitment];
// we cannot call getRateCommmitment for 0 index if the membership doesn't exist
if (membership.rateLimit == 0) {
return (0, 0, 0);
}
return (membership.rateLimit, membership.index, _getRateCommmitment(membership.index));
}
/// @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 >= nextFreeIndex) revert InvalidPaginationQuery(startIndex, endIndex);
uint256[] memory rateCommitments = new uint256[](endIndex - startIndex + 1);
for (uint32 i = startIndex; i <= endIndex; i++) {
rateCommitments[i - startIndex] = _getRateCommmitment(i);
}
return rateCommitments;
}
/// @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 while erasing some expired memberships to reuse their rate limit
/// @param idCommitment The idCommitment of the new membership
/// @param rateLimit The rate limit of the new membership
/// @param idCommitmentsToErase The list of idCommitments of expired memberships to erase
function register(
uint256 idCommitment,
uint32 userMessageLimit
uint32 rateLimit,
uint256[] calldata idCommitmentsToErase
)
external
onlyValidIdCommitment(idCommitment)
onlyValidUserMessageLimit(userMessageLimit)
noDuplicateMembership(idCommitment)
membershipSetNotFull
{
_register(idCommitment, userMessageLimit);
// erase memberships without overwriting membership set data to zero (save gas)
_eraseMemberships(idCommitmentsToErase, false);
_register(idCommitment, rateLimit);
}
/// Registers a member
/// @param idCommitment The idCommitment of the member
/// @param userMessageLimit The message limit of the member
function _register(uint256 idCommitment, uint32 userMessageLimit) internal {
if (memberExists(idCommitment)) revert DuplicateIdCommitment();
if (commitmentIndex >= SET_SIZE) revert FullTree();
uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]);
MembershipInfo memory member = MembershipInfo({ userMessageLimit: userMessageLimit, index: commitmentIndex });
LazyIMT.insert(imtData, rateCommitment);
memberInfo[idCommitment] = member;
emit MemberRegistered(rateCommitment, commitmentIndex);
commitmentIndex += 1;
}
/// @notice Returns the commitments of a range of members
/// @param startIndex The start index of the range
/// @param endIndex The end index of the range
/// @return The commitments of the members
function getCommitments(uint32 startIndex, uint32 endIndex) public view returns (uint256[] memory) {
if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex);
if (endIndex > commitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex);
uint256[] memory commitments = new uint256[](endIndex - startIndex + 1);
for (uint32 i = startIndex; i <= endIndex; i++) {
commitments[i - startIndex] = indexToCommitment(i);
/// @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;
}
return commitments;
emit MembershipRegistered(idCommitment, rateLimit, index);
}
/// @notice Returns the root of the IMT
/// @return The root of the IMT
/// @notice Returns the root of the Merkle tree that stores rate commitments of memberships
/// @return The root of the Merkle tree that stores rate commitments of memberships
function root() external view returns (uint256) {
return LazyIMT.root(imtData, DEPTH);
return LazyIMT.root(merkleTree, MERKLE_TREE_DEPTH);
}
/// @notice Returns the merkle proof elements of a given membership
/// @param index The index of the member
/// @return The merkle proof elements of the member
function merkleProofElements(uint40 index) public view returns (uint256[DEPTH] memory) {
uint256[DEPTH] memory castedProof;
uint256[] memory proof = LazyIMT.merkleProofElements(imtData, index, DEPTH);
for (uint8 i = 0; i < DEPTH; i++) {
castedProof[i] = proof[i];
/// @notice Returns the Merkle proof that a given membership is in the membership set
/// @param index The index of the membership
/// @return The Merkle proof (an array of MERKLE_TREE_DEPTH elements)
function getMerkleProof(uint40 index) public view returns (uint256[MERKLE_TREE_DEPTH] memory) {
uint256[] memory dynamicSizeProof = LazyIMT.merkleProofElements(merkleTree, index, MERKLE_TREE_DEPTH);
uint256[MERKLE_TREE_DEPTH] memory fixedSizeProof;
for (uint8 i = 0; i < MERKLE_TREE_DEPTH; i++) {
fixedSizeProof[i] = dynamicSizeProof[i];
}
return castedProof;
return fixedSizeProof;
}
/// @notice Extend a 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++) {
_extendMembership(_msgSender(), idCommitments[i]);
}
}
/// @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 {
_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 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);
}
/// @notice Set the address of the price calculator
/// @param _priceCalculator new price calculator address
function setPriceCalculator(address _priceCalculator) external onlyOwner {
priceCalculator = IPriceCalculator(_priceCalculator);
}
/// @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 _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 _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 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 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;
}
}

19
test/TestToken.sol Normal file
View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;
import { BaseScript } from "../script/Base.s.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TTT") { }
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract TestTokenFactory is BaseScript {
function run() public broadcast returns (address) {
return address(new TestToken());
}
}

View File

@ -5,33 +5,54 @@ import { Test } from "forge-std/Test.sol";
import { Deploy } from "../script/Deploy.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import "../src/WakuRlnV2.sol"; // solhint-disable-line
import "../src/Membership.sol"; // solhint-disable-line
import { IPriceCalculator } from "../src/IPriceCalculator.sol";
import { LinearPriceCalculator } from "../src/LinearPriceCalculator.sol";
import { TestToken } from "./TestToken.sol";
import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/console.sol";
contract WakuRlnV2Test is Test {
WakuRlnV2 internal w;
address internal impl;
DeploymentConfig internal deploymentConfig;
TestToken internal token;
address internal deployer;
uint256[] noIdCommitmentsToErase = new uint256[](0);
function setUp() public virtual {
token = new TestToken();
Deploy deployment = new Deploy();
(w, impl) = deployment.run();
(w, impl) = deployment.deploy(address(token));
// Minting a large number of tokens to not have to worry about
// Not having enough balance
token.mint(address(this), 100_000_000 ether);
}
function test__ValidRegistration__kats() external {
vm.pauseGasMetering();
// Merkle tree leaves are calculated using 2 as rateLimit
vm.prank(w.owner());
w.setMinMembershipRateLimit(2);
uint256 idCommitment = 2;
uint32 userMessageLimit = 2;
uint32 membershipRateLimit = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
w.register(idCommitment, userMessageLimit);
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
vm.pauseGasMetering();
assertEq(w.commitmentIndex(), 1);
assertEq(w.memberExists(idCommitment), true);
(uint32 fetchedUserMessageLimit, uint32 index) = w.memberInfo(idCommitment);
assertEq(fetchedUserMessageLimit, userMessageLimit);
assertEq(w.nextFreeIndex(), 1);
assertEq(w.isInMembershipSet(idCommitment), true);
(,,,, uint32 membershipRateLimit1, uint32 index, address holder,) = w.memberships(idCommitment);
assertEq(membershipRateLimit1, membershipRateLimit);
assertEq(holder, address(this));
assertEq(index, 0);
// kats from zerokit
uint256 rateCommitment =
@ -40,12 +61,11 @@ contract WakuRlnV2Test is Test {
w.root(),
13_801_897_483_540_040_307_162_267_952_866_411_686_127_372_014_953_358_983_481_592_640_000_001_877_295
);
(uint32 fetchedUserMessageLimit2, uint32 index2, uint256 rateCommitment2) =
w.idCommitmentToMetadata(idCommitment);
assertEq(fetchedUserMessageLimit2, userMessageLimit);
(uint32 membershipRateLimit2, uint32 index2, uint256 rateCommitment2) = w.getMembershipInfo(idCommitment);
assertEq(membershipRateLimit2, membershipRateLimit);
assertEq(index2, 0);
assertEq(rateCommitment2, rateCommitment);
uint256[20] memory proof = w.merkleProofElements(0);
uint256[20] memory proof = w.getMerkleProof(0);
uint256[20] memory expectedProof = [
0,
14_744_269_619_966_411_208_579_211_824_598_458_697_587_494_354_926_760_081_771_325_075_741_142_829_156,
@ -74,124 +94,675 @@ contract WakuRlnV2Test is Test {
vm.resumeGasMetering();
}
function test__ValidRegistration(uint256 idCommitment, uint32 userMessageLimit) external {
vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit));
function test__ValidRegistration(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
uint256 minMembershipRateLimit = w.minMembershipRateLimit();
uint256 maxMembershipRateLimit = w.maxMembershipRateLimit();
vm.assume(minMembershipRateLimit <= membershipRateLimit && membershipRateLimit <= maxMembershipRateLimit);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.resumeGasMetering();
assertEq(w.memberExists(idCommitment), false);
w.register(idCommitment, userMessageLimit);
uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]);
assertEq(w.isInMembershipSet(idCommitment), false);
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
uint256 rateCommitment = PoseidonT3.hash([idCommitment, membershipRateLimit]);
(uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) =
w.idCommitmentToMetadata(idCommitment);
assertEq(fetchedUserMessageLimit, userMessageLimit);
(uint32 fetchedMembershipRateLimit, uint32 index, uint256 fetchedRateCommitment) =
w.getMembershipInfo(idCommitment);
assertEq(fetchedMembershipRateLimit, membershipRateLimit);
assertEq(index, 0);
assertEq(fetchedRateCommitment, rateCommitment);
assertEq(token.balanceOf(address(w)), price);
assertEq(w.currentTotalRateLimit(), membershipRateLimit);
}
function test__LinearPriceCalculation(uint32 membershipRateLimit) external view {
IPriceCalculator priceCalculator = w.priceCalculator();
uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerEpoch();
assertNotEq(pricePerMessagePerPeriod, 0);
uint256 expectedPrice = uint256(membershipRateLimit) * pricePerMessagePerPeriod;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
assertEq(price, expectedPrice);
}
function test__InvalidTokenAmount(uint256 idCommitment, uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 minMembershipRateLimit = w.minMembershipRateLimit();
uint256 maxMembershipRateLimit = w.maxMembershipRateLimit();
vm.assume(minMembershipRateLimit <= membershipRateLimit && membershipRateLimit <= maxMembershipRateLimit);
vm.assume(w.isValidIdCommitment(idCommitment) && w.isValidMembershipRateLimit(membershipRateLimit));
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
token.approve(address(w), price - 1);
vm.expectRevert(bytes("ERC20: insufficient allowance"));
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
}
function test__IdCommitmentToMetadata__DoesntExist() external view {
uint256 idCommitment = 2;
(uint32 userMessageLimit, uint32 index, uint256 rateCommitment) = w.idCommitmentToMetadata(idCommitment);
assertEq(userMessageLimit, 0);
(uint32 membershipRateLimit, uint32 index, uint256 rateCommitment) = w.getMembershipInfo(idCommitment);
assertEq(membershipRateLimit, 0);
assertEq(index, 0);
assertEq(rateCommitment, 0);
}
function test__InvalidRegistration__InvalidIdCommitment__Zero() external {
vm.pauseGasMetering();
uint256 idCommitment = 0;
uint32 userMessageLimit = 2;
uint32 membershipRateLimit = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
token.approve(address(w), price);
vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0));
w.register(idCommitment, userMessageLimit);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
}
function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external {
vm.pauseGasMetering();
uint32 membershipRateLimit = 20;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
uint256 idCommitment = w.Q() + 1;
uint32 userMessageLimit = 2;
token.approve(address(w), price);
vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment));
w.register(idCommitment, userMessageLimit);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
}
function test__InvalidRegistration__InvalidUserMessageLimit__Zero() external {
function test__InvalidRegistration__InvalidMembershipRateLimit__MinMax() external {
uint256 idCommitment = 2;
uint32 userMessageLimit = 0;
vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, 0));
w.register(idCommitment, userMessageLimit);
uint32 invalidMin = w.minMembershipRateLimit() - 1;
uint32 invalidMax = w.maxMembershipRateLimit() + 1;
vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector));
w.register(idCommitment, invalidMin, noIdCommitmentsToErase);
vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector));
w.register(idCommitment, invalidMax, noIdCommitmentsToErase);
}
function test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() external {
function test__ValidRegistrationExtend(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
uint32 userMessageLimit = w.MAX_MESSAGE_LIMIT() + 1;
vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, userMessageLimit));
w.register(idCommitment, userMessageLimit);
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.assume(
w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()
);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
(,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment);
assertFalse(w.isInGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
vm.warp(gracePeriodStartTimestamp);
assertTrue(w.isInGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
uint256[] memory commitmentsToExtend = new uint256[](1);
commitmentsToExtend[0] = idCommitment;
// Attempt to extend the membership (but it is not owned by us)
address randomAddress = vm.addr(block.timestamp);
vm.prank(randomAddress);
vm.expectRevert(abi.encodeWithSelector(NonHolderCannotExtend.selector, commitmentsToExtend[0]));
w.extendMemberships(commitmentsToExtend);
// Attempt to extend the membership (but now we are the owner)
vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment)
emit MembershipUpgradeable.MembershipExtended(idCommitment, 0, 0, 0);
(, uint256 oldActiveDuration, uint256 oldGracePeriodStartTimestamp, uint32 oldGracePeriodDuration,,,,) =
w.memberships(idCommitment);
w.extendMemberships(commitmentsToExtend);
(, uint256 newActiveDuration, uint256 newGracePeriodStartTimestamp, uint32 newGracePeriodDuration,,,,) =
w.memberships(idCommitment);
assertEq(oldActiveDuration, newActiveDuration);
assertEq(oldGracePeriodDuration, newGracePeriodDuration);
assertEq(
oldGracePeriodStartTimestamp + oldGracePeriodDuration + newActiveDuration, newGracePeriodStartTimestamp
);
assertFalse(w.isInGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
// Attempt to extend a non grace period membership
token.approve(address(w), price);
w.register(idCommitment + 1, membershipRateLimit, noIdCommitmentsToErase);
commitmentsToExtend[0] = idCommitment + 1;
vm.expectRevert(abi.encodeWithSelector(CannotExtendNonGracePeriodMembership.selector, commitmentsToExtend[0]));
w.extendMemberships(commitmentsToExtend);
}
function test__ValidRegistrationNoGracePeriod(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.assume(
w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()
);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.startPrank(w.owner());
w.setGracePeriodDuration(0);
vm.stopPrank();
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
(,, uint256 gracePeriodStartTimestamp, uint32 gracePeriodDuration,,,,) = w.memberships(idCommitment);
assertEq(gracePeriodDuration, 0);
assertFalse(w.isInGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
uint256 expectedExpirationTimestamp = gracePeriodStartTimestamp + uint256(gracePeriodDuration);
uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment);
assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp);
vm.warp(membershipExpirationTimestamp);
assertFalse(w.isInGracePeriod(idCommitment));
assertTrue(w.isExpired(idCommitment));
}
function test__ValidRegistrationExtendSingleMembership(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.assume(
w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()
);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
uint256 ogExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment);
(,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment);
vm.warp(gracePeriodStartTimestamp);
uint256[] memory commitmentsToExtend = new uint256[](1);
commitmentsToExtend[0] = idCommitment;
// Extend the membership
vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment)
emit MembershipUpgradeable.MembershipExtended(idCommitment, 0, 0, 0);
w.extendMemberships(commitmentsToExtend);
(,, uint256 newGracePeriodStartTimestamp, uint32 newGracePeriodDuration,,,,) = w.memberships(idCommitment);
uint256 expectedExpirationTimestamp = newGracePeriodStartTimestamp + uint256(newGracePeriodDuration);
uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment);
assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp);
assertTrue(expectedExpirationTimestamp > ogExpirationTimestamp);
}
function test__ValidRegistrationExpiry(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.assume(
w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()
);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
(,, uint256 fetchedgracePeriodStartTimestamp, uint32 fetchedGracePeriod,,,,) = w.memberships(idCommitment);
uint256 expectedExpirationTimestamp = fetchedgracePeriodStartTimestamp + uint256(fetchedGracePeriod);
uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment);
assertEq(expectedExpirationTimestamp, membershipExpirationTimestamp);
vm.warp(membershipExpirationTimestamp);
assertFalse(w.isInGracePeriod(idCommitment));
assertTrue(w.isExpired(idCommitment));
}
function test__ValidRegistrationWithEraseList() external {
vm.pauseGasMetering();
vm.startPrank(w.owner());
w.setMinMembershipRateLimit(20);
w.setMaxMembershipRateLimit(100);
w.setMaxTotalRateLimit(100);
vm.stopPrank();
vm.resumeGasMetering();
(, uint256 priceA) = w.priceCalculator().calculate(20);
for (uint256 i = 1; i <= 5; i++) {
token.approve(address(w), priceA);
w.register(i, 20, noIdCommitmentsToErase);
// Make sure they're expired
vm.warp(w.membershipExpirationTimestamp(i));
}
// Time travel to a point in which the last membership is active
(,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(5);
vm.warp(gracePeriodStartTimestamp - 1);
// Ensure that this is the case
assertTrue(w.isExpired(4));
assertFalse(w.isExpired(5));
assertFalse(w.isInGracePeriod(5));
(, uint256 priceB) = w.priceCalculator().calculate(60);
token.approve(address(w), priceB);
// Should fail. There's not enough free rate limit
vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector));
w.register(6, 60, noIdCommitmentsToErase);
// Attempt to erase 3 memberships including one that can't be erased (the last one)
uint256[] memory commitmentsToErase = new uint256[](3);
commitmentsToErase[0] = 1;
commitmentsToErase[1] = 2;
commitmentsToErase[2] = 5; // This one is still active
token.approve(address(w), priceB);
vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, 5));
w.register(6, 60, commitmentsToErase);
// Attempt to erase 3 memberships that can be erased
commitmentsToErase[2] = 4;
vm.expectEmit(true, false, false, false);
emit MembershipUpgradeable.MembershipExpired(1, 0, 0);
vm.expectEmit(true, false, false, false);
emit MembershipUpgradeable.MembershipExpired(2, 0, 0);
vm.expectEmit(true, false, false, false);
emit MembershipUpgradeable.MembershipExpired(4, 0, 0);
w.register(6, 60, commitmentsToErase);
// Ensure that the chosen memberships were erased and others unaffected
address holder;
(,,,,,, holder,) = w.memberships(1);
assertEq(holder, address(0));
(,,,,,, holder,) = w.memberships(2);
assertEq(holder, address(0));
(,,,,,, holder,) = w.memberships(3);
assertEq(holder, address(this));
(,,,,,, holder,) = w.memberships(4);
assertEq(holder, address(0));
(,,,,,, holder,) = w.memberships(5);
assertEq(holder, address(this));
(,,,,,, holder,) = w.memberships(6);
assertEq(holder, address(this));
// The balance available for withdrawal should match the amount of the expired membership
uint256 availableBalance = w.depositsToWithdraw(address(this), address(token));
assertEq(availableBalance, priceA * 3);
}
function test__RegistrationWhenMaxRateLimitIsReached() external {
vm.pauseGasMetering();
vm.startPrank(w.owner());
w.setMinMembershipRateLimit(1);
w.setMaxMembershipRateLimit(5);
w.setMaxTotalRateLimit(5);
vm.stopPrank();
vm.resumeGasMetering();
bool isValid = w.isValidMembershipRateLimit(6);
assertFalse(isValid);
// Exceeds the max rate limit per membership
uint32 membershipRateLimit = 10;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
token.approve(address(w), price);
vm.expectRevert(abi.encodeWithSelector(InvalidMembershipRateLimit.selector));
w.register(1, membershipRateLimit, noIdCommitmentsToErase);
// Should register succesfully
membershipRateLimit = 4;
(, price) = w.priceCalculator().calculate(membershipRateLimit);
token.approve(address(w), price);
w.register(2, membershipRateLimit, noIdCommitmentsToErase);
// Exceeds the rate limit
membershipRateLimit = 2;
(, price) = w.priceCalculator().calculate(membershipRateLimit);
token.approve(address(w), price);
vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector));
w.register(3, membershipRateLimit, noIdCommitmentsToErase);
// Should register succesfully
membershipRateLimit = 1;
(, price) = w.priceCalculator().calculate(membershipRateLimit);
token.approve(address(w), price);
w.register(3, membershipRateLimit, noIdCommitmentsToErase);
// We ran out of rate limit again
membershipRateLimit = 1;
(, price) = w.priceCalculator().calculate(membershipRateLimit);
token.approve(address(w), price);
vm.expectRevert(abi.encodeWithSelector(CannotExceedMaxTotalRateLimit.selector));
w.register(4, membershipRateLimit, noIdCommitmentsToErase);
}
function test__indexReuse_eraseMemberships(uint32 idCommitmentsLength) external {
vm.assume(0 < idCommitmentsLength && idCommitmentsLength < 50);
(, uint256 price) = w.priceCalculator().calculate(20);
uint32 index;
uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength);
uint256 time = block.timestamp;
for (uint256 i = 1; i <= idCommitmentsLength; i++) {
token.approve(address(w), price);
w.register(i, 20, noIdCommitmentsToErase);
(,,,,, index,,) = w.memberships(i);
assertEq(index, w.nextFreeIndex() - 1);
commitmentsToErase[i - 1] = i;
time += 100;
vm.warp(time);
}
// None of the commitments can be deleted because they're still active
uint256[] memory singleCommitmentToErase = new uint256[](1);
for (uint256 i = 1; i <= idCommitmentsLength; i++) {
singleCommitmentToErase[0] = i;
vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, i));
w.eraseMemberships(singleCommitmentToErase);
}
// Fastfwd to commitment grace period, and try to erase it without being the owner
(,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(1);
vm.warp(gracePeriodStartTimestamp);
assertTrue(w.isInGracePeriod(1));
singleCommitmentToErase[0] = 1;
address randomAddress = vm.addr(block.timestamp);
vm.prank(randomAddress);
vm.expectRevert(abi.encodeWithSelector(NonHolderCannotEraseGracePeriodMembership.selector, 1));
w.eraseMemberships(singleCommitmentToErase);
// time travel to the moment we can erase all expired memberships
uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitmentsLength);
vm.warp(membershipExpirationTimestamp);
w.eraseMemberships(commitmentsToErase);
// Verify that expired indices match what we expect
for (uint32 i = 0; i < idCommitmentsLength; i++) {
assertEq(i, w.indicesOfLazilyErasedMemberships(i));
}
uint32 expectedNextFreeIndex = w.nextFreeIndex();
for (uint256 i = 1; i <= idCommitmentsLength; i++) {
uint256 idCommitment = i + 10;
uint256 expectedindexReusedPos = idCommitmentsLength - i;
uint32 expectedReusedIndex = w.indicesOfLazilyErasedMemberships(expectedindexReusedPos);
token.approve(address(w), price);
w.register(idCommitment, 20, noIdCommitmentsToErase);
(,,,,, index,,) = w.memberships(idCommitment);
assertEq(expectedReusedIndex, index);
// Should have been removed from the list
vm.expectRevert();
w.indicesOfLazilyErasedMemberships(expectedindexReusedPos);
// Should not have been affected
assertEq(expectedNextFreeIndex, w.nextFreeIndex());
}
// No indices should be available for reuse
vm.expectRevert();
w.indicesOfLazilyErasedMemberships(0);
// Should use a new index since we got rid of all reusable indexes
token.approve(address(w), price);
w.register(100, 20, noIdCommitmentsToErase);
(,,,,, index,,) = w.memberships(100);
assertEq(index, expectedNextFreeIndex);
assertEq(expectedNextFreeIndex + 1, w.nextFreeIndex());
}
function test__RemoveExpiredMemberships(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.assume(
w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()
);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.resumeGasMetering();
uint256 time = block.timestamp;
for (uint256 i = 0; i < 5; i++) {
token.approve(address(w), price);
w.register(idCommitment + i, membershipRateLimit, noIdCommitmentsToErase);
time += 100;
vm.warp(time);
}
// Expiring the first 3 memberships
uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitment + 2);
vm.warp(membershipExpirationTimestamp);
for (uint256 i = 0; i < 5; i++) {
if (i <= 2) {
assertTrue(w.isExpired(idCommitment + i));
} else {
assertFalse(w.isExpired(idCommitment + i));
}
}
uint256[] memory commitmentsToErase = new uint256[](2);
commitmentsToErase[0] = idCommitment + 1;
commitmentsToErase[1] = idCommitment + 2;
vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment)
emit MembershipUpgradeable.MembershipExpired(commitmentsToErase[0], 0, 0);
vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment)
emit MembershipUpgradeable.MembershipExpired(commitmentsToErase[0], 0, 0);
w.eraseMemberships(commitmentsToErase);
address holder;
(,,,,,, holder,) = w.memberships(idCommitment + 1);
assertEq(holder, address(0));
(,,,,,, holder,) = w.memberships(idCommitment + 2);
assertEq(holder, address(0));
// Attempting to call erase when some of the commitments can't be erased yet
// idCommitment can be erased (in grace period), but idCommitment + 4 is still active
(,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment + 4);
vm.warp(gracePeriodStartTimestamp - 1);
commitmentsToErase[0] = idCommitment;
commitmentsToErase[1] = idCommitment + 4;
vm.expectRevert(abi.encodeWithSelector(CannotEraseActiveMembership.selector, idCommitment + 4));
w.eraseMemberships(commitmentsToErase);
}
function test__RemoveAllExpiredMemberships(uint32 idCommitmentsLength) external {
vm.pauseGasMetering();
vm.assume(1 < idCommitmentsLength && idCommitmentsLength <= 100);
uint32 membershipRateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
uint256 time = block.timestamp;
for (uint256 i = 1; i <= idCommitmentsLength; i++) {
token.approve(address(w), price);
w.register(i, membershipRateLimit, noIdCommitmentsToErase);
time += 100;
vm.warp(time);
}
uint256 membershipExpirationTimestamp = w.membershipExpirationTimestamp(idCommitmentsLength);
vm.warp(membershipExpirationTimestamp);
for (uint256 i = 1; i <= 5; i++) {
assertTrue(w.isExpired(i));
}
uint256[] memory commitmentsToErase = new uint256[](idCommitmentsLength);
for (uint256 i = 0; i < idCommitmentsLength; i++) {
commitmentsToErase[i] = i + 1;
vm.expectEmit(true, false, false, false); // only check the first parameter of the event (the idCommitment)
emit MembershipUpgradeable.MembershipExpired(i + 1, 0, 0);
}
w.eraseMemberships(commitmentsToErase);
// Erased memberships are gone!
for (uint256 i = 0; i < commitmentsToErase.length; i++) {
(,,,, uint32 fetchedMembershipRateLimit,,,) = w.memberships(commitmentsToErase[i]);
assertEq(fetchedMembershipRateLimit, 0);
}
}
function test__WithdrawToken(uint32 membershipRateLimit) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator()));
vm.prank(priceCalculator.owner());
priceCalculator.setTokenAndPrice(address(token), 5 wei);
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
token.mint(address(this), price);
vm.assume(
w.minMembershipRateLimit() <= membershipRateLimit && membershipRateLimit <= w.maxMembershipRateLimit()
);
vm.assume(w.isValidMembershipRateLimit(membershipRateLimit));
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
(,, uint256 gracePeriodStartTimestamp,,,,,) = w.memberships(idCommitment);
vm.warp(gracePeriodStartTimestamp);
uint256[] memory commitmentsToErase = new uint256[](1);
commitmentsToErase[0] = idCommitment;
w.eraseMemberships(commitmentsToErase);
uint256 availableBalance = w.depositsToWithdraw(address(this), address(token));
assertEq(availableBalance, price);
assertEq(token.balanceOf(address(w)), price);
uint256 balanceBeforeWithdraw = token.balanceOf(address(this));
w.withdraw(address(token));
uint256 balanceAfterWithdraw = token.balanceOf(address(this));
availableBalance = w.depositsToWithdraw(address(this), address(token));
assertEq(availableBalance, 0);
assertEq(token.balanceOf(address(w)), 0);
assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw);
}
function test__InvalidRegistration__DuplicateIdCommitment() external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
uint32 userMessageLimit = 2;
w.register(idCommitment, userMessageLimit);
vm.expectRevert(DuplicateIdCommitment.selector);
w.register(idCommitment, userMessageLimit);
uint32 membershipRateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
token.approve(address(w), price);
vm.expectRevert(bytes("Duplicate idCommitment: membership already exists"));
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
}
function test__InvalidRegistration__FullTree() external {
uint32 userMessageLimit = 2;
vm.pauseGasMetering();
uint32 membershipRateLimit = 20;
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
// we progress the tree to the last leaf
/*| Name | Type | Slot | Offset | Bytes |
|---------------------|-----------------------------------------------------|------|--------|-------|
| MAX_MESSAGE_LIMIT | uint32 | 0 | 0 | 4 |
| SET_SIZE | uint32 | 0 | 4 | 4 |
| commitmentIndex | uint32 | 0 | 8 | 4 |
| memberInfo | mapping(uint256 => struct WakuRlnV2.MembershipInfo) | 1 | 0 | 32 |
| deployedBlockNumber | uint32 | 2 | 0 | 4 |
| imtData | struct LazyIMTData | 3 | 0 | 64 |*/
// we set MAX_MESSAGE_LIMIT to 20 (unaltered)
// we set SET_SIZE to 4294967295 (1 << 20) (unaltered)
// we set commitmentIndex to 4294967295 (1 << 20) (altered)
vm.store(address(w), bytes32(uint256(201)), 0x0000000000000000000000000000000000000000ffffffffffffffff00000014);
vm.expectRevert(FullTree.selector);
w.register(1, userMessageLimit);
| nextFreeIndex | uint32 | 206 | 0 | 4 | */
/*
Pro tip: to easily find the storage slot of a variable, without having to calculate the storage layout
based on the variable declaration, set the variable to an easily grepable value like 0xDEADBEEF, and then
execute:
```
for (uint256 i = 0; i <= 500; i++) {
bytes32 slot0Value = vm.load(address(w), bytes32(i));
console.log("%s", i);
console.logBytes32(slot0Value);
}
revert();
```
Search the value in the output (i.e. `DEADBEEF`) to determine the storage slot being used.
If the storage layout changes, update the next line accordingly
*/
// we set nextFreeIndex to 4294967295 (1 << 20) = 0x00100000
vm.store(address(w), bytes32(uint256(206)), 0x0000000000000000000000000000000000000000000000000000000000100000);
token.approve(address(w), price);
vm.expectRevert(bytes("Membership set is full"));
w.register(1, membershipRateLimit, noIdCommitmentsToErase);
}
function test__InvalidPaginationQuery__StartIndexGTEndIndex() external {
vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0));
w.getCommitments(1, 0);
w.getRateCommitmentsInRangeBoundsInclusive(1, 0);
}
function test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() external {
function test__InvalidPaginationQuery__EndIndexGTNextFreeIndex() external {
vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 0, 2));
w.getCommitments(0, 2);
w.getRateCommitmentsInRangeBoundsInclusive(0, 2);
}
function test__ValidPaginationQuery__OneElement() external {
uint32 userMessageLimit = 2;
vm.pauseGasMetering();
uint256 idCommitment = 1;
w.register(idCommitment, userMessageLimit);
uint256[] memory commitments = w.getCommitments(0, 0);
uint32 membershipRateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, membershipRateLimit, noIdCommitmentsToErase);
uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(0, 0);
assertEq(commitments.length, 1);
uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]);
uint256 rateCommitment = PoseidonT3.hash([idCommitment, membershipRateLimit]);
assertEq(commitments[0], rateCommitment);
}
function test__ValidPaginationQuery(uint32 idCommitmentsLength) external {
vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100);
uint32 userMessageLimit = 2;
vm.pauseGasMetering();
for (uint256 i = 0; i < idCommitmentsLength; i++) {
w.register(i + 1, userMessageLimit);
vm.assume(0 < idCommitmentsLength && idCommitmentsLength <= 100);
uint32 membershipRateLimit = w.minMembershipRateLimit();
(, uint256 price) = w.priceCalculator().calculate(membershipRateLimit);
for (uint256 i = 0; i <= idCommitmentsLength; i++) {
token.approve(address(w), price);
w.register(i + 1, membershipRateLimit, noIdCommitmentsToErase);
}
vm.resumeGasMetering();
uint256[] memory commitments = w.getCommitments(0, idCommitmentsLength);
assertEq(commitments.length, idCommitmentsLength + 1);
uint256[] memory rateCommitments = w.getRateCommitmentsInRangeBoundsInclusive(0, idCommitmentsLength - 1);
assertEq(rateCommitments.length, idCommitmentsLength);
for (uint256 i = 0; i < idCommitmentsLength; i++) {
uint256 rateCommitment = PoseidonT3.hash([i + 1, userMessageLimit]);
assertEq(commitments[i], rateCommitment);
uint256 rateCommitment = PoseidonT3.hash([i + 1, membershipRateLimit]);
assertEq(rateCommitments[i], rateCommitment);
}
}
function test__Upgrade() external {
address testImpl = address(new WakuRlnV2());
bytes memory data = abi.encodeCall(WakuRlnV2.initialize, 20);
bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (address(0), 100, 1, 10, 10 minutes, 4 minutes));
address proxy = address(new ERC1967Proxy(testImpl, data));
address newImpl = address(new WakuRlnV2());