feat: membership

This commit is contained in:
Richard Ramos 2024-09-03 13:33:09 -04:00
parent 64df4593c6
commit f3d085df8d
No known key found for this signature in database
GPG Key ID: 1CE87DB518195760
9 changed files with 2503 additions and 1181 deletions

View File

@ -1,14 +1,11 @@
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: 27478)
WakuRlnV2Test:test__InsertionNormalOrder(uint32) (runs: 1001, μ: 1123469, ~: 494837)
WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTcommitmentIndex() (gas: 18261)
WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 16108)
WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 249370)
WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 12135)
WakuRlnV2Test:test__Upgrade() (gas: 6728616)
WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1002, μ: 226428, ~: 52927)
WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 262351)
WakuRlnV2Test:test__ValidRegistration(uint256,uint32) (runs: 1001, μ: 268741, ~: 268741)
WakuRlnV2Test:test__ValidRegistration__kats() (gas: 238744)

2453
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,18 +2,22 @@
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) {
// TODO: Use the correct values when deploying to mainnet
address priceCalcAddr = address(new LinearPriceCalculator(address(0), 0.05 ether));
// TODO: set DAI address 0x6B175474E89094C44Da98b954EedeAC495271d0F
impl = address(new WakuRlnV2());
bytes memory data = abi.encodeCall(WakuRlnV2.initialize, 100);
bytes memory data = abi.encodeCall(WakuRlnV2.initialize, (priceCalcAddr, 160_000, 20, 600, 30 days, 5 days));
// (priceCalcAddr, 160000, 20, 600, 30 days, 5 days)
address proxy = address(new ERC1967Proxy(impl, data));
w = WakuRlnV2(proxy);
}

11
src/IPriceCalculator.sol Normal file
View File

@ -0,0 +1,11 @@
// 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
/// @param _numberOfPeriods the number of periods the user wants to acquire
/// @return address of the erc20 token
/// @return uint price to pay for acquiring the specified `_rateLimit`
function calculate(uint256 _rateLimit, uint32 _numberOfPeriods) external view returns (address, uint256);
}

View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol";
import { IPriceCalculator } from "./IPriceCalculator.sol";
/// @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(0) represents ETH
address public token;
/// @notice The price per message per epoch per period
uint256 public pricePerMessagePerPeriod;
constructor(address _token, uint256 _pricePerPeriod) Ownable() {
token = _token;
pricePerMessagePerPeriod = _pricePerPeriod;
}
/// 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 {
token = _token;
pricePerMessagePerPeriod = _pricePerPeriod;
}
/// Returns the token and price to pay in `token` for some `_rateLimit`
/// @param _rateLimit the rate limit the user wants to acquire
/// @param _numberOfPeriods the number of periods the user wants to acquire
/// @return address of the erc20 token
/// @return uint price to pay for acquiring the specified `_rateLimit`
function calculate(uint256 _rateLimit, uint32 _numberOfPeriods) external view returns (address, uint256) {
return (token, uint256(_numberOfPeriods) * uint256(_rateLimit) * pricePerMessagePerPeriod);
}
}

427
src/Membership.sol Normal file
View File

