diff --git a/contracts/IPriceCalculator.sol b/contracts/IPriceCalculator.sol index 335c0ba..6810adf 100644 --- a/contracts/IPriceCalculator.sol +++ b/contracts/IPriceCalculator.sol @@ -5,4 +5,4 @@ interface IPriceCalculator { /// Returns the token and price to pay in `token` for some `_rateLimit` /// @param _rateLimit the rate limit the user wants to acquire function calculate(uint _rateLimit) external view returns (address, uint); -} \ No newline at end of file +} diff --git a/contracts/Membership.sol b/contracts/Membership.sol index 8c93c05..49509ba 100644 --- a/contracts/Membership.sol +++ b/contracts/Membership.sol @@ -12,85 +12,208 @@ error TokenMismatch(); error InvalidRateLimit(); error ExceedMaxRateLimitPerEpoch(); +error NotInGracePeriod(uint256 membershipMapIdx); +error NotHolder(uint256 membershipMapIdx); contract Membership { using SafeERC20 for IERC20; - IPriceCalculator public priceCalculator; + // TODO START - add owned setters to all these variables - uint public maxTotalRateLimitPerEpoch; + IPriceCalculator public priceCalculator; + uint256 public maxTotalRateLimitPerEpoch; uint16 public maxRateLimitPerMembership; uint16 public minRateLimitPerMembership; - enum MembershipStatus { Undefined, Active, GracePeriod, Expired, ErasedAwaitsWithdrawal, Erased } + uint256 public expirationTerm; + uint256 public gracePeriod; + + // TODO END + + enum MembershipStatus { + NonExistent, + Active, + GracePeriod, + Expired, + ErasedAwaitsWithdrawal, + Erased + } uint public totalRateLimitPerEpoch; + mapping(uint256 => MembershipDetails) public memberships; + uint256 public oldestMembership = 1; + uint256 public newestMembership = 1; + + struct MembershipDetails { + address holder; + // TODO: should we store the commitment? + uint256 expirationDate; + address token; + uint256 amount; + } + + event ExpiredMembership(uint256 membershipMapIndex, address holder); // TODO: should it contain the commitment? + event MembershipExtended( + uint256 membershipMapIndex, + uint256 newExpirationDate + ); // TODO: should it contain the commitment? + function __Membership_init( address _priceCalculator, uint _maxTotalRateLimitPerEpoch, uint16 _maxRateLimitPerMembership, - uint16 _minRateLimitPerMembership + uint16 _minRateLimitPerMembership, + uint _expirationTerm, + uint _gracePeriod ) internal { priceCalculator = IPriceCalculator(_priceCalculator); maxTotalRateLimitPerEpoch = _maxTotalRateLimitPerEpoch; maxRateLimitPerMembership = _maxRateLimitPerMembership; minRateLimitPerMembership = _minRateLimitPerMembership; + expirationTerm = _expirationTerm; + gracePeriod = _gracePeriod; } - function transferMembershipFees(address _from, uint _rateLimit) internal { - (address token, uint price) = priceCalculator.calculate(_rateLimit); - if (token == address(0)) { - if (msg.value != price) revert IncorrectAmount(); + function registerMembership( + address _sender, + uint256[] memory commitments, + uint _rateLimit + ) internal { + // TODO: for each commitment? + (address token, uint amount) = priceCalculator.calculate(_rateLimit); + acquireRateLimit(_sender, commitments, _rateLimit, token, amount); + transferMembershipFees( + _sender, + token, + amount * _rateLimit * commitments.length + ); + } + + function transferMembershipFees( + address _from, + address _token, + uint _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), price); + IERC20(_token).safeTransferFrom(_from, address(this), _amount); } } - function acquireRateLimit(uint256[] memory commitments, uint _rateLimit) internal { + function acquireRateLimit( + address _sender, + uint256[] memory _commitments, + uint256 _rateLimit, + address _token, + uint256 _amount + ) internal { + // TODO: for each commitment? if ( _rateLimit < minRateLimitPerMembership || _rateLimit > maxRateLimitPerMembership ) revert InvalidRateLimit(); uint newTotalRateLimitPerEpoch = totalRateLimitPerEpoch + _rateLimit; - if (newTotalRateLimitPerEpoch > maxTotalRateLimitPerEpoch) revert ExceedMaxRateLimitPerEpoch(); - // TODO: store _rateLimit - // TODO: - // Epoch length epoch 10 minutes - // Membership expiration term T 180 days - // Membership grace period G 30 days + if (newTotalRateLimitPerEpoch > maxTotalRateLimitPerEpoch) { + // Determine if there are any available spot in the membership map + // by looking at the oldest membership. If it's expired, we can use it + MembershipDetails storage oldestMembershipDetails = memberships[ + oldestMembership + ]; + + if (isExpired(oldestMembershipDetails.expirationDate)) { + emit ExpiredMembership( + oldestMembership, + memberships[oldestMembership].holder + ); + deleteOldestMembership(); + // TODO: move balance from expired to the current holder + } else { + revert ExceedMaxRateLimitPerEpoch(); + } + } + + newestMembership += 1; + memberships[newestMembership] = MembershipDetails({ + holder: _sender, + expirationDate: block.timestamp + expirationTerm, + token: _token, + amount: _amount + }); } - mapping (uint256 => TODO) memberships; - - uint256 private oldest = 1; - uint256 private newest = 1; + function extendMembership( + address _sender, + uint256[] memory membershipMapIdx + ) public { + for (uint256 i = 0; i < membershipMapIdx.length; i++) { + uint256 currentMembershipMapIdx = membershipMapIdx[i]; - // TODO - use a struct to store the membership details: commitment, date, status - // TODO - keep track of balances, use msg.sender? + MembershipDetails storage mdetails = memberships[ + currentMembershipMapIdx + ]; - function pushMembership(TODO data) internal { - newest += 1; - memberships[newest] = data; + if (!_isGracePeriod(mdetails.expirationDate)) + revert NotInGracePeriod(currentMembershipMapIdx); + + if (_sender != mdetails.holder) + revert NotHolder(currentMembershipMapIdx); + + uint256 newExpirationDate = block.timestamp + expirationTerm; + + // TODO: remove current membership + // TODO: add membership at the end (since it will be the newest) + + emit MembershipExtended(currentMembershipMapIdx, newExpirationDate); + } } - function oldestMembership() public TODO { - return memberships[oldest]; + function _isExpired(uint256 expirationDate) internal view returns (bool) { + return expirationDate + gracePeriod > block.timestamp; } - function popOldestMembership() public returns (uint256) { - TODO data; - require(newest > oldest); - data = memberships[oldest]; - delete memberships[oldest]; - oldest += 1; - return data; + function isExpired(uint256 membershipMapIdx) public view returns (bool) { + return _isExpired(memberships[membershipMapIdx].expirationDate); } - function length() public view returns (uint256) { - return newest - oldest; + function _isGracePeriod( + uint256 expirationDate + ) internal view returns (bool) { + return + block.timestamp >= expirationDate && + block.timestamp <= expirationDate + gracePeriod; + } + + function isGracePeriod( + uint256 membershipMapIdx + ) public view returns (bool) { + uint256 expirationDate = memberships[membershipMapIdx].expirationDate; + return _isGracePeriod(expirationDate); + } + + function withdraw() public {} + + // TODO - keep track of balances, use msg.sender + + function getOldestMembership() + public + view + returns (MembershipDetails memory) + { + return memberships[oldestMembership]; + } + + function deleteOldestMembership() internal { + require(newestMembership > oldestMembership); + delete memberships[oldestMembership]; + oldestMembership += 1; + } + + function getMembershipLength() public view returns (uint256) { + return newestMembership - oldestMembership; } } diff --git a/contracts/WakuRlnRegistry.sol b/contracts/WakuRlnRegistry.sol index f5e395f..003b093 100644 --- a/contracts/WakuRlnRegistry.sol +++ b/contracts/WakuRlnRegistry.sol @@ -83,8 +83,7 @@ contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable, Membership { function register(uint16 storageIndex, uint256[] calldata commitments) external onlyValidStorageIndex(storageIndex) { // TODO: modify function to receive the ratelimit to buy uint _rateLimit = 4; - acquireRateLimit(commitments, _rateLimit); - transferMembershipFees(_msgSender(), _rateLimit * commitments.length); + registerMembership(_msgSender(), commitments, _rateLimit); WakuRln(storages[storageIndex]).register(commitments); } @@ -95,8 +94,7 @@ contract WakuRlnRegistry is OwnableUpgradeable, UUPSUpgradeable, Membership { // TODO: modify function to receive the number of messages uint _rateLimit = 4; - acquireRateLimit(commitments, _rateLimit); - transferMembershipFees(_msgSender(), _rateLimit); + registerMembership(_msgSender(), commitments, _rateLimit); WakuRln(storages[storageIndex]).register(commitments); }