Compounding Streams

Covering how to create, withdraw from and cancel compounding streams.‌

Introduction

Compounding streams are very similar to normal streams, but they have an additional feature; they allow the sender to keep a portion or all of the interest to herself. To understand how this works, let's go back to the stream between Alice and Bob over the month of January, but let us use cDAI instead of DAI now.

If the following happen on Jan 1:

  • The value of the initial lock-up in cDAI is equivalent to 3,000 DAI

  • Alice opts-in to keep 80% of the interest to herself, giving the remaining 20% to Bob.

  • The interest for January yielded by holding cDAI will be 10%

  • Alice does not cancel the stream

The following will happen on Feb 1:

  • The 3,000 DAI will be credited in full to Bob

  • 300 DAI will be generated in interest

  • 240 DAI (80%) will be credited to Alice

  • 60 DAI (20%) will be credited to Bob

Therefore, Alice will earn her 80% share of the interest, while Bob will get his salary streamed and also receive 20% of the interest, as a perk. He will have his cake and eat it too.

Compounding streams can be created with cTokens exclusively. In a future iteration of the protocol, we may enable other interest-bearing assets, but we designed v1 with simplicity in mind.

Exponential Math

The Sablier smart contract relies on a system of exponential math in order to represent fractional quantities with sufficient precision. You can read more about this in the Compound protocol documentation, but here's an excerpt for you:

Mantissas are unsigned integers scaled up by a factor of 1e18 from their nominal value. By using mantissas within our contracts, we may perform basic mathematical operations like multiplication and division at a higher resolution than working with the unscaled quantities directly as integers. To gain a better understanding of how this works, see Exponential.sol.

Compound Token Whitelist

For security purposes, we restricted what cTokens compounding streams can be created with. As of today, these are:

As the Sablier protocol evolves, we may add more cTokens to this whitelist. If you want to suggest the addition of a specific cToken, please ping us on Discord.

Non-Constant Functions

Create Compounding Stream

The create compounding stream function transfers the cTokens into the Sablier smart contract, stamping the rules of the compounding stream into blockchain storage. As soon as the clock hits the start time of the compounding stream, a little bit of money starts getting "transferred" from the sender to the recipient once every second.‌

function createCompoundingStream(address recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256 stopTime, uint256 senderSharePercentage, uint256 recipientSharePercentage) returns (uint256)
  • msg.sender : The address which shall fund the compounding stream, and pay the recipient in real-time.

  • recipient : The address towards which the money shall be streamed.

  • deposit : The amount of money to be streamed, in units of the streaming currency.

  • tokenAddress : The address of the cToken to use as streaming currency.

  • startTime : The unix timestamp for when the compounding stream starts, in seconds.

  • stopTime : The unix timestamp for when the compounding stream stops, in seconds.

  • senderSharePercentage : The sender's share of the interest, as a percentage.

  • recipientSharePercentage : The recipient's share of the interest, as a percentage.

  • RETURN : The compounding stream's id as an unsigned integer on success, reverts on error.

All the constraints described in the createStream section apply here too, since "createCompoundingStream" calls "createStream" under the hood.

Solidity

Sablier sablier = Sablier(0xabcd...); // get a handle for the Sablier contract
address recipient = 0xcdef...;
uint256 deposit = 2999999999999998944000; // almost 3,000, but not quite
uint256 startTime = block.timestamp + 3600; // 1 hour from now
uint256 stopTime = block.timestamp + 2592000 + 3600; // 30 days and 1 hour from now
uint256 senderSharePercentage = 80;
uint256 recipientSharePercentage = 20;
CErc20 cToken = CErc20(0xcafe...); // get a handle for the cToken contract
cToken.approve(address(sablier), deposit); // approve the transfer
// the compounding stream id is needed later to withdraw from or cancel the stream
uint256 compoundingStreamId = sablier.createCompoundingStream(recipient, deposit, address(cToken), startTime, stopTime, senderSharePercentage, recipientSharePercentage);

Ethers.js