@ -0,0 +1,427 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { IPriceCalculator } from "./IPriceCalculator.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import "openzeppelin-contracts/contracts/utils/Context.sol";
// The number of periods should be greater than zero
error NumberOfPeriodsCantBeZero();
// ETH Amount passed as value on the transaction is not correct
error IncorrectAmount();
// An eth value was assigned in the transaction and only tokens were expected
error OnlyTokensAccepted();
// The specified rate limit was not correct or within the expected limits
error InvalidRateLimit();
// It's not possible to acquire the rate limit due to exceeding the expected limits
// even after attempting to erase expired memberships
error ExceedMaxRateLimitPerEpoch();
// This membership is not in grace period yet
error NotInGracePeriod(uint256 idCommitment);
// The sender is not the holder of the membership
error NotHolder(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);
contract Membership {
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 tree
uint32 public maxTotalRateLimitPerEpoch;
/// @notice Maximum rate limit of one membership
uint32 public maxRateLimitPerMembership;
/// @notice Minimum rate limit of one membership
uint32 public minRateLimitPerMembership;
/// @notice Membership billing period
uint32 public billingPeriod;
/// @notice Membership grace period
uint32 public gracePeriod;
/// @notice balances available to withdraw
mapping(address => mapping(address => uint256)) public balancesToWithdraw; // holder -> token -> balance
/// @notice Total rate limit of all memberships in the tree
uint256 public totalRateLimitPerEpoch;
/// @notice List of registered memberships (IDCommitment to membership metadata)
mapping(uint256 => MembershipInfo) public members;
/// @notice The index of the next member to be registered
uint32 public commitmentIndex;
/// @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;
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 numPeriods
uint32 numberOfPeriods;
/// @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
uint32 index;
/// @notice address of the owner of this membership
address holder;
/// @notice token used to acquire this membership
address token;
}
/// @notice Emitted when a membership is erased due to having exceeded the grace period or the owner having chosen
/// to not extend it
/// @param idCommitment the idCommitment of the member
/// @param userMessageLimit the rate limit of this membership
/// @param index the index of the membership in the merkle tree
event MemberExpired(uint256 idCommitment, uint32 userMessageLimit, uint32 index);
/// @notice Emitted when a membership in grace period is extended
/// @param idCommitment the idCommitment of the member
/// @param userMessageLimit the rate limit of this membership
/// @param index the index of the membership in the merkle tree
/// @param newExpirationDate the new expiration date of this membership
event MemberExtended(uint256 idCommitment, uint32 userMessageLimit, uint32 index, uint256 newExpirationDate);
/// @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
function __Membership_init(
address _priceCalculator,
uint32 _maxTotalRateLimitPerEpoch,
uint32 _minRateLimitPerMembership,
uint32 _maxRateLimitPerMembership,
uint32 _expirationTerm,
uint32 _gracePeriod
)
internal
{
priceCalculator = IPriceCalculator(_priceCalculator);
maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch;
maxRateLimitPerMembership = _maxRateLimitPerMembership;
minRateLimitPerMembership = _minRateLimitPerMembership;
billingPeriod = _expirationTerm;
gracePeriod = _gracePeriod;
}
/// @notice Checks if a user message limit is valid. This does not take into account whether we the total
/// memberships have reached already the `maxTotalRateLimitPerEpoch`
/// @param userMessageLimit The user message limit
/// @return true if the user message limit is valid, false otherwise
function isValidUserMessageLimit(uint32 userMessageLimit) external view returns (bool) {
return userMessageLimit >= minRateLimitPerMembership && userMessageLimit <= maxRateLimitPerMembership;
}
/// @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 _numberOfPeriods the number of periods the user wants to acquire
/// @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
function _acquireMembership(
address _sender,
uint256 _idCommitment,
uint32 _rateLimit,
uint32 _numberOfPeriods
)
internal
returns (uint32 index, bool reusedIndex)
{
if (_numberOfPeriods == 0) revert NumberOfPeriodsCantBeZero();
(address token, uint256 amount) = priceCalculator.calculate(_rateLimit, _numberOfPeriods);
(index, reusedIndex) =
_setupMembershipDetails(_sender, _idCommitment, _rateLimit, _numberOfPeriods, token, amount);
_transferFees(_sender, token, amount);
}
function _transferFees(address _from, address _token, uint256 _amount) internal {
if (_token == address(0)) {
if (msg.value != _amount) revert IncorrectAmount();
} else {
if (msg.value != 0) revert OnlyTokensAccepted();
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 _numberOfPeriods the number of periods the user wants to acquire
/// @param _token Address of the token used to acquire the membership
/// @param _amount Amount of the token used to acquire the membership
/// @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,
uint32 _numberOfPeriods,
address _token,
uint256 _amount
)
internal
returns (uint32 index, bool reusedIndex)
{
if (_rateLimit < minRateLimitPerMembership || _rateLimit > maxRateLimitPerMembership) {
revert InvalidRateLimit();
}
// Attempt to free expired membership slots
while (totalRateLimitPerEpoch + _rateLimit > maxTotalRateLimitPerEpoch) {
// 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 (
oldestMembership.holder != address(0) // membership has a holder
&& isExpired(oldestMembership.gracePeriodStartDate)
) {
emit MemberExpired(head, oldestMembership.userMessageLimit, oldestMembership.index);
// Deduct the expired membership rate limit
totalRateLimitPerEpoch -= oldestMembership.userMessageLimit;
// Promote the next oldest membership to oldest
uint256 nextOldest = oldestMembership.next;
head = nextOldest;
if (nextOldest != 0) {
members[nextOldest].prev = 0;
}
// Move balance from expired membership to holder balance
balancesToWithdraw[oldestMembership.holder][oldestMembership.token] += oldestMembership.amount;
availableExpiredIndices.push(oldestMembership.index);
delete members[head];
} else {
revert ExceedMaxRateLimitPerEpoch();
}
}
uint256 prev = 0;
if (tail != 0) {
MembershipInfo storage latestMembership = members[tail];
latestMembership.next = _idCommitment;
prev = tail;
} else {
// First item
// TODO: test adding memberships after the list has been emptied
head = _idCommitment;
}
totalRateLimitPerEpoch += _rateLimit;
// Reuse available slots from previously removed expired memberships
uint256 arrLen = availableExpiredIndices.length;
if (arrLen != 0) {
index = availableExpiredIndices[arrLen - 1];
availableExpiredIndices.pop();
reusedIndex = true;
} else {
index = commitmentIndex;
}
members[_idCommitment] = MembershipInfo({
holder: _sender,
gracePeriodStartDate: block.timestamp + (uint256(billingPeriod) * uint256(_numberOfPeriods)),
gracePeriod: gracePeriod,
numberOfPeriods: _numberOfPeriods,
token: _token,
amount: _amount,
userMessageLimit: _rateLimit,
next: 0, // It's the last value, so point to nowhere
prev: prev,
index: index
});
tail = _idCommitment;
}
/// @dev Extend a membership expiration date. Membership must be on grace period
/// @param _sender the address of the holder of the membership
/// @param _idCommitment the idCommitment of the membership
function _extendMembership(address _sender, uint256 _idCommitment) public {
MembershipInfo storage mdetails = members[_idCommitment];
if (!_isGracePeriod(mdetails.gracePeriodStartDate, mdetails.gracePeriod, mdetails.numberOfPeriods)) {
// TODO: can a membership that has exceeded the expired period be extended?
revert NotInGracePeriod(_idCommitment);
}
if (_sender != mdetails.holder) revert NotHolder(_idCommitment);
uint256 newExpirationDate = block.timestamp + (uint256(billingPeriod) * uint256(mdetails.numberOfPeriods));
uint256 mdetailsNext = mdetails.next;
uint256 mdetailsPrev = mdetails.prev;
// Remove current membership references
if (mdetailsPrev != 0) {
members[mdetailsPrev].next = mdetailsNext;
} else {
head = mdetailsNext;
}
if (mdetailsNext != 0) {
members[mdetailsNext].prev = mdetailsPrev;
} else {
tail = mdetailsPrev;
}
// Move membership to the end (since it will be the newest)
mdetails.next = 0;
mdetails.prev = tail;
mdetails.gracePeriodStartDate = newExpirationDate;
mdetails.gracePeriod = gracePeriod;
members[tail].next = _idCommitment;
tail = _idCommitment;
emit MemberExtended(_idCommitment, mdetails.userMessageLimit, mdetails.index, newExpirationDate);
}
/// @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
/// @param _numberOfPeriods the number of periods the user wants to acquire
function _isExpired(
uint256 _gracePeriodStartDate,
uint32 _gracePeriod,
uint32 _numberOfPeriods
)
internal
view
returns (bool)
{
return block.timestamp > _gracePeriodStartDate + (uint256(_gracePeriod) * uint256(_numberOfPeriods));
}
/// @notice Determine if a membership is expired (has exceeded the grace period)
/// @param _idCommitment the idCommitment of the membership
function isExpired(uint256 _idCommitment) public view returns (bool) {
MembershipInfo memory m = members[_idCommitment];
return _isExpired(m.gracePeriodStartDate, m.gracePeriod, m.numberOfPeriods);
}
function expirationDate(uint256 _idCommitment) public view returns (uint256) {
MembershipInfo memory m = members[_idCommitment];
return m.gracePeriodStartDate + (uint256(m.gracePeriod) * uint256(m.numberOfPeriods));
}
/// @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
/// @param _numberOfPeriods the number of periods the user wants to acquire
function _isGracePeriod(
uint256 _gracePeriodStartDate,
uint32 _gracePeriod,
uint32 _numberOfPeriods
)
internal
view
returns (bool)
{
uint256 blockTimestamp = block.timestamp;
return blockTimestamp >= _gracePeriodStartDate
&& blockTimestamp <= _gracePeriodStartDate + (uint256(_gracePeriod) * uint256(_numberOfPeriods));
}
/// @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, m.numberOfPeriods);
}
/// @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, _mdetails.numberOfPeriods);
bool isGracePeriodAndOwned = _isGracePeriod(
_mdetails.gracePeriodStartDate, _mdetails.gracePeriod, _mdetails.numberOfPeriods
) && _mdetails.holder == _sender;
if (!membershipExpired && !isGracePeriodAndOwned) revert CantEraseMembership(_idCommitment);
emit MemberExpired(head, _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. Use 0x000...000 to withdraw ETH
function _withdraw(address _sender, address _token) internal {
uint256 amount = balancesToWithdraw[_sender][_token];
require(amount > 0, "Insufficient balance");
balancesToWithdraw[_sender][_token] = 0;
if (_token == address(0)) {
// ETH
(bool success,) = _sender.call{ value: amount }("");
require(success, "eth transfer failed");
} else {
IERC20(_token).safeTransfer(_sender, amount);
}
}
}

