Skip to content

Add fixed subscription duration#120

Open
viatrix wants to merge 8 commits intomainfrom
feat/fixed-subscription-price
Open

Add fixed subscription duration#120
viatrix wants to merge 8 commits intomainfrom
feat/fixed-subscription-price

Conversation

@viatrix
Copy link
Copy Markdown

@viatrix viatrix commented Apr 29, 2026

Implements #31

@viatrix viatrix requested a review from rob1997 April 29, 2026 13:13
Copy link
Copy Markdown
Contributor

@rob1997 rob1997 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work! Clean PR I like it but needs some changes

Comment thread src/Asset.sol Outdated
}

function getSubscriptionDuration(uint256 value) external view returns (uint256) {
return (value / subscriptionPrice) * SUBSCRIPTION_DURATION;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should enforce value to be a multiple of subscriptionPrice in which case getSubscriptionPriceAndDuration already covers its use case, so it's not relevant.

Comment thread src/Asset.sol Outdated
@@ -146,7 +177,7 @@ contract Asset is Ownable, ReentrancyGuard, IAsset {
}

function _subscribe(bytes32 subscriber, address payer, uint256 value) internal returns (uint256) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on this new implementation, value should be refactored to count instead, so value = count * subscriptionPrice and duration = subscriptionDuration * count, subscribe should be changed from
subscribe(bytes32 subscriber, address payer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
to
subscribe(bytes32 subscriber, address payer, address spender, uint256 count, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
signature is the same but value and duration derivation changes

Comment thread src/Asset.sol
bool isRegistry,
uint256 timestamp
) internal view returns (uint256 claimable, uint256 claimedNonce) {
) internal view returns (uint256 claimable, uint256 claimedNonce, uint256 newClaimedAtTimestamp) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claim function should not change, the Asset/Registry Owner should still be able to claim all the amount due. If this is a fix for a flaw/bug lmk but if this is just an optimization please create another PR for it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was added to avoid losing dust during claiming. If we allow claiming for partial periods then some money will be lost in this computation and stuck in the contract (for example, the price for the 2-week period is 5, we claim for 1 week, half of that, round to 2, then claim for the other half, round to 2 again, and 1 is lost). One of the solutions is to claim only for whole passed periods, that's why the time is clamped to the end of the period and timestamps are stored. Let me know if we should choose another solution

Copy link
Copy Markdown
Contributor

@rob1997 rob1997 Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right yeah, Good catch!

One of the solutions is to claim only for whole passed periods

Yeah this is a good rule, lets go with this!

Comment thread src/Asset.sol Outdated
// Active subscription: mark cancelled immediately; truncate endTime to end of last
// started period so _claimable charges the full in-progress period as fee
subscriptions[id].cancelled = true;
subscriptions[id].endTime = subscription.endTime - (refundablePeriods * SUBSCRIPTION_DURATION);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while emitting SubscriptionCancelled perhaps we should also add the new endTime of the subscription as an input

Comment thread src/Asset.sol Outdated

uint256 returnable = 0;
// Effective start for refund: use startTime for future subscriptions, timestamp for active subscriptions
uint256 effectiveStart = subscription.startTime >= timestamp ? subscription.startTime : timestamp;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (subscription.startTime >= timestamp) subscription hasn't started yet, considering that subscription duration must be a multiple of subscriptionDuration refundable amount would be equal to ((endTime - startTime) / subscriptionDuration) * subscriptionPrice, we can avoid some extra computation.

Comment thread src/Asset.sol Outdated
// and payer are the same (and it was not cancelled).
if (
startTime == subscription.endTime && subscription.payer == payer
!subscription.cancelled && startTime == subscription.endTime && subscription.payer == payer
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should allow subscription extensions even after a subscription has been cancelled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since endTime - startTime is still a multiple of subscriptionDuration even after cancellation it shouldn't be an issue

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current implementation endTime can be in the future for the cancelled subscription. What should the behavior be? I suppose we should create a new subscription, not extend the cancelled one

Copy link
Copy Markdown
Contributor

@rob1997 rob1997 Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still be able to extend it, there's no need to add a new index/nonce

Comment thread src/Asset.sol Outdated
uint256 subscriptionPrice;
uint256 registryFeeShare;
address payer;
bool cancelled;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this additional field cancelled to avoid extra computation if a subscriber tries to cancel again? Which isn't consequential because it's unlikely and the computation is also negligible since we start the loop from the latest subscription and the condition check should be simple enough
if ((endTime - timestamp) / subscriptionDuration == 0) break;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cancelled field was added to allow claiming fees for the future part of the current period. If we just set endTime for the subscription to the current timestamp then the claiming should be modified as well because it would lose the future part of the fee. In the current version, the endTime is clamped to the end of the current period to allow full fee collection, but it would mean that the subscription stays active without introducing the cancelled flag. We can discuss other options

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not suggesting we set endTime to current timestamp, it should be set to the end of the current period yes but claiming in its current implementation can only claim any amount till the current timestamp. Consider the following

duration = 3
startTime = 2
endTime = 14

This means subscribed for 4 periods, if subscriber cancels at timestamp = 6 so now endTime can now be 8. 8 - 14 (2 periods) is refundable. Asset/Registry owners can still claim for duration 2 - 8 until timestamp == 8

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's how claiming will work now: only for fully passed periods. If we set endTime to the end of the current period then the subscription will be still considered active without the cancelled flag

@viatrix viatrix requested a review from rob1997 May 5, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants