@daisypayments/smart-contracts

0.1.1 • Public • Published

Daisy Contracts

CircleCI Coverage Status

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.
  • price: 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.
  • planIdHash: the hash of the id of the plan the subscription belongs to.
  • credits: amount of tokens to substract from future payments. On each payment, these credits get consumed until they get to 0.

A subscription can be in one of four states:

  1. Active: Once it has been executed and the next payment is in the future. A subscription is first executed upon its creation.
  2. Active and Cancelled: When it is cancelled before reaching the next payment timestamp. It will remain active until the end of the period.
  3. Cancelled: when it has been cancelled and the last billed period has ended.
  4. Deleted: the subscription data will be deleted upon execution if the subscription can't be executed anymore.

Plans

A plan is just a way to group multiple subscriptions, and allows the publisher to cancel a group of subscriptions at once. A plan is identified in the contract by a bytes32. When a plan based subscription is created, it is necessary to specify the plan's identifier, and the creation only goes through if a valid authorization signature is provided.

The contract provides a function to delete a plan by providing the plan on chain id. All subscriptions with that plan get invalidated (they get deleted on the next execution).

Contracts

SubscriptionManager.sol

Handles the creation, execution, and cancellation of subscriptions.

It also provides methods for enumerating subscriptions and subscription by subscriber. This implementation was adapted from OpenZeppelin's EIP721Enumerable implementation.

IndexedArrayLib.sol

Library that provides an indexed array of bytes32. It allows us to remove items from the array by specifying the item itself that we want to remove.

Delegated.sol

Allows child contracts to support delegated execution without changing their external APIs. See Delegation section.

Time

To support different time units (days, months and years) we use BokkyPooBah's Date Time Library.

Delegation

In order to support delegated execution through the use of EIP712 signatures, the SubscriptionManager inherits from Delegated. The key feature of the Delegated contract is that child contracts don't need to polute their APIs with parameters related to delegated execution.

The Delegated contract exposes the external delegate function which receives the following parameters:

  • bytes data: ABI encoded data to be used for the delegated call.
  • bytes32 nonce: Random value for replay protection. If the function to call is idempotent, this value is optional.
  • uint256 signatureExpiresAt: Timestamp in which the signature stops being valid.
  • bytes signature: The signature for the EIP712 type.

For example, for the setWallet(address _wallet) function in the SubscriptionManager, the EIP712 type is SetWallet(address wallet,bytes32 nonce,uint256 signatureExpiresAt). This function can only be called directly by the owner account or by using the delegate function with the following parameters (pseudo code):

// Data to call the setWallet function normally
const data = abiEncode(setWalletSelector, _wallet);

// Random value for replay protection
const nonce = randomBytes32();

// Signature expiration
const signatureExpiresAt = now + signatureDuration;

// Owner's signature
const signature = signTypedData({
  account: owner,  
  primaryType: "SetWallet", // The EIP712 type
  domain: { 
    verifyingContract: manager.address
  },
  message: {
    wallet: _wallet,
    nonce,
    signatureExpiresAt,
  }
});

// Call setWallet through the delegate function
await manager.delegate(
  data,
  nonce,
  signatureExpiresAt,
  signature,
  {
    from: anyone
  }
);

delegate function and signature checks

The delegate function performs the following steps:

  1. Reentrancy guard (delegate can only be called once in the same transaction).
  2. Check that the signature hasn't expired (now < signatureExpiresAt).
  3. Store the nonce, signatureExpiresAt and signature parameters.
  4. Call itself with the ABI encoded data (address(this).call(data));
  5. Revert if the call failed.
  6. Delete the stored parameters.

As the delegation data exists in the storage while the call is being executed, the called function in the child contract can use the internal _delegatedCheck(bytes32 typeHash, bytes data, bool usesNonce) function to get the address of the caller. Internally, the _delegatedCheck checks if msg.sender == this and performs the signature validation using the provided type hash. If msg.sender != this, it just returns the msg.sender. It also stores the hashed data so it can't be used again for another delegated call.

Subscription Flow with Delegated Execution

The only function where delegated execution checks are done manually (doesn't use the Delegated functionality) is the create function, used to create subscriptions. The flow for creating a subscription works as follows:

  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, the subscriber signs the creation of a subscription (using the EIP712 type CreateSubscription) and sends the data to the authorizer server.
  3. The authorizer checks that the parameters are OK depending on its own rules, signs the data and sends the data and signatures to the relayer.
  4. The relayer executes the creation by calling the method create(), passing the parameters and signatures.
  5. At the beggining of each payment period, anyone can execute the subscription and bill the subscriber.

For subscription services created using Daisy the platform, Daisy will be the authorizer AND the relayer in this flow. We are also working on a system that will allow clients to be the authorizers by using webhooks (Daisy will request an authorizer signature for each subscription creation request).

Off-chain service flow

Checking if the publisher needs to provide the service

  1. Receive request from subscriber.
  2. Call subscriptionManager.nextPaymentTimestamp(subscriptionId).
  3. If now < nextPaymentTimestamp, answer the request.

Executing subscriptions

To retrieve all subscription ids from the contract (Note: if count is too large, it is possible to obtain the ids in batches):

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

To execute a batch of subscriptions:

  1. Call subscriptionManager.executeBatch(ids).
  2. The following events can be present in the transaction receipt, which can be used by the service to update its state:
    • SubscriptionNotFound(subscriptionId): Subscription doesn't exist in the contract.
    • SubscriptionNotReady(subscriptionId, nextPayment): Next payment timestamp hasn't been reached yet.
    • SubscriptionExecuted(subscriptionId, nextPayment): The subscription was executed successfuly.
    • SubscriptionDeleted(subscriptionId, reason): The subscription was deleted from the contract.

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. This happens if the subscription's plan was deleted.
  • 3 (NOT_ENOUGH_FUNDS): The subscriber address doesn't have or didn't approve enough funds to perform the execution.

If the SubscriptionExecuted event is present, the following two events can also be present, which represent token allocations (see next section):

  • PaymentAllocated(subscriptionId, token, recipient, amount)
  • FeeAllocated(subscriptionId, token, recipient, amount)

Token Allocations and Fees

The owner of a SubscriptionManager can optionally set a fee and a feeRecipient (which can be changed at any point in time). On each execution, the amount of tokens to be paid to the feeRecipient is calculated as feeAmount = price * fee / MAX_FEE, and the tokens to be paid to the publisher's wallet becomes price - feeAmount. MAX_FEE is a constant set to 100 * 10^18, and fee <= MAX_FEE always.

The SubscriptionManager uses a pull payment model: on each execution, tokens are always transferred from the subscriber to the manager itself, and the manager allocates tokens to the current feeRecipient and wallet by storing their payments in the availableFunds mapping. When an allocation happens, the FeeAllocated event or the PaymentAllocated event will be emitted, specifying the subscriptionId, token, recipient and amount.

At any time, anyone can call the withdraw(address[] tokens, address[] recipients) function to transfer all the available tokens to each recipient specified in the parameters.

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 id

This allows for easier batching of executions and makes it easier for off-chain services to execute subscriptions.

Delegated execution is not part of the interface

In order to be compatible with other proposals for standardizing delegated execution, the core functions don't need to support delegated execution.

isValidSubscription() split into isCancelled(subscriptionId) and nextPaymentTimestamp(subscriptionId)

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 @daisypayments/smart-contracts

Weekly Downloads

26

Version

0.1.1

License

MIT

Unpacked Size

2.6 MB

Total Files

9

Last publish

Collaborators

  • jmoreira
  • lopezjurip
  • nchastain
  • sloreti
  • vdrg
  • wachunei