Git Product home page Git Product logo

spool-foundry's Introduction

Super Pool

Demo app on Goerli live at https://super-pool-6991a.web.app

Description

The super-pool contract allows users to deposit superTokens (in one-shot or streaming) into the pool. For every token a user stakes into the pool, they will receive a “Super Pool” (spToken) token (ERC20 token interest-bearing token).

The super pool will push tokens to a Defi strategy, the super pool accepts n-strategies per superToken creating a pool for every strategy.

The earned yield will be allocated to the users according to “dollar-seconds” accounting rules.

Users will see their balance of “spToken” increasing over time.

Users can redeem at any moment their “spTokens” and get them converted to superTokens

In the same way, a user can redeem a flow of “spTokens” in this case, the user will receive a flow of supeTokens .Although the flow is constant, the Defi withdrawal will follow a “down-stairs” pattern to ensure the pool does not run out of funds while maintaining the maximum deposit into the Defi protocol


Contract Structure

Contracts can be found at: contracts

The major proxy contracts are verified and can be found at

User Interaction PoolContract

Internal PoolInternal

Yield Strategy (aave) PoolStrategy

PoolFactory (create a pool passing superToken and strategy) PoolFactory

Super Pool Factory

The contract is responsible for creating the pools, it creates proxies of the pool and pool internal implementation and initializes them.

It creates a pool per superToken and pool strategy (aave, compound….)

Pool

Main user interaction (send tokens (IERC77 receive), stream, withdraw, out stream) implements is an ERC20 and SuperApp implementing callbacks.

The pool contract is like an API that interact with the user and redirect the calls to the backend (the pool Internal contract)

PoolInternal

Responsible for holding the state of the pool, updating it, and launching the withdrawal, streams etc…

PoolStrategy

The strategy to use with this pool, by deploying the pool will approve the pool strategy contract to move tokens and superTokens around.

The pool Strategy contract must implement a very simple interface with two functions:

  • The current balance of the strategy: balanceOf()
  • Withdraw from the strategy: withdraw()

The strategy decides when to push tokens and is responsible for accruing yield.


Data Objects

Pool

For every pool interaction, a new pool object will be created with the relevant fields and store by timestamp

struct PoolV1 {
        uint256 id; 
        uint256 timestamp;
        uint256 nrSuppliers; //Supplies already interacting with the pool

        uint256 deposit; // Current Deposit
        uint256 depositFromInFlowRate; // required to track two indexes

        int96 inFlowRate; // stream in flow
        int96 outFlowRate; //supplier receiving flow
        uint256 outFlowBuffer; 
				// minimal balance in the pool for covering out streams

        uint256 yieldTokenIndex; // Indexes ot calculate user accrued yield
        uint256 yieldInFlowRateIndex; // Indexes ot calculate user accrued yield
			
        uint256 yieldAccrued; // Yield accrued since the last updated
        uint256 yieldSnapshot; // Total balance in the pool strategy
        uint256 totalYield; // total yield accrued by the pool

        APY apy; // APY so far
    }

Supplier

Every time a supplier interact with the pool the following object will be updated

struct Supplier {
        uint256 id;
        address supplier;
        uint256 cumulatedYield;
        uint256 deposit;
        uint256 timestamp; 
        uint256 createdTimestamp;
        uint256 eventId;
        Stream inStream;
        OutStream outStream;
        APY apy;
    }

struct Stream {
        int96 flow;
        bytes32 cancelFlowId; //deprecated
    }

 struct OutStream {
        int96 flow;
        bytes32 cancelFlowId;//deprecated
        uint256 stepAmount; //amount to transfer every step
        uint256 streamDuration;// time between steps
        uint256 initTime;// when step started
        uint256 minBalance; // min balance to ensure pool not run out of funds
        bytes32 cancelWithdrawId; //withdraw task id by Gelato
    }

Rough short view roadmap

Last achieved:

  • 2000 tests (60 events, 4 users, 42 unit tests per event)

  • Gelato automation implemented
  • Uups implementation
  • Refactored from 7 contracts to 4

Next:

  • last bugs (automatic losing redeem flows)

Super Pool Factory initialization parameters

Network

  • Superfluid Host
  • Supertoken
  • Token
  • Gelato Ops

Redeem Flow Buffer

  • MIN_OUTFLOW_ALLOWED, buffer time on top of the 4h superfluid deposit. Bear in mind that every block a gelato task will rebalance if needed. Demo value 3600 seconds
  • PARTIAL_DEPOSIT, percentual steps of Defi withdrawal, 1 equals to withdraw from the Defi protocol the whole user balance, no risk of liquidation, but no additional yield earned. 50 equals to withdraw 2%, much more transactions but additional yield earned. Demo value = 10 (timed withdrawals could be configured)

