mirror of
https://github.com/logos-messaging/logos-messaging-rlnv2-contract.git
synced 2026-01-11 02:13:05 +00:00
477 lines
21 KiB
Solidity
477 lines
21 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.24;
|
|
|
|
import { IPriceCalculator } from "./IPriceCalculator.sol";
|
|
import { IDAIPermit } from "./IDAIPermit.sol";
|
|
import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.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(_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);
|
|
}
|
|
|
|
/// @dev acquire a membership and transfer the deposit to the contract
|
|
/// Uses the RC20 Permit extension allowing approvals to be made via signatures, as defined in
|
|
/// [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612).
|
|
/// @param _owner The address of the token owner who is giving permission and will own the membership.
|
|
/// @param _deadline The timestamp until when the permit is valid.
|
|
/// @param _v The recovery byte of the signature.
|
|
/// @param _r Half of the ECDSA signature pair.
|
|
/// @param _s Half of the ECDSA signature pair.
|
|
/// @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 _acquireMembershipWithPermit(
|
|
address _owner,
|
|
uint256 _deadline,
|
|
uint8 _v,
|
|
bytes32 _r,
|
|
bytes32 _s,
|
|
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);
|
|
|
|
ERC20Permit(token).permit(_owner, address(this), depositAmount, _deadline, _v, _r, _s);
|
|
|
|
// Possibly reuse an index of an erased membership
|
|
(index, indexReused) = _getFreeIndex();
|
|
|
|
memberships[_idCommitment] = MembershipInfo({
|
|
holder: _owner,
|
|
activeDuration: activeDurationForNewMemberships,
|
|
gracePeriodStartTimestamp: block.timestamp + uint256(activeDurationForNewMemberships),
|
|
gracePeriodDuration: gracePeriodDurationForNewMemberships,
|
|
token: token,
|
|
depositAmount: depositAmount,
|
|
rateLimit: _rateLimit,
|
|
index: index
|
|
});
|
|
|
|
IERC20(token).safeTransferFrom(_owner, address(this), depositAmount);
|
|
}
|
|
|
|
/// @dev acquire a membership and transfer the deposit to the contract
|
|
/// Uses DAI permit extension allowing approvals to be made via signatures
|
|
/// @param _owner The address of the token owner who is giving permission and will own the membership.
|
|
/// @param _deadline The timestamp until when the permit is valid.
|
|
/// @param _nonce The nonce used for the permission
|
|
/// @param _v The recovery byte of the signature.
|
|
/// @param _r Half of the ECDSA signature pair.
|
|
/// @param _s Half of the ECDSA signature pair.
|
|
/// @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 _acquireMembershipWithDAIPermit(
|
|
address _owner,
|
|
uint256 _deadline,
|
|
uint256 _nonce,
|
|
uint8 _v,
|
|
bytes32 _r,
|
|
bytes32 _s,
|
|
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);
|
|
|
|
IDAIPermit(token).permit(_owner, address(this), _nonce, _deadline, true, _v, _r, _s);
|
|
|
|
// Possibly reuse an index of an erased membership
|
|
(index, indexReused) = _getFreeIndex();
|
|
|
|
memberships[_idCommitment] = MembershipInfo({
|
|
holder: _owner,
|
|
activeDuration: activeDurationForNewMemberships,
|
|
gracePeriodStartTimestamp: block.timestamp + uint256(activeDurationForNewMemberships),
|
|
gracePeriodDuration: gracePeriodDurationForNewMemberships,
|
|
token: token,
|
|
depositAmount: depositAmount,
|
|
rateLimit: _rateLimit,
|
|
index: index
|
|
});
|
|
|
|
IERC20(token).safeTransferFrom(_owner, 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) internal {
|
|
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;
|
|
}
|