Daisy Contracts
(DOCGEN NOT WORKING IN CURRENT VERSION) NatSpec docs are available here (generated with solidity-codegen)
Overview
Subscriptions
A subscription is composed of:
- subscriber: address from which tokens will be transferred from.
- token: address of the ERC 20 token that the subscription is denominated in.
- amount: number of tokens to transfer in each payment.
- periodUnit: "Day", "Month", or "Year".
- periods: number of period units between payments.
- maxExecutions: max number of payments that can be made before the subscriber must manually renew their subscription. If this value is 0, there is no execution limit.
A subscription can be in one of four states:
- Created: after it is created but has yet to be executed.
- Active: Once it has been executed and the next payment is in the future.
- Active and Cancelled: When it is cancelled before reaching the next payment timestamp. It will remain active until the end of the period.
- Cancelled: when it has been cancelled and the last billed period has ended.
- Deleted: the subscription data will be deleted upon execution if the subscription can't be executed anymore.
Plans
A plan is composed of:
- periodUnit: "Day", "Month", or "Year"
- periods: number of period units between payments
- amount: number of tokens to transfer in each payment
- maxExecutions: max number of payments that can be made before the subscriber must manually renew their subscription. If this value is 0, there is no execution limit.
- priv: boolean indicating whether the plan is private of public
- active: boolean indicating if the plan is accepting new subscribers or not
- removed: boolean indicating if the plan is ongoing or not
When a plan based subscription is created, it is necessary to specify an identifier for the plan, and the creation only goes through if the subscription parameters match the plan fields.
A plan can be created as public or private. A public plan accepts all subscriptions while a private plan requires each subscription to be authorized by providing a cryptographic signature generated by the authorizer
. The data signed by the authorizer contains a subscriber
address which needs to match the subscription's subscriber or be the zero address (in which case anyone can use that signature to join the private plan).
A plan can be in one of three states:
- Active: the plan can accept new subscriptions.
- Inactive: the plan doesn't accept new subscriptions, but existing subscriptions can be executed normally.
- Removed: the owner can remove a plan and this causes the existing subscriptions attached to the plan to become invalid. A removed plan can't accept new subscriptions.
Core Contracts
SubscriptionManager.sol
This is the base implementation. It handles creation, execution, and cancellation, and provides internal methods that can be used or overriden by child contracts.
It also provides helpers like getActionHash
, _getPeriodUnitHash
and _replayGuard
for delegated execution (optional extension for delegated execution).
Finally, it provides methods for enumerating subscriptions and subscription by subscriber (optional extension for enumeration). This implementation was adapted from OpenZeppelin's EIP721Enumerable
implementation.
Specific Use Cases
PlanSubscriptionManager.sol
This contract inherits from the base contract and implements plan based subscriptions.
Time
To support different time units (days, months and years) we use BokkyPooBah's Date Time Library.
Subscription Flow with Delegated Execution (for Plan based usecase)
- Subscriber calls the
approve
function in the token in which the subscription is denominated in to allow the manager contract to manipulate her funds. - Using EIP 712, subscriber signs the creation of a subscription.
- A relayer executes the creation by calling the method
create()
and passing the parameters and signature. - At the beggining of each payment period, a relayer can execute the subscription and bill the subscriber.
For subscription services created using Daisy the platform, Daisy will serve as the relayer in this flow.
Off-chain service flow
Checking if the publisher needs to provide the service
- Receive request from subscriber.
- Call
subscriptionManager.nextPaymentTimestamp(subscriptionHash)
. - If
now < nextPaymentTimestamp
, answer the request.
Executing subscriptions
To retrieve all subscription hashes from the contract (Note: if count is too large, it could be better to obtain the hashes in batches):
- Call
subscriptionManager.subscriptionCount()
. - Call
subscriptionManager.subscriptionRange(0, count)
.
To execute a batch of subscriptions:
- Call
subscriptionManager.executeBatch(hashes)
. - In the logs, there may be one event present that can be used by the service to update its state:
SubscriptionNotFound(subscriptionHash)
SubscriptionNotReady(subscriptionHash, nextPayment)
SubscriptionExecuted(subscriptionHash, nextPayment)
SubscriptionDeleted(subscriptionHash, reason)
The reason
parameter in the SubscriptionDeleted event can correspond to:
- 0 (CANCELLED): The subscription was cancelled before the execution.
- 1 (EXPIRED): The subscription reached its maxExecutions in the previous execution.
- 2 (INVALID_STATE): The state was invalid during the execution. In the plan based usecase, this can happen if the subscription's plan was deleted.
- 3 (NOT_ENOUGH_FUNDS): The subscriber address doesn't have enough funds to perform the execution.
Key Differences Between Daisy and the Current EIP 1337 Proposal
Subscriptions creation is not standardized
By not defining how the subscription must be created, we can support multiple different use cases (e.g. plan or tier based subscriptions, or subscriptions whose prices are determined by what feautures are turned on/off).
Subscription data is stored in the contract
This makes interoperability with other contracts/services easier, without needing to rely on a centralized data source. Subscription data is removed from the contract if it can't be executed anymore or if the user doesn't have enough funds.
Execution of a payment only requires specifying the subscription hash
This allows for easier batching of executions and makes it easier for off-chain services to execute subscriptions.
Delegated execution and enumeration are OPTIONAL
There are three interfaces used by the base implementation: ISubscriptionManager
, IDelegatedSubscriptionManager
and IEnumerableSubscriptionManager
. In this implementation, all of these interfaces are implemented in the base contract as most of the time they will be used together. However, we propose that the delegated execution and enumerable functionality should be optional (similar to the ideas present in ERC721).
isValidSubscription()
split into isCancelled(subscriptionHash)
and nextPaymentTimestamp(subscriptionHash)
Instead of having an isValidSubscription()
function that returns a boolean, this implementation provides a nextPaymentTimestamp()
function that returns the unix timestamp at which the next payment can be executed. This returned value is more usable than a boolean. On the other hand, with isCancelled
one can get more granular information about a subscription, improving interoperability.
Testing
To run the tests, clone this repo and run yarn install
to install dependencies.
Run yarn test
to run all tests.
Run yarn coverage
to run tests and get code coverage.