Pool Accounting

We allocate the yield depending on the dollar-seconds to be able to have a common scale between deposits and streams.

In every pool update, we will calculate the area associated with the stream or the deposit in dollars/seconds and then simply proportionally split the yield.

We will keep track of two indexes.

  • The yield earned by token: yieldTokenIndex.
  • The yield earned by the incoming flow-rate: yieldInFlowRateIndex.

The calculation of the yield accrued is pretty straightforward, deposit and stream times the corresponding index (we will use yieldTokenIndex and the yieldInFlowRateIndex)

In doing so we can linearize the calculation of the yield earned by each supplier

uint256 yieldShare = 
//// deposit part
 ( yieldTokenIndex(block.timestamp) - yieldTokenIndex(depositTimeStamp)) * deposit
 
//// streaming part Inflow
+  (  yieldInFlowRateIndex(block.timestamp)
   - yieldInFlowRateIndex(startStreamTimeStamp)) * inFlowRate

Our target now is to set a simple, clean and consistent mechanism for maintaining these two indexes. Let’s see an example of how we could do that.

Pool Events

We define “PoolEvent” as any event which changes the “Current Pool State” being the flow as the deposit or the Accrued Yield either in-stream or deposit.

Type of PoolEvents:

User Interaction (see code)

  • Deposit Supertoken (ERC777 send)

  • Stream-In Start Supertoken

  • Stream-in Update SuperToken

  • Stream-In Stop SuperToken

  • Redeem sTokens (shares) to SuperToken

  • Redeem Flow of sTokens (shares) to SuperToken

  • Redeem Flow Stop

Pool Interaction

  • Accrue Yield (Pool Borrow)

Simple easy showcase of two-period calculation

if we are at period(I) and we have the following values stored at the beginning of this period

period(0) {
					deposit:20
					flowRate:5
					depositFromInFlowRate:0
					yieldTokenIndex: 0
					yieldInFlowRateIndex: 0		
					timestamp:0
					}

alt text

t0 init :

- start stream 5 tokens/s

  • deposit 20 tokens

  • yield 1 token/second

t1 increase yield revenue to 2token/s:

First we have to update both indexes, to do that we are going to calculate the total dollar second and the allocation to the stream portion and to the deposit portion:

Total Dollar Second Deposit = 20 tokens * 10 seconds = 200 tokens second

Total Dollar Second Stream = 5 Token/sec * 10 * 10 /2 = 250 tokens second

yieldTokenIndex = previousYieldTokenIndex + ( (1 token/sec * 10 sec) * 200 /450)/ (total tokens) = 0 + (4.44/20) = 0.22

yieldFlowRateIndex = previousYieldFlowRateIndex + ((1 token/sec * 10 sec) * 250 /450) (total flow rate units) = 0 + (5.55/5) = 1,11

The period-end object:

period(1) {
					deposit:20
					flowRate:5
					depositFromFlowRate:50
					yieldTokenIndex: 0.22
					yieldFlowRateIndex: 1.11	
					timestamp:10
					}

In t1 the accrued yield increase to 2 tokens/2 and in t2 we re-do the calculation

First we have to update both indexes, to do that we are going to calculate the total dollar second and the allocation to the stream portion and to the deposit portion:

Total Dollar Second Deposit = 20 tokens * 10 seconds = 200 tokens second

Total Dollar Second Stream = 5 Token/sec * 10 * 10 /2 = 250 tokens second + depositFromflowRate * 10 = 750 tokens second

yieldTokenIndex =0.22 + ( (2 token/sec * 10 sec) * 200 /950)/ (total tokens) = 0.22 + 0.21 = 0.43;

yieldFlowRateIndex = 1.11 + ((2 token/sec * 10 sec) * 750 /950) (total flow rate units) = 1.11 + (15-79/5) = 1.11 + 3.16 = 4.27;

Then the yield earned by a user can be linearized if we store following data

supplier = {
	flowRate:5;
	deposit:20;
	timestamp:0
	cumulatedYield:0
}

Yield Calculated at t2

