mirror of
https://github.com/logos-messaging/logos-messaging-rlnv2-contract.git
synced 2026-03-31 00:43:17 +00:00
348 lines
16 KiB
Solidity
348 lines
16 KiB
Solidity
// 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 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 ExceededMaxTotalRateLimit();
|
|
|
|
// This membership is not in grace period yet // FIXME: yet or also already?
|
|
error NotInGracePeriod(uint256 idCommitment);
|
|
|
|
// The sender is not the holder of the membership
|
|
error AttemptedExtensionByNonHolder(uint256 idCommitment);
|
|
|
|
// This membership cannot be erased (either it is not expired or not in grace period and/or not the owner) // FIXME:
|
|
// separate into two errors?
|
|
error CantEraseMembership(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; // FIXME: naming: price vs deposit?
|
|
|
|
/// @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 expiration term (T in the spec)
|
|
uint32 public expirationTerm;
|
|
|
|
/// @notice Membership grace period (G in the spec)
|
|
uint32 public gracePeriodDuration;
|
|
|
|
/// @notice deposit balances available to withdraw // FIXME: are balances unavailable for withdrawal stored
|
|
/// elsewhere?
|
|
mapping(address holder => mapping(address token => uint256 balance)) public balancesToWithdraw;
|
|
|
|
/// @notice Total rate limit of all memberships in the membership set (messages per epoch)
|
|
uint256 public totalRateLimit;
|
|
|
|
/// @notice List of registered memberships
|
|
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 expired memberships (can be erased)
|
|
uint32[] public availableExpiredMembershipsIndices;
|
|
|
|
struct MembershipInfo {
|
|
/// @notice amount of the token used to make the deposit to register this membership
|
|
uint256 depositAmount;
|
|
/// @notice timestamp of when the grace period starts for this membership
|
|
uint256 gracePeriodStartTimestamp;
|
|
/// @notice duration of the grace period
|
|
uint32 gracePeriodDuration;
|
|
/// @notice the membership rate limit
|
|
uint32 rateLimit;
|
|
/// @notice the index of the membership in the membership set
|
|
uint32 index;
|
|
/// @notice address of the holder of this membership
|
|
address holder;
|
|
/// @notice token used to make the deposit to register this membership
|
|
address token;
|
|
}
|
|
|
|
/// @notice Emitted when a membership is erased due to having exceeded the grace period or the owner having chosen
|
|
/// to not extend it // FIXME: expired or erased?
|
|
/// @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 in its grace period is 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
|
|
/// @param newExpirationTime the new expiration timestamp of this membership
|
|
event MembershipExtended(uint256 idCommitment, uint32 membershipRateLimit, uint32 index, uint256 newExpirationTime);
|
|
|
|
/// @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 _expirationTerm Expiration term of each membership
|
|
/// @param _gracePeriodDuration Grace period duration for each membership
|
|
function __MembershipUpgradeable_init(
|
|
address _priceCalculator,
|
|
uint32 _maxTotalRateLimit,
|
|
uint32 _minMembershipRateLimit,
|
|
uint32 _maxMembershipRateLimit,
|
|
uint32 _expirationTerm,
|
|
uint32 _gracePeriodDuration
|
|
)
|
|
internal
|
|
onlyInitializing
|
|
{
|
|
__MembershipUpgradeable_init_unchained(
|
|
_priceCalculator,
|
|
_maxTotalRateLimit,
|
|
_minMembershipRateLimit,
|
|
_maxMembershipRateLimit,
|
|
_expirationTerm,
|
|
_gracePeriodDuration
|
|
);
|
|
}
|
|
|
|
function __MembershipUpgradeable_init_unchained(
|
|
address _priceCalculator,
|
|
uint32 _maxTotalRateLimit,
|
|
uint32 _minMembershipRateLimit,
|
|
uint32 _maxMembershipRateLimit,
|
|
uint32 _expirationTerm,
|
|
uint32 _gracePeriodDuration
|
|
)
|
|
internal
|
|
onlyInitializing
|
|
{
|
|
require(_maxTotalRateLimit >= maxMembershipRateLimit);
|
|
require(_maxMembershipRateLimit > minMembershipRateLimit); // FIXME: > or >=?
|
|
require(_minMembershipRateLimit > 0);
|
|
require(_expirationTerm > 0); // FIXME: also _gracePeriodDuration > 0?
|
|
|
|
priceCalculator = IPriceCalculator(_priceCalculator);
|
|
maxTotalRateLimit = _maxTotalRateLimit;
|
|
maxMembershipRateLimit = _maxMembershipRateLimit;
|
|
minMembershipRateLimit = _minMembershipRateLimit;
|
|
expirationTerm = _expirationTerm;
|
|
gracePeriodDuration = _gracePeriodDuration;
|
|
}
|
|
|
|
/// @notice Checks if a membership rate limit is valid. This does not take into account whether the total
|
|
/// memberships have reached already the `maxTotalRateLimit` // FIXME: clarify
|
|
/// @param membershipRateLimit The membership rate limit
|
|
/// @return true if the membership rate limit is valid, false otherwise
|
|
function isValidMembershipRateLimit(uint32 membershipRateLimit) external view returns (bool) {
|
|
return membershipRateLimit >= minMembershipRateLimit && membershipRateLimit <= maxMembershipRateLimit;
|
|
}
|
|
|
|
/// @dev acquire a membership and trasnfer the fees to the contract // FIXME: fees == deposit?
|
|
/// @param _sender address of the owner of the new membership
|
|
/// @param _idCommitment the idCommitment of the new membership
|
|
/// @param _rateLimit the membership rate limit
|
|
/// @return index the index in the membership set
|
|
/// @return indexReused indicates whether using a new Merkle tree leaf or an existing one
|
|
function _acquireMembership(
|
|
address _sender,
|
|
uint256 _idCommitment,
|
|
uint32 _rateLimit
|
|
)
|
|
internal
|
|
returns (uint32 index, bool indexReused)
|
|
{
|
|
(address token, uint256 amount) = priceCalculator.calculate(_rateLimit);
|
|
(index, indexReused) = _setupMembershipDetails(_sender, _idCommitment, _rateLimit, token, amount);
|
|
_transferFees(_sender, token, amount);
|
|
}
|
|
|
|
function _transferFees(address _from, address _token, uint256 _amount) internal {
|
|
IERC20(_token).safeTransferFrom(_from, address(this), _amount);
|
|
}
|
|
|
|
/// @dev Setup a new membership. If there are not enough remaining rate limit to acquire
|
|
/// a new membership, it will attempt to erase existing expired memberships
|
|
/// and reuse one of their slots
|
|
/// @param _sender holder of the membership. Generally `msg.sender`
|
|
/// @param _idCommitment idCommitment
|
|
/// @param _rateLimit membership rate limit
|
|
/// @param _token Address of the token used to acquire the membership
|
|
/// @param _depositAmount Amount of the tokens used to acquire the membership
|
|
/// @return index membership index in the membership set
|
|
/// @return indexReused indicates whether the index returned was a reused slot on the tree or not
|
|
function _setupMembershipDetails(
|
|
address _sender,
|
|
uint256 _idCommitment,
|
|
uint32 _rateLimit,
|
|
address _token,
|
|
uint256 _depositAmount
|
|
)
|
|
internal
|
|
returns (uint32 index, bool indexReused)
|
|
{
|
|
if (_rateLimit < minMembershipRateLimit || _rateLimit > maxMembershipRateLimit) {
|
|
revert InvalidRateLimit();
|
|
}
|
|
|
|
// Determine if we exceed the total rate limit
|
|
totalRateLimit += _rateLimit;
|
|
if (totalRateLimit > maxTotalRateLimit) {
|
|
revert ExceededMaxTotalRateLimit();
|
|
}
|
|
|
|
// Reuse available slots from previously removed (FIXME: clarify "removed") expired memberships
|
|
(index, indexReused) = _getFreeIndex();
|
|
|
|
memberships[_idCommitment] = MembershipInfo({
|
|
holder: _sender, // FIXME: inconsistent with spec (keeper vs holder)
|
|
gracePeriodStartTimestamp: block.timestamp + uint256(expirationTerm),
|
|
gracePeriodDuration: gracePeriodDuration,
|
|
token: _token,
|
|
depositAmount: _depositAmount,
|
|
rateLimit: _rateLimit,
|
|
index: index
|
|
});
|
|
}
|
|
|
|
/// @dev Get a free index (possibly from reusing a slot of an expired membership)
|
|
/// @return index index to be used for another membership registration
|
|
/// @return indexReused indicates whether index comes form reusing a slot of an expired membership
|
|
function _getFreeIndex() internal returns (uint32 index, bool indexReused) {
|
|
// Reuse available slots from previously removed (FIXME: clarify "removed") expired memberships
|
|
uint256 arrLen = availableExpiredMembershipsIndices.length;
|
|
if (arrLen != 0) {
|
|
index = availableExpiredMembershipsIndices[arrLen - 1];
|
|
availableExpiredMembershipsIndices.pop();
|
|
indexReused = true;
|
|
} else {
|
|
index = nextFreeIndex;
|
|
}
|
|
}
|
|
|
|
/// @dev Extend the expiration date of a grace-period membership
|
|
/// @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 membership = memberships[_idCommitment];
|
|
|
|
if (!_isInGracePeriodNow(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration)) {
|
|
revert NotInGracePeriod(_idCommitment);
|
|
}
|
|
|
|
if (_sender != membership.holder) revert AttemptedExtensionByNonHolder(_idCommitment); // FIXME: turn into a
|
|
// modifier?
|
|
// FIXME: see spec: should extension depend on the current block.timestamp?
|
|
uint256 newGracePeriodStartTimestamp = block.timestamp + uint256(expirationTerm);
|
|
|
|
membership.gracePeriodStartTimestamp = newGracePeriodStartTimestamp;
|
|
membership.gracePeriodDuration = gracePeriodDuration; // FIXME: redundant: just assigns old value
|
|
|
|
emit MembershipExtended(
|
|
_idCommitment, membership.rateLimit, membership.index, membership.gracePeriodStartTimestamp
|
|
);
|
|
}
|
|
|
|
/// @dev Determine whether a grace period has passed (the membership is expired)
|
|
/// @param _gracePeriodStartTimestamp timestamp in which the grace period starts
|
|
/// @param _gracePeriodDuration duration of the grace period
|
|
function _isExpired(uint256 _gracePeriodStartTimestamp, uint32 _gracePeriodDuration) internal view returns (bool) {
|
|
return block.timestamp > _gracePeriodStartTimestamp + uint256(_gracePeriodDuration);
|
|
}
|
|
|
|
/// @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 _isExpired(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
|
|
}
|
|
|
|
/// @notice Returns the timestamp on which a membership can be considered expired
|
|
/// @param _idCommitment the idCommitment of the membership
|
|
function membershipExpirationTimestamp(uint256 _idCommitment) public view returns (uint256) {
|
|
MembershipInfo memory membership = memberships[_idCommitment];
|
|
return membership.gracePeriodStartTimestamp + uint256(membership.gracePeriodDuration) + 1;
|
|
}
|
|
|
|
/// @dev Determine whether the current timestamp is in a given grace period
|
|
/// @param _gracePeriodStartTimestamp timestamp in which the grace period starts
|
|
/// @param _gracePeriodDuration duration of the grace period
|
|
function _isInGracePeriodNow(
|
|
uint256 _gracePeriodStartTimestamp,
|
|
uint32 _gracePeriodDuration
|
|
)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
uint256 blockTimestamp = block.timestamp;
|
|
return blockTimestamp >= _gracePeriodStartTimestamp
|
|
&& blockTimestamp <= _gracePeriodStartTimestamp + uint256(_gracePeriodDuration);
|
|
}
|
|
|
|
/// @notice Determine if a membership is in grace period now
|
|
/// @param _idCommitment the idCommitment of the membership
|
|
function isInGracePeriodNow(uint256 _idCommitment) public view returns (bool) {
|
|
MembershipInfo memory membership = memberships[_idCommitment];
|
|
return _isInGracePeriodNow(membership.gracePeriodStartTimestamp, membership.gracePeriodDuration);
|
|
}
|
|
|
|
/// @dev Erase expired memberships or owned grace-period memberships.
|
|
/// @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 _membership) internal {
|
|
bool membershipExpired = _isExpired(_membership.gracePeriodStartTimestamp, _membership.gracePeriodDuration);
|
|
bool isInGracePeriodAndOwned = // FIXME: separate into two checks? cf. short-circuit
|
|
_isInGracePeriodNow(_membership.gracePeriodStartTimestamp, _membership.gracePeriodDuration)
|
|
&& _membership.holder == _sender;
|
|
// FIXME: we already had a non-holder check: reuse it here as a modifier
|
|
if (!membershipExpired && !isInGracePeriodAndOwned) revert CantEraseMembership(_idCommitment);
|
|
|
|
emit MembershipExpired(_idCommitment, _membership.rateLimit, _membership.index);
|
|
|
|
// Move balance from expired membership to holder balance
|
|
balancesToWithdraw[_membership.holder][_membership.token] += _membership.depositAmount;
|
|
|
|
// Deduct the expired membership rate limit
|
|
totalRateLimit -= _membership.rateLimit;
|
|
|
|
// Note: the Merkle tree data will be erased lazily later // FIXME: when?
|
|
availableExpiredMembershipsIndices.push(_membership.index);
|
|
|
|
delete memberships[_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
|
|
function _withdraw(address _sender, address _token) internal {
|
|
require(_token != address(0), "ETH is not allowed");
|
|
|
|
uint256 amount = balancesToWithdraw[_sender][_token];
|
|
require(amount > 0, "Insufficient balance");
|
|
|
|
balancesToWithdraw[_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;
|
|
}
|