const sablier = new ethers.Contract(0xabcd..., sablierABI, signer); // get a handle for the Sablier contract
const recipient = 0xcdef...;
const deposit = "2999999999999998944000"; // almost 3,000, but not quite
const now = Math.round(new Date().getTime() / 1000); // get seconds since unix epoch
const startTime = now + 3600; // 1 hour from now
const stopTime = now + 2592000 + 3600; // 30 days and 1 hour from now
const senderSharePercentage = 80;
const recipientSharePercentage = 20;
const cToken = new ethers.Contract(0xcafe..., cErc20ABI, signer); // get a handle for the token contract
const approveTx = await cToken.approve(sablier.address, deposit); // approve the transfer
await approveTx.wait();
const createCompoundingStreamTx = await sablier.createCompoundingStream(recipient, deposit, cToken.address, startTime, stopTime, senderSharePercentage, recipientSharePercentage);
await createCompoundingStreamTx.wait();

Withdraw from Compounding Stream

The API for withdrawing from compounding streams is the same as with normal streams. Use the compounding stream's id when calling the withdrawFromStream function.

Interest Distribution

When money is withdrawn from a compounding stream, the smart contract verifies if:

  1. The recipient share percentage is bigger than 0

  2. The interest accrued by keeping by the withdrawn amount in the contract is bigger than 0

If both conditions are true, the recipient will be also paid some interest.

Alice and Bob

Let's imagine Bob withdraws everything on Jan 15 at midday. The base value will be approximately 1,500 DAI, as it's the middle of the month, but this is not all the money that Bob is paid.

We assumed that the interest rate at the end of the month will be 10%, which means that for half a month it will be around 5%. Now, the smart contract will perform these final steps:

  1. Calculate the interest generated by holding the withdrawal amount; 5% of 1,500 DAI is 75 DAI

  2. Calculate Alice's share of 75 DAI; 80% of 75 DAI is 60 DAI

  3. Calculate Bob's share of 75 DAI; 20% of 75 DAI is 15 DAI

  4. Transfer the 60 DAI to Alice's address

  5. Add the 15 DAI interest to Bob's 1,500 DAI withdrawal amount and transfer 1,515 DAI to his address

Cancel Compounding Stream

The API for cancelling compounding streams is the same as with normal streams. Use the compounding stream's id when calling the cancelStream function.

Interest Distribution

When a compounding stream is cancelled, the smart contract checks if the streamed money generated any interest. If yes, it distributes the interest on a pro-rata basis by using the share percentages chosen at stream creation time.

Alice and Bob

Let's imagine Alice cancels the stream on Jan 10 at midday. The initial deposit of 3,000 DAI will be split on a pro-rata basis to both Alice and Bob; that is, 1,000 DAI is Bob's streamed money, while the remaining 2,000 DAI is returned to Alice.

We assumed that the interest rate at the end of the month will be 10%, which means that for a third of the month it will be around 3%. Now, the smart contract will perform these final steps:

  1. Calculate the interest generated by holding the initial deposit; 3% of 3,000 DAI is 90 DAI

  2. Calculate Alice's share of 90 DAI; 80% of 90 DAI is 72 DAI

  3. Calculate Bob's share of 90 DAI; 20% of 90 DAI is 18 DAI

  4. Add the 72 DAI interest to Alice's allocation of 2,000 DAI and transfer 2,072 DAI to her address

  5. Add the 18 DAI interest to Alice's allocation of 1,000 DAI and transfer 1,018 DAI to her address

Constant Functions

Get Compounding Stream

The get compounding stream function returns all properties for the provided compounding stream id.

function getCompoundingStream(uint256 streamId) view returns (address sender, address recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256stopTime,uint256remainingBalance,uint256 ratePerSecond, uint256 exchangeRateInitial, uint256 senderSharePercentage, uint256 recipientSharePercentage)
  • streamId : The id of the compounding stream to query.

  • RETURN

    • sender : The address that created and funded the compounding stream.

    • recipient : The address towards which the money is streamed.

    • tokenAddress : The address of the ERC-20 token used as streaming currency.

    • startTime : The unix timestamp for when the compounding stream starts, in seconds.

    • stopTime : The unix timestamp for when the compounding stream stops, in seconds.

    • remainingBalance : How much money is still allocated to this compounding stream, in the smart contract.

    • ratePerSecond : How much money is allocated from the sender to the recipient every second.