View File

@ -8,6 +8,9 @@ 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";
import { Membership } from "./Membership.sol";
import { IPriceCalculator } from "./IPriceCalculator.sol";
/// The tree is full
error FullTree();
@ -23,34 +26,17 @@ 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, Membership {
/// @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
uint8 public constant DEPTH = 20;
/// @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
uint32 public deployedBlockNumber;
@ -69,21 +55,39 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
_;
}
/// @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);
_;
}
constructor() {
_disableInitializers();
}
function initialize(uint32 maxMessageLimit) public 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 _billingPeriod Membership billing period
/// @param _gracePeriod Membership grace period
function initialize(
address _priceCalculator,
uint32 _maxTotalRateLimitPerEpoch,
uint16 _minRateLimitPerMembership,
uint16 _maxRateLimitPerMembership,
uint32 _billingPeriod,
uint32 _gracePeriod
)
public
initializer
{
__Ownable_init();
__UUPSUpgradeable_init();
MAX_MESSAGE_LIMIT = maxMessageLimit;
__Membership_init(
_priceCalculator,
_maxTotalRateLimitPerEpoch,
_minRateLimitPerMembership,
_maxRateLimitPerMembership,
_billingPeriod,
_gracePeriod
);
SET_SIZE = uint32(1 << DEPTH);
deployedBlockNumber = uint32(block.number);
LazyIMT.init(imtData, DEPTH);
@ -99,13 +103,6 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
return idCommitment != 0 && 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
@ -117,9 +114,9 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
/// @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) {
MembershipInfo memory member = members[idCommitment];
// we cannot call indexToCommitment if the member doesn't exist
if (member.holder == address(0)) {
return (0, 0, 0);
}
return (member.userMessageLimit, member.index, indexToCommitment(member.index));
@ -133,34 +130,46 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
return rateCommitment != 0;
}
/// Allows a user to register as a member
/// @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 numberOfPeriods The number of periods to acquire
function register(
uint256 idCommitment,
uint32 userMessageLimit
uint32 userMessageLimit,
uint32 numberOfPeriods // TODO: is there a maximum number of periods allowed?
)
external
payable
onlyValidIdCommitment(idCommitment)
onlyValidUserMessageLimit(userMessageLimit)
{
_register(idCommitment, userMessageLimit);
if (memberExists(idCommitment)) revert DuplicateIdCommitment();
uint32 index;
bool reusedIndex;
(index, reusedIndex) = _acquireMembership(_msgSender(), idCommitment, userMessageLimit, numberOfPeriods);
_register(idCommitment, userMessageLimit, index, reusedIndex);
}
/// Registers a member
/// @dev 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();
/// @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 (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;
if (reusedIndex) {
LazyIMT.update(imtData, rateCommitment, index);
} else {
LazyIMT.insert(imtData, rateCommitment);
commitmentIndex += 1;
}
emit MemberRegistered(rateCommitment, commitmentIndex);
commitmentIndex += 1;
emit MemberRegistered(rateCommitment, index);
}
/// @notice Returns the commitments of a range of members
@ -195,4 +204,70 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
}
return castedProof;
}
/// @notice Extend a membership expiration date. Memberships must be on grace period
/// @param idCommitments list of idcommitments
function extend(uint256[] calldata idCommitments) external {
for (uint256 i = 0; i < idCommitments.length; i++) {
uint256 idCommitment = idCommitments[i];
_extendMembership(_msgSender(), idCommitment);
}
}
/// @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
function eraseMemberships(uint256[] calldata idCommitments) external {
for (uint256 i = 0; i < idCommitments.length; i++) {
uint256 idCommitment = idCommitments[i];
MembershipInfo memory mdetails = members[idCommitment];
_eraseMembership(_msgSender(), idCommitment, mdetails);
LazyIMT.update(imtData, 0, mdetails.index);
}
}
/// @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
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 tree
/// @param _maxTotalRateLimitPerEpoch new value
function setMaxTotalRateLimitPerEpoch(uint32 _maxTotalRateLimitPerEpoch) external onlyOwner {
maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch;
}
/// @notice Set the maximum rate limit of one membership
/// @param _maxRateLimitPerMembership new value
function setMaxRateLimitPerMembership(uint32 _maxRateLimitPerMembership) external onlyOwner {
maxRateLimitPerMembership = _maxRateLimitPerMembership;
}
/// @notice Set the minimum rate limit of one membership
/// @param _minRateLimitPerMembership new value
function setMinRateLimitPerMembership(uint32 _minRateLimitPerMembership) external onlyOwner {
minRateLimitPerMembership = _minRateLimitPerMembership;
}
/// @notice Set the membership billing period
/// @param _billingPeriod new value
function setBillingPeriod(uint32 _billingPeriod) external onlyOwner {
billingPeriod = _billingPeriod;
}
/// @notice Set the membership grace period
/// @param _gracePeriod new value
function setGracePeriod(uint32 _gracePeriod) external onlyOwner {
gracePeriod = _gracePeriod;
}
}

