@tokenfoundry/subscription-contracts

4.2.1 • Public • Published

Daisy Contracts

CircleCI codecov

(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:

  1. Created: after it is created but has yet to be executed.
  2. Active: Once it has been executed and the next payment is in the future.
  3. Active and Cancelled: When it is cancelled before reaching the next payment timestamp. It will remain active until the end of the period.
  4. Cancelled: when it has been cancelled and the last billed period has ended.
  5. 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:

  1. Active: the plan can accept new subscriptions.
  2. Inactive: the plan doesn't accept new subscriptions, but existing subscriptions can be executed normally.
  3. 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)

  1. Subscriber calls the approve function in the token in which the subscription is denominated in to allow the manager contract to manipulate her funds.
  2. Using EIP 712, subscriber signs the creation of a subscription.
  3. A relayer executes the creation by calling the method create() and passing the parameters and signature.
  4. 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

  1. Receive request from subscriber.
  2. Call subscriptionManager.nextPaymentTimestamp(subscriptionHash).
  3. 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):

  1. Call subscriptionManager.subscriptionCount().
  2. Call subscriptionManager.subscriptionRange(0, count).

To execute a batch of subscriptions:

  1. Call subscriptionManager.executeBatch(hashes).
  2. 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.

Readme

Keywords

none

Package Sidebar

Install

npm i @tokenfoundry/subscription-contracts

Weekly Downloads

30

Version

4.2.1

License

MIT

Unpacked Size

2.98 MB

Total Files

10

Last publish

Collaborators

  • lopezjurip
  • vdrg
  • wachunei