Solidity

‌Sablier sablier = Sablier(0xabcd...);
uint256 compoundingStreamId = 42;
(uint256 sender, uint256 recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256 stopTime, uint256 remainingBalance, uint256 ratePerSecond, uint256 senderSharePercentage, uint256 recipientSharePercentage) = sablier.getCompoundingStream(compoundingStreamId);

Ethers.js

const sablier = new ethers.Contract(0xabcd..., sablierABI, signer);
const compoundingStreamId = 42;
const compoundingStream = await sablier.getCompoundingStream(compoundingStreamId);

Is Compounding Stream

The is compounding stream function checks if the provided id points to a a valid compounding stream.

function isCompoundingStream(uint256 streamId) view returns (bool)
  • streamId : The id of the compounding stream to check.

  • RETURN : True if the id points to a valid compounding stream, otherwise false.

Solidity

Sablier sablier = Sablier(0xabcd...);
uint256 compoundingStreamId = 42;
bool isCompoundingStream = sablier.isCompoundingStream(compoundingStreamId).

Ethers.js

const sablier = new ethers.Contract(0xabcd..., sablierABI, signer);
const compoundingStreamId = 42;
const isCompoundingStream = await sablier.isCompoundingStream(compoundingStreamId);

Interest Of

The interest of function calculates the interest accrued while the money has been streamed. The functions returns (0, 0, 0) if the stream is not a compounding stream or it does not exist.

function interestOf(uint256 streamId, uint256 amount) returns (uint256 senderInterest, uint256 recipientInterest, uint256 sablierInterest)
  • streamId : The id of the compounding stream for which to query the interest.

  • amount : The amount of money with respect to which to calculate the interest.

  • RETURN : The interest accrued by the sender, the recipient and the Sablier protocol, respectively, as unsigned integers.

The interest accrued by the Sablier protocol depends on the storage property fee. This is currently set to 0 and we have no plans to update it in the near future.

Solidity

Sablier sablier = Sablier(0xabcd...);
uint256 compoundingStreamId = 42;
(uint256 senderInterest, uint256 recipientInterest, uint256 sablierInterest) = sablier.interestOf(compoundingStreamId);

Ethers.js

const sablier = new ethers.Contract(0xabcd..., sablierABI, signer);
const compoundingStreamId = 42;
const interest = await sablier.interestOf(compoundingStreamId);

Balance Of

The API for querying the balance is the same as with normal streams. Use the compounding stream's id when calling the balanceOf function.

The value returned by the balance of function may not be the same value that gets transferred on withdrawal or cancellation, because this function does not take into account the interest distribution - senderSharePercentage and recipientSharePercentage. It implicitly assumes that the recipient share percentage is 100.

Delta Of

The API for querying the time delta is the same as with normal streams. Use the compounding stream's id when calling the deltaOf function.

Error Table

The table below lists all possible reasons for reverting a contract call that creates, withdraws from or cancels a compounding stream. The "Id" column is merely a counter, because the smart contract does not yield error codes, just strings.

Id

Error

Reason

1

compounding stream does not exist

The provided stream id does not point to a valid compounding stream.

2

sender token transfer failure

Malicious token

3

recipient token transfer failure

Malicious token

2

cToken is not whitelisted

For security purposes, we restricted what cTokens compounding streams can be created with.

3

share sum calculation error

The sum of the sender share percentage and the recipient share percentage cannot overflow.

4

shares do not sum up to 100

The sum of the sender share percentage and the recipient share percentage must be 100.

5

interest calculation error

Happens when providing an absurdly high value as the 2nd argument to the interestOf function.

7

sablier interest calculation error

Same as #5 and when the Sablier is fee is bigger than 0 and smaller than 100.

8

sender interest calculation error

Same as #5.

9

sender interest conversion error

Happens when the cToken exchangeRate is absurdly high.

10

recipient interest conversion error

Same as #9.

11

sablier interest conversion error

Same as #9.

The contract call could revert with no reason provided. In this case, you probably did not approve the Sablier contract to spend your token balance, although this is not strictly true. Ping us on Discord if you get stuck.