yield = (yieldTokenIndex(t2)-yieldTokenIndex(to))*deposit 
+ (yieldFlowRateIndex(t2)-yieldFlowRateIndex(to)*flowrate
+ cumulatedYield;

balance = deposit + flowRate*(t2-to) + yield 

Notice that the deposit and the flowrate have different timestamps as a user can do a deposit and a later point of time start a stream

💡 I think this way of accounting solves the scalability issue and is consistent between yield earnings allocated to streams or deposits

The “magic” of this solution is that we merge deposits and streams within a period where we can very easily calculate the dollar-second value of each one, we split then the yield and then we track independently the two indexes.

Repo Use Case Tests

This use cases are not yet adapted to the final version

alt text

Example of the 14th period tests.

alt text


spool-foundry's People

Contributors

donoso-eth avatar saflamini avatar

Stargazers

Philip Andersson avatar Phidel Musungu avatar  avatar

Watchers

 avatar  avatar  avatar

spool-foundry's Issues

Questions & Suggestions For Super Pool V1

Overview

First off, here’s my general understanding of how this works. Please correct me if I am wrong about this @donoso-eth

You can send funds to a given super pool contract by doing the following

  1. Acquire super tokens by wrapping an existing ERC20 (if you already have super tokens in your wallet, great)
  2. Calling send() on the super token you’re working with to send these tokens into the pool you’re interacting with or creating a stream of tokens to the pool you’re interacting with
    **Correct 👍 **

Once you’ve sent super tokens into a pool contract, you’ll receive spTokens in return
Correct 👍

  • For the entire time that a user has their tokens sitting inside of a super pool strategy, the user will see their balance of spTokens increase.
    Correct 👍

  • These tokens are delivering yield to users according to a pre-defined strategy. The Super Pool is designed such that the strategies are configurable. For example, devs using this template could create strategy contracts which plug in to existing defi protocol that enables users to deposit tokens & earn yield
    Correct 👍

Users can redeem their underlying tokens using either a stream or lump sum withdrawal. However, there are some limits on which of these you can use depending on the circumstance:

  • A user may lump sum deposit in and lump sum withdraw (aka - withdraw their underlying super tokens and burn spTokens)
    Correct if the underlying refers to the supertoken as underlying from spTokens!!

  • A user can stream in and lump sum redeem (aka - withdraw their underlying super tokens and burn spTokens)
    Correct if the underlying refers to the supertoken as underlying from spTokens!!

  • A user can lump sum in and stream redeem (aka - withdraw their underlying super tokens and burn spTokens)
    Correct if the underlying refers to the supertoken as underlying from spTokens!!

  • A user cannot stream in and stream redeem (aka withdraw in a stream)
    Correct 👍

Questions

General

  • What is the roadmap here? Are there any new functions/features that need to be finished before we are able to say that this is ready to be used on testnet?

    • if so, what are they?
      ** 👉 Emergency pause**
      ** 👉 Close Account **
      already implemented

    • Note: our goal is to turn this project into the ultimate Template for Sophisticated RealTime Finance applications

  • I see several commented out pieces of code throughout. can we delete these to avoid confusion?
    👍 cleaned

  • Can we use the new SuperTokenV1Library instead of CFALibrary? Will save a lot of boilerplate and be much more readable. I can help with this and you can find the library here
    👍 will check

  • Is PoolV1 meant to inherit from PoolInternal? Or is PoolInternal its own separately deployed contract?
    👉 yes both are separated contracts pool-internal works as a library, there are separated due to bytes size. We use delegateCall() from PooV1 to communicate with PoolInternal to preserve the calling context

  • inside of PoolV1, we inherit from PoolState, but not PoolInternal. PoolInternal then inherits from PoolState?
    👉 exactly, PoolState is abstract contract defining the contract state, in order to ensure that the delegatecalls do not break, we inherit the PoolState so PoolV1 and PoolInternal have the same storage layout

  • Can we get better labeling on the test names vs user-expected 1-n? Because we’re using this json format, it’s really hard to figure out what each test is actually checking
    👉 Sorry for that, you are completely right, in order to reproduce the same scenarios I tested on hadhat I exported from hardhat the results in json and used with foundry, without further info, it is impossible to understand!!

Pool-V1 Contract

  • Can we document the usage of assembly in callInternal() ? I.e. what is actually happening here?
    👉 yes, I will do. As delegateCall() do not revert, only returns success = false, this is a way to get revert message if any in this case

  • What happens if I pass 0 to _redeemFlow? Does it just do what _redeemFlowStop does?
    👉 redeemFlow(0) should revert

    • it looks like it might, but balanceTreasury() is not called in that case?
  • There are two instances of transfer(), one of which seems to be defined with an unlabeled parameter address , and another which calls gelato in its implementation
    👉 corrected, changed the payment to gelato method

    • there’s also an implementation of transfer() commented out, and one version of transfer has 2 lines commented out in its implementation (lines 575/576)
      👉 cleaned
  • Is something supposed to be happening in beforeTokenTransfer and afterTokenTransfer? I don’t see an implementation. And it also seems like the necessary functionality is already happening in PoolInternal’s transferSTokens?
    👉 cleaned

  • When I want to transfer my spTokens, what function will I call?
    👉 normal erc20 transfer

  • Should transferSTokens be an external function? Or should it be internal if it’s only meant to be called later down in the stack by transfer() on the pool contract? Or am I misunderstanding what is happening here?
    👉 You are right, this method overrides the transfer erc20 functionality updating the pool state and can only be called when the erc20 transfer is called

  • TransferSTokens - shouldn’t this be ‘transferSPTokens’ if we’re calling the pool tokens spTokens?
    👉 Much better, changed

  • Lines 116-125 in Pool - what these commented out lines meant to communicate? Are they meant to communicate something about the current contract? (i.e. protocolFee is 3, etc). Or are they meant to be placeholders for future functionality?

    • ah - I see that these are implemented inside of PoolState. can we remove them from poolv1 in this case?
      yes, I will do that, that was a gas optimization I did the updates last days according to issue opened by Miao #1 and wanted to test before deleting them
      👉 cleaned
  • closeAccount() in PoolInternal has no implementation. What is the purpose of that function?
    👉 Already implemented, close streams and withdraw all in one transaction.

  • What are the emergency functions used for?
    👉 the emergency functions should pause the protocol in the case that one incident happen and provide helper functions to stop all streams and rebalance the accounts if required, these functions will be called off chain

  • What is readStorageSlot() used for?
    👉 cleaned, used another approach for testing storage

PoolInternal-V1 Contract

  • On what intervals will we recommend that _balanceTreasuryFromGelato() be called? My understanding is that this is called at a pre-defined interval. We’ll use every state change as an opportunity to balance the entire treasury, regardless of who calls that state-changing operation. However, this function is designed to reduce reliance on regular interaction from users for treasury to be balanced. Is this correct?
    👉 👉 Gelato will check every block whether the balanceTreasury() has to be fired or not, it does this with a resolver contract that checks the following:
     canExec =
            block.timestamp >= lastExecution + BALANCE_TRIGGER_TIME && (pool.outFlowRate > 0 
            || //or
            poolBalance > DEPOSIT_TRIGGER_AMOUNT);

if canExec returns true, then will be executed and this happens in two scenarios

  1. The pool is outstreaming (pool.outFlowRate > 0) and the time elapsed since the last execution is greater than BALANCE_TRIGGER_TIME (24h), then we need to rebalance the pool to ensure remains with enough liquidity for the next 24 hours.
  2. If the pool superToken balance is greater than a certain threshold the balance treasury task will push this amount to our strategy ensuring max capital efficiency.
  • Lines 365 and 366 are commented out, is there a reason for this?
    👉 cleaned

cc @kobuta23

High Level Suggestions

First of all, this is awesome. 👍 Good work @donoso-eth !

Here are several high-level suggestions. They come from my experience building Airlift Finance, a hackathon project (finalist at Unicode 2021). Airlift is based on the Vault / Strategy setup of Beefy.Finance. Reading about SuperPool, it reminded me of this experience. Based on that, here are few things that come to mind:

  1. I think using the term "Pool" is confusing in this context. In the Aave example, Aave is where the "pool" is. And when I see PoolV1 as dev who has worked with Aave contracts, I automatically think of Aave V1. With Beefy and Yearn, and others, the term Vault is used. DefFi users know what a "vault" is. As such I think the term Super Vaults is both more accurate and better in terms of education and marketing for both devs and "suppliers".
  2. I think Strategies should be non-Super!! Meaning, strategies should not know anything about SuperFluid, and thus never upgrade or downgrade tokens. Upgrade/Downgrades should happen in the Pool/Vault code instead. The Strategy should be concerned with accepting deposits, deploying the deposits into the strategy, and processing withdrawals to back to the Pool/Vault. This means that even a developer who knows nothing about Superfluid can write a strategy. Which makes it easier to attract more developers and thus more strategies. And it keeps things cleaner: all Superfluid stuff happens in the Pool/Vault, and Strategies are interfaces to non-super DeFi protocols.
  3. The previous suggestion begs the question: does it make a sense to take a step further? What if Super Vaults adopted the same "standard" or Interface as Beefy or Yearn, etc.? Would it is be possible to suddenly plug in all the pre-existing yearn/beefy Strategies into any new Super Vault? This seems possible, but perhaps diving deeper would reveal obstacles. But I would argue that it is the holy grail here. Rather than have to re-write Strategies that effectively exist already, if Super Pool/Vaults could adopt an existing strategy interface, a broad array of Strategies could be available very quickly. Side benefits could be the re-use of existing (open-source) code that may have been already audited or at least has some Lindy effect.

The above three suggestions are high-level, more in thinking of how we might maximize both developers and users, rather than any deeper technical suggestions. I will continue to dive deeper into the contracts and perhaps provide some lower level questions / feedback...

Overall, this is really cool.... 👍

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.