2025-06-19 10:00:25 -04:00

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;
}