✨ New!✨  C4 Cosmos leagueRead more »

Contest ran 30 November 20217 December 2021

7 day contest

Streaming Protocol contest

$100,000 USDC

Total Awards

Streaming Protocol contest details

This repo consists of 3 main contracts:

Contract Name Description SLOC
StreamFactory A factory for creating Streams ~600
Stream A contract for managing 2 counterparty token lockup rental & buying ~80
LockeERC20 Inherited by Stream, this turns Stream deposits into a transferable ERC20 ~200

External Calls

StreamFactory : The stream factory cannot make external calls

Stream: Each stream interacts with arbitrary contracts in everyday operation.

Notably:

  1. There is flashloan capability. Therefore, a user can call an arbitrary contract with a stream as msg.sender. There are strong checks around important balances here.
  2. The governor can make arbitrary calls to any address except rewardToken and depositToken, as well as any token that is incentivizing the streamer

LockeERC20: relatively standard ERC20 (only difference is a timelock on when transfers start)

Libraries

Solmate from Rari Capital is used extensively as well as DSTest from dapphub for tests.

Testing

Download & use dapp. dapp test to run all tests.

Uniqueness

We take a lot of inspiration for Synthetix's Staker contract, but add virtual balances to handle the fact that real balances are streamed over to the other counterparty.

ERC20 conformity

We maintain erc20 conformity with the caveat that transfers are not possible before the end of a streaming period.

Where to focus/areas of concern

  1. Arbitrary calls making it not trustless for the 2 counterparites
  2. Flashloans
  3. Math around virtual balances + time restrictions on capital flows
  4. RecoverTokens functionality - attempted to enable creator to withdraw accidentally sent depositTokens and rewardTokens, which causes complexity

Contract Overview

StreamFactory

This contract is a factory that generates Streams. It has 6 governable parameters. The only limitation should be on feePercent which should not exceed 5%.

The main entry is for DAOs to call createStream, passing in a token they are going to be rewarding depositors (the rewardToken), the deposit token, the start time of the stream, how long the stream lasts, how long deposits are locked for after the end of the stream, and how long rewards are locked after the end of the stream.

Calling the function should deploy a new contract with a unique id and pass in the factory's fee parameters + the creator's parameters.

Updating of stream limits + fee limits are only accessible to the governor of the contract. This governorship is updateable in a 2 step process.

Stream

Lifecycle

A stream is created by a stream creator. This initializes a bunch of immutable values, mostly to do with the lifetime of the contract. The lifetime can be broken down as follows:

  1. Creation
  2. Funding + Staking period
  3. Stream + Staking period
  4. Deposit Locked period
  5. Claiming period

The lifetimes are defined by the following variables

Creation: self explanatory

Funding + Staking: creation thru startTime

Streaming + Staking: startTime thru startTime + streamDuration (endStream)

Deposit Locked: endStream thru endDepositLock

Claiming: for rewards endRewardLock, for deposits endDepositLock thru infinity (technically to block.timestamp = 2**32)

Creation

Creation is just the construction of the contract. It sets a bunch of immutables mostly to do with lifecylce timing, fees, and tokens. Of note, there is a isSale, which denotes that the depositors are selling their deposit tokens for the rewardToken.

Funding + Staking Period

Once a contract is created, it is ready to be funded by anyone, via the fundStream function. This function should support any ERC20 as the rewardToken besides rebasing tokens which is explicitly out of scope. Fee on transfer tokens and no-return ERC20s are in scope. Additionally, ERC20s whose balance may go above type(uint112).max are unsupported, similar to uniswap V2.

Additionally, if the StreamFactory had fees enabled, the governor of the factory contract should be able to collect the fees which should be a percentage of the rewardTokens used to fund the stream.

During this time, user can also deposit their depositTokens, via the stake function. The stream hasn't started yet so no rewards should stream to them yet though. Additionally, the user should be able to withdraw their entire balance via the exit function prior to the stream starting, or specify an amount of depositTokens to withdraw via the withdraw function.

Stream + Staking period

Once a stream has started, depositTokens become linearly locked until endDepositLock (unless it is a sale, in which case they are claimable by the stream creator).

For example, if you deposit 100 USDC before startTime, as soon as the clock passes startTime, tokens should become locked linearly over the streamDuration. So if the stream duration is 100 seconds, each second, 1 USDC would be locked until endDepositLock.

What the depositor gets in return is a continuous stream of rewardTokens. The amount of rewardTokens should be equivalent to each depositors % of the unstreamed amounts becoming locked. So if you represent 20% of the tokens being streamed, you should earn 20% of the reward tokens while your tokens represent that 20%. If the % changes, your reward should change as well, while maintaining the previous rewards. This is basically like other Synthetix Rewards contract, with the added caveat that your actual balance decreases over time as tokens become locked. To work around this, we use virtualBalances, which takes into account how much of the remaining rewardTokens can you fight to earn. This is detailed in the dilutedBalance function.

Withdrawing during this timeperiod has some caveats as well, as some of your tokens will have already been locked. So we must keep track of the remaining unlocked tokens that are still available to withdraw (TokenStream.tokens).

There is a helper exit function that just withdraws your remaining balance.

Additionally, there is an ERC20 minted (if its not a sale), that represents your deposit into the protocol. This becomes transferable afterward and will be needed to reclaim your deposit tokens after endDepositLock.

Deposit Locked period

At this point, all deposit tokens should be locked and the receipt tokens minted should be transferable. Once the streamDuration has elapsed, if isSale, the creator can withdraw the deposit tokens & the governor can withdraw any fees. Otherwise, the contract lays dormant. At any point in any of the time periods, flashloaning (of depositToken or rewardToken only) and arbitrary calls (to contracts != depositToken || rewardToken) can be performed by anyone and governance respectively.

Claiming period

After endDepositLock, deposits can be reclaimed by burning the receipt tokens received from depositing. After endRewardLock, any earned rewards should be claimable via claimReward function.