12
test/TestToken.sol Normal file
View File

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TTT") { }
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}

View File

@ -5,33 +5,47 @@ 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 "../src/LinearPriceCalculator.sol"; // solhint-disable-line
import "./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;
function setUp() public virtual {
Deploy deployment = new Deploy();
(w, impl) = deployment.run();
token = new TestToken();
}
function test__ValidRegistration__kats() external {
vm.pauseGasMetering();
// Merkle tree leaves are calculated using 2 as rateLimit
vm.prank(w.owner());
w.setMinRateLimitPerMembership(2);
uint256 idCommitment = 2;
uint32 userMessageLimit = 2;
uint32 numberOfPeriods = 1;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
w.register(idCommitment, userMessageLimit);
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
vm.pauseGasMetering();
assertEq(w.commitmentIndex(), 1);
assertEq(w.memberExists(idCommitment), true);
(uint32 fetchedUserMessageLimit, uint32 index) = w.memberInfo(idCommitment);
(,,,,,, uint32 fetchedUserMessageLimit, uint32 index, address holder,) = w.members(idCommitment);
assertEq(fetchedUserMessageLimit, userMessageLimit);
assertEq(holder, address(this));
assertEq(index, 0);
// kats from zerokit
uint256 rateCommitment =
@ -74,11 +88,19 @@ 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 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
uint256 minUserMessageLimit = w.minRateLimitPerMembership();
uint256 maxUserMessageLimit = w.maxRateLimitPerMembership();
vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit);
vm.assume(w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
assertEq(w.memberExists(idCommitment), false);
w.register(idCommitment, userMessageLimit);
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]);
(uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) =
@ -86,6 +108,95 @@ contract WakuRlnV2Test is Test {
assertEq(fetchedUserMessageLimit, userMessageLimit);
assertEq(index, 0);
assertEq(fetchedRateCommitment, rateCommitment);
assertEq(address(w).balance, price);
assertEq(w.totalRateLimitPerEpoch(), userMessageLimit);
}
function test__InsertionNormalOrder(uint32 idCommitmentsLength) external {
vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 50);
uint32 userMessageLimit = w.minRateLimitPerMembership();
uint32 numberOfPeriods = 1;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, 1);
// Register some commitments
for (uint256 i = 0; i < idCommitmentsLength; i++) {
uint256 idCommitment = i + 1;
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
(uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment);
// new membership will always be the tail
assertEq(next, 0);
assertEq(w.tail(), idCommitment);
// current membership prevLink will always point to previous membership
assertEq(prev, idCommitment - 1);
}
assertEq(w.head(), 1);
assertEq(w.tail(), idCommitmentsLength);
// Ensure that prev and next are chained correctly
for (uint256 i = 0; i < idCommitmentsLength; i++) {
uint256 idCommitment = i + 1;
(uint256 prev, uint256 next,,,,,,,,) = w.members(idCommitment);
assertEq(prev, idCommitment - 1);
if (i == idCommitmentsLength - 1) {
assertEq(next, 0);
} else {
assertEq(next, idCommitment + 1);
}
}
}
function test__LinearPriceCalculation(uint32 userMessageLimit, uint32 numberOfPeriods) external view {
IPriceCalculator priceCalculator = w.priceCalculator();
uint256 pricePerMessagePerPeriod = LinearPriceCalculator(address(priceCalculator)).pricePerMessagePerPeriod();
assertNotEq(pricePerMessagePerPeriod, 0);
uint256 expectedPrice = uint256(userMessageLimit) * uint256(numberOfPeriods) * pricePerMessagePerPeriod;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
assertEq(price, expectedPrice);
}
function test__RegistrationWithTokens(
uint256 idCommitment,
uint32 userMessageLimit,
uint32 numberOfPeriods
)
external
{
vm.pauseGasMetering();
vm.assume(numberOfPeriods > 0);
LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator()));
vm.prank(priceCalculator.owner());
priceCalculator.setTokenAndPrice(address(token), 5 wei);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
uint256 minUserMessageLimit = w.minRateLimitPerMembership();
uint256 maxUserMessageLimit = w.maxRateLimitPerMembership();
vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit);
vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
token.mint(address(this), price);
token.approve(address(w), price);
w.register(idCommitment, userMessageLimit, numberOfPeriods);
assertEq(token.balanceOf(address(w)), price);
assertEq(token.balanceOf(address(this)), 0);
}
function test__InvalidETHAmount(uint256 idCommitment, uint32 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
uint256 minUserMessageLimit = w.minRateLimitPerMembership();
uint256 maxUserMessageLimit = w.maxRateLimitPerMembership();
vm.assume(userMessageLimit >= minUserMessageLimit && userMessageLimit <= maxUserMessageLimit);
vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit));
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector));
w.register{ value: price - 1 }(idCommitment, userMessageLimit, numberOfPeriods);
vm.expectRevert(abi.encodeWithSelector(IncorrectAmount.selector));
w.register{ value: price + 1 }(idCommitment, userMessageLimit, numberOfPeriods);
}
function test__IdCommitmentToMetadata__DoesntExist() external view {
@ -97,43 +208,398 @@ contract WakuRlnV2Test is Test {
}
function test__InvalidRegistration__InvalidIdCommitment__Zero() external {
vm.pauseGasMetering();
uint256 idCommitment = 0;
uint32 userMessageLimit = 2;
uint32 numberOfPeriods = 2;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0));
w.register(idCommitment, userMessageLimit);
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
}
function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external {
vm.pauseGasMetering();
uint32 userMessageLimit = 20;
uint32 numberOfPeriods = 3;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
uint256 idCommitment = w.Q() + 1;
uint32 userMessageLimit = 2;
vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment));
w.register(idCommitment, userMessageLimit);
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
}
function test__InvalidRegistration__InvalidUserMessageLimit__Zero() external {
function test__InvalidRegistration__InvalidUserMessageLimit__MinMax() external {
uint256 idCommitment = 2;
uint32 userMessageLimit = 0;
vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, 0));
w.register(idCommitment, userMessageLimit);
uint32 invalidMin = w.minRateLimitPerMembership() - 1;
uint32 invalidMax = w.maxRateLimitPerMembership() + 1;
vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector));
w.register(idCommitment, invalidMin, 1);
vm.expectRevert(abi.encodeWithSelector(InvalidRateLimit.selector));
w.register(idCommitment, invalidMax, 1);
}
function test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() external {
function test__ValidRegistrationExtend(uint32 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
uint32 userMessageLimit = w.MAX_MESSAGE_LIMIT() + 1;
vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, userMessageLimit));
w.register(idCommitment, userMessageLimit);
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.assume(
userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership()
);
vm.assume(w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
(,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment);
assertFalse(w.isGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
vm.warp(gracePeriodStartDate);
assertTrue(w.isGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
// Registering other memberships just to check linkage is correct
for (uint256 i = 1; i < 5; i++) {
w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods);
}
assertEq(w.head(), idCommitment);
uint256[] memory commitmentsToExtend = new uint256[](1);
commitmentsToExtend[0] = idCommitment;
// Attempt to extend the membership (but it is not owned by us)
address randomAddress = vm.addr(block.timestamp);
vm.prank(randomAddress);
vm.expectRevert(abi.encodeWithSelector(NotHolder.selector, commitmentsToExtend[0]));
w.extend(commitmentsToExtend);
// Attempt to extend the membership (but now we are the owner)
w.extend(commitmentsToExtend);
(,,,, uint256 newGracePeriodStartDate,,,,,) = w.members(idCommitment);
assertEq(block.timestamp + (uint256(w.billingPeriod()) * uint256(numberOfPeriods)), newGracePeriodStartDate);
assertFalse(w.isGracePeriod(idCommitment));
assertFalse(w.isExpired(idCommitment));
// Verify list order is correct
assertEq(w.tail(), idCommitment);
assertEq(w.head(), idCommitment + 1);
// Ensure that prev and next are chained correctly
for (uint256 i = 0; i < 5; i++) {
uint256 currIdCommitment = idCommitment + i;
(uint256 prev, uint256 next,,,,,,,,) = w.members(currIdCommitment);
console.log("idCommitment: %s - prev: %s - next: %s", currIdCommitment, prev, next);
if (i == 0) {
// Verifying links of extended idCommitment
assertEq(next, 0);
assertEq(prev, idCommitment + 4);
} else if (i == 1) {
// The second element in the chain became the oldest
assertEq(next, currIdCommitment + 1);
assertEq(prev, 0);
} else if (i == 4) {
assertEq(prev, currIdCommitment - 1);
assertEq(next, idCommitment);
} else {
// The rest of the elements maintain their order
assertEq(prev, currIdCommitment - 1);
assertEq(next, currIdCommitment + 1);
}
}
// TODO: should it be possible to extend expired memberships?
// Attempt to extend a non grace period membership
commitmentsToExtend[0] = idCommitment + 1;
vm.expectRevert(abi.encodeWithSelector(NotInGracePeriod.selector, commitmentsToExtend[0]));
w.extend(commitmentsToExtend);
}
function test__ValidRegistrationExpiry(uint32 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.assume(
userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership()
);
vm.assume(w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
(,,, uint32 fetchedNumberOfPeriods, uint256 fetchedGracePeriodStartDate, uint32 fetchedGracePeriod,,,,) =
w.members(idCommitment);
uint256 expectedExpirationDate =
fetchedGracePeriodStartDate + (uint256(fetchedGracePeriod) * uint256(fetchedNumberOfPeriods));
uint256 expirationDate = w.expirationDate(idCommitment);
assertEq(expectedExpirationDate, expirationDate);
vm.warp(expirationDate + 1);
assertFalse(w.isGracePeriod(idCommitment));
assertTrue(w.isExpired(idCommitment));
// Registering other memberships just to check linkage is correct
for (uint256 i = 1; i <= 5; i++) {
w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods);
}
assertEq(w.head(), idCommitment);
assertEq(w.tail(), idCommitment + 5);
}
function test__RegistrationWhenMaxRateLimitIsReached() external {
// TODO: implement
// TODO: validate elements are chained correctly
// TODO: validate reuse of index
}
function test__RegistrationWhenMaxRateLimitIsReachedAndSingleExpiredMemberAvailable() external {
// TODO: implement
// TODO: validate elements are chained correctly
// TODO: validate reuse of index
// TODO: validate balance
}
function test__RegistrationWhenMaxRateLimitIsReachedAndMultipleExpiredMembersAvailable() external {
// TODO: implement
// TODO: validate elements are chained correctly
// TODO: validate reuse of index
// TODO: validate balance
}
function test__RemoveExpiredMemberships(uint32 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.assume(
userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership()
);
vm.assume(w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
uint256 time = block.timestamp;
for (uint256 i = 0; i < 5; i++) {
w.register{ value: price }(idCommitment + i, userMessageLimit, numberOfPeriods);
time += 100;
vm.warp(time);
}
// Expiring the first 3
uint256 expirationDate = w.expirationDate(idCommitment + 2);
vm.warp(expirationDate + 1);
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;
w.eraseMemberships(commitmentsToErase);
address holder;
(,,,,,,,, holder,) = w.members(idCommitment + 1);
assertEq(holder, address(0));
(,,,,,,,, holder,) = w.members(idCommitment + 2);
assertEq(holder, address(0));
// Verify list order is correct
uint256 prev;
uint256 next;
(prev, next,,,,,,,,) = w.members(idCommitment);
assertEq(prev, 0);
assertEq(next, idCommitment + 3);
(prev, next,,,,,,,,) = w.members(idCommitment + 3);
assertEq(prev, idCommitment);
assertEq(next, idCommitment + 4);
(prev, next,,,,,,,,) = w.members(idCommitment + 4);
assertEq(prev, idCommitment + 3);
assertEq(next, 0);
assertEq(w.head(), idCommitment);
assertEq(w.tail(), idCommitment + 4);
// Attempting to call erase when some of the commitments can't be erased yet
// idCommitment can be erased (in grace period), but idCommitment + 4 is still active
(,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment + 4);
vm.warp(gracePeriodStartDate - 1);
commitmentsToErase[0] = idCommitment;
commitmentsToErase[1] = idCommitment + 4;
vm.expectRevert(abi.encodeWithSelector(CantEraseMembership.selector, idCommitment + 4));
w.eraseMemberships(commitmentsToErase);
}
function test__RemoveAllExpiredMemberships(uint32 idCommitmentsLength) external {
vm.pauseGasMetering();
vm.assume(idCommitmentsLength > 1 && idCommitmentsLength <= 100);
uint32 userMessageLimit = w.minRateLimitPerMembership();
uint32 numberOfPeriods = 5;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
uint256 time = block.timestamp;
for (uint256 i = 1; i <= idCommitmentsLength; i++) {
w.register{ value: price }(i, userMessageLimit, numberOfPeriods);
time += 100;
vm.warp(time);
}
uint256 expirationDate = w.expirationDate(idCommitmentsLength);
vm.warp(expirationDate + 1);
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;
}
w.eraseMemberships(commitmentsToErase);
// No memberships registered
assertEq(w.head(), 0);
assertEq(w.tail(), 0);
for (uint256 i = 10; i <= idCommitmentsLength + 10; i++) {
w.register{ value: price }(i, userMessageLimit, numberOfPeriods);
assertEq(w.tail(), i);
}
// Verify list order is correct
assertEq(w.head(), 10);
assertEq(w.tail(), idCommitmentsLength + 10);
uint256 prev;
uint256 next;
(prev, next,,,,,,,,) = w.members(10);
assertEq(prev, 0);
assertEq(next, 11);
(prev, next,,,,,,,,) = w.members(idCommitmentsLength + 10);
assertEq(prev, idCommitmentsLength + 9);
assertEq(next, 0);
}
function test__WithdrawETH(uint32 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.assume(
userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership()
);
vm.assume(w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
(,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment);
vm.warp(gracePeriodStartDate);
uint256[] memory commitmentsToErase = new uint256[](1);
commitmentsToErase[0] = idCommitment;
w.eraseMemberships(commitmentsToErase);
uint256 availableBalance = w.balancesToWithdraw(address(this), address(0));
assertEq(availableBalance, price);
assertEq(address(w).balance, price);
uint256 balanceBeforeWithdraw = address(this).balance;
w.withdraw(address(0));
uint256 balanceAfterWithdraw = address(this).balance;
availableBalance = w.balancesToWithdraw(address(this), address(0));
assertEq(availableBalance, 0);
assertEq(address(w).balance, 0);
assertEq(balanceBeforeWithdraw + price, balanceAfterWithdraw);
}
function test__WithdrawToken(uint32 userMessageLimit, uint32 numberOfPeriods) external {
vm.pauseGasMetering();
uint256 idCommitment = 2;
LinearPriceCalculator priceCalculator = LinearPriceCalculator(address(w.priceCalculator()));
vm.assume(numberOfPeriods > 0 && numberOfPeriods < 100);
vm.prank(priceCalculator.owner());
priceCalculator.setTokenAndPrice(address(token), 5 wei);
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
token.mint(address(this), price);
vm.assume(
userMessageLimit >= w.minRateLimitPerMembership() && userMessageLimit <= w.maxRateLimitPerMembership()
);
vm.assume(w.isValidUserMessageLimit(userMessageLimit));
vm.resumeGasMetering();
token.approve(address(w), price);
w.register(idCommitment, userMessageLimit, numberOfPeriods);
(,,,, uint256 gracePeriodStartDate,,,,,) = w.members(idCommitment);
vm.warp(gracePeriodStartDate);
uint256[] memory commitmentsToErase = new uint256[](1);
commitmentsToErase[0] = idCommitment;
w.eraseMemberships(commitmentsToErase);
uint256 availableBalance = w.balancesToWithdraw(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.balancesToWithdraw(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);
uint32 userMessageLimit = w.minRateLimitPerMembership();
uint32 numberOfPeriods = 2;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
vm.expectRevert(DuplicateIdCommitment.selector);
w.register(idCommitment, userMessageLimit);
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
}
// TODO: state has changed due to adding new variables. Update this
/*
function test__InvalidRegistration__FullTree() external {
vm.pauseGasMetering();
uint32 userMessageLimit = 2;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit);
vm.resumeGasMetering();
// we progress the tree to the last leaf
/*| Name | Type | Slot | Offset | Bytes |
|---------------------|-----------------------------------------------------|------|--------|-------|
@ -143,13 +609,15 @@ contract WakuRlnV2Test is Test {
| 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);
// 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);
w.register{ value: price }(1, userMessageLimit);
}
*/
function test__InvalidPaginationQuery__StartIndexGTEndIndex() external {
vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0));
@ -162,9 +630,14 @@ contract WakuRlnV2Test is Test {
}
function test__ValidPaginationQuery__OneElement() external {
uint32 userMessageLimit = 2;
vm.pauseGasMetering();
uint256 idCommitment = 1;
w.register(idCommitment, userMessageLimit);
uint32 userMessageLimit = w.minRateLimitPerMembership();
uint32 numberOfPeriods = 2;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
vm.resumeGasMetering();
w.register{ value: price }(idCommitment, userMessageLimit, numberOfPeriods);
uint256[] memory commitments = w.getCommitments(0, 0);
assertEq(commitments.length, 1);
uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]);
@ -172,12 +645,14 @@ contract WakuRlnV2Test is Test {
}
function test__ValidPaginationQuery(uint32 idCommitmentsLength) external {
vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100);
uint32 userMessageLimit = 2;
vm.pauseGasMetering();
vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100);
uint32 userMessageLimit = w.minRateLimitPerMembership();
uint32 numberOfPeriods = 2;
(, uint256 price) = w.priceCalculator().calculate(userMessageLimit, numberOfPeriods);
for (uint256 i = 0; i < idCommitmentsLength; i++) {
w.register(i + 1, userMessageLimit);
w.register{ value: price }(i + 1, userMessageLimit, numberOfPeriods);
}
vm.resumeGasMetering();
@ -191,7 +666,7 @@ contract WakuRlnV2Test is Test {
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());
@ -207,4 +682,6 @@ contract WakuRlnV2Test is Test {
);
assertEq(fetchedImpl, newImpl);
}
receive() external payable { }
}