The vault-v2 repository contains the contracts that enable Yield v2 as a Collateralized Debt Platform. Together, these contracts allow users to post collateral and borrow fyToken, while the protocol takes care of topics such as accounting, collateralization, redemption and liquidations.
The Yield Protocol uses its own Oracle interface as a wrapper to external oracles, so that it is possible to switch to external oracles with arbitrary interfaces with no impact on the core contracts.
The IOracle interface offers a get(bytes32 base, bytes32 quote, uint256 amount) returns (uint256 value)
function that accepts base and quote identifiers and an amount of base
to convert according to them and to the value returned by the external oracle.
peek
is the same as get
, but for oracles that offer non-transactional readings.
Each oracle stores multiple sources of the same type in a single contract, to reduce deployment costs.
Spot oracles return the value
of an amount
of base
in quote
terms. We use these to determine the collateralization level of vaults.
To charge borrowers for maintaining a borrowing position after maturity, Yield charges interest. The interest is determined by an oracle called the ‘rate’ oracle. Likewise, to compensate lenders who leave fyTokens unredeemed, fyTokens increase in redemption value after maturity based upon an oracle called the ‘chi’ oracle.
Rate and chi oracles return the value
of an amount
multiplied by the borrowing and lending rates for base
, respectively. These rates we currently get from Compound cTokens. We use these oracles to determine the debt of vaults after maturity, and the redemption value of fyToken after maturity.
Join contracts hold ERC20 and other assets that are external to the Yield Protocol, but that we manage as collateral or underlying.
We deploy one Join for each asset (collateral or underlying) that is accepted in the Yield Protocol. The Ladle keeps a registry of these Joins and is the only contract or account with permissions to move assets in or out of a Join.
The Join contract keeps track of the assets it should be holding, instead of relying on checking its balance at the asset contract. This allows removing the need for approvals in the integration with other contracts.
The join
function takes assets from the specified account. If there are any unaccounted assets already in the Join, these are used before transferring from the user.
Transfer an amount of asset to the given address.
Transfer an amount of any ERC20 other than asset to the given address, to enable airdrops to the Join.
Flash Loans
The Join can serve as a lender for ERC3156 compliant flash loans of the asset it holds.
Join Factory
To reduce the deployment size of Wand to under 24Kb, Joins are deployed through a Join Factory.
fyTokens are Ethereum based ERC-20 tokens that can be redeemed for an underlying asset one-to-one after a predetermined maturity date.
Redemptions can be executed at or after maturity. In a redemption, the fyTokens will be burnt and the underlying asset will be sent to a user on a one-to-one basis.
The fyTokens for redemption needs to either have been approved by the caller or needs to have been transferred to the fyToken contract. If transferring, the transfer
and the redeem
should happen in the same transaction.
On the first redemption or the first time that mature
is called, the chi oracle is read to record the chi value at maturity. On any subsequent redemptions, the chi oracle is read to calculate the chi accrual, defined as current chi / chi at maturity
.
The amount of underlying transferred on redemption is the amount of fyToken redeemed multiplied by the chi accrual.
On redemption, fyToken instructs the Join for the underlying to transfer the underlying to the target account.
The mint
and burn
functions are restricted, and only the Ladle can call them. It does so when issuing or repaying debt.
Can be flash minted with no fees following the ERC3156 standard.
For a given underlying asset, such as Dai, there would be a fyToken contract for each maturity. If, for example, we decide to have quarterly maturities for Dai in 2021, we would deploy 4 fyDai contracts: 31/03/21, 30/06/21, 30/09/21, and 31/12/21.
The Cauldron is the debt and collateral accounting system for Yield. The only external dependencies from Cauldron are towards rate oracles and spot oracles. All transactional functions in Cauldron require privileged access.
User debt and collateral are recorded in numbered vaults so that each account can have multiple independent positions. A series of fyToken is a specific ERC-20 contract that has a maturity time and an underlying or “base” asset. Vaults permit an owner to provide a single kind of collateral to borrow a single series of fyTokens. Vaults can be created, modified, transferred, or destroyed.
Each vault is linked to a single series and a single ilk. A series is a fyToken contract with associated data. An ilk is an asset that is used as collateral.
The vault data is divided into two separate structs, both accessible through the vaultId: Vault and Balances.
DataTypes.Vault records the owner, seriesId and ilkId.
DataTypes.Balances record the vault debt(art) and vault collateral(ink). You use ink to draw art.
stir
moves collateral and debt between vaults. It can only move debt or collateral between vaults with matching seriesId or ilkId, respectively.
It can be combined with build
and destroy
to split and merge vaults.
pour
is used to add and remove debt and collateral to vaults. It underpins borrowing and repaying.
stir
and pour
will revert if any affected vaults would be undercollateralized at the end of the transaction.
The debt of a vault is stored in fyToken terms, and only ever changes through user action.
However, if the debt is expressed in underlying assets, it increases after maturity through applying a borrowing rate obtained from the rate oracle for the underlying.
A different way to look at this mechanism
1 fyDai of debt can always be repaid with 1 fyDai
1 fyDai of debt can be paid with 1 Dai before maturity, or 1 Dai * rate_accrual after maturity, where rate_accrual = borrowing_rate_now / borrowing_rate_at_maturity
All vaults need to remain collateralized, or they will be at risk of liquidation.
A vault is collateralized if the value of its collateral is greater than the value of its debt by a factor equal to the collateralization ratio of the underlying and collateral pair.
ink * price * ratio >= art * rate_accrual
For the protocol to not go bankrupt, it must act when the value of the collateral in the vaults risks being lower than the debt owed by the vaults.
Liquidation engines are given access to the Cauldron to resolve the situation. Any undercollateralized vault can be seized by a liquidation engine using grab
. At that point, the vault effectively belongs to the liquidation engine.
As the auction proceeds, the liquidation engine will use settle
to send any obtained underlying to the appropriate Join, and the sold collateral to the auction buyers.
Once all the debt is sold, the liquidation engine will return the vault to its owner, with the debt reset to zero, and an amount of collateral that depends on the outcome of the auction.
Liquidation engines can have multiple implementations, but they must be approved by the Cauldron before they can operate.
The Cauldron also records the protocol debt (DataTypes.Debt.debt) for every underlying and collateral pair.
As users borrow from a given series, the debt is accounted in the protocol totals as the underlying from the series and the collateral that was used.
There are ceiling debt limits (DataTypes.Debt.max), and the Cauldron will reject registering any more debt that surpasses them.
The Witch is a Liquidation Engine, one of many possible implementations.
The Witch grabs uncollateralized vaults, replacing the owner by itself. Then it sells the vault collateral in exchange for underlying to pay its debt. The amount of collateral given increases over time, until it offers to sell all the collateral for underlying to pay all the debt. The auction is held open at the final price indefinitely.
After the debt is settled, the Witch returns the vault to its original owner.
The Ladle is a routing and asset management contract for Yield. It can be upgraded through Modules or replaced entirely. It has considerable privileges and is the most complex contract in the protocol.
The Ladle is authorized to make changes to the accounting in Cauldron. It is as well the only contract that is authorized to create, modify or destroy Vaults in the Cauldron.
The Ladle keeps a registry of all Joins and it is authorized to move assets from any Join to any account. The Ladle also moves assets from users to Joins, with allowances approved by the users.
The Ladle is authorized to mint fyToken at will. The Ladle also moves fyToken from users to FYToken contracts for burning, with allowances approved by the users. The Ladle knows about all the existing fyTokens through the series registry in the Cauldron.
The Ladle keeps a registry of all the Pools, indexed by the id of the series traded. The Ladle also moves assets from users to Pool contracts for trading, with allowances approved by the users.
Any number of actions can be batched in a single transaction using the Ladle using batch
. This is the most common method of operation.
The Ladle can also be used to execute arbitrary calls on any registered contracts using route
. This is used, for example, to deal with Pools and Strategies.
The Ladle can be extended by the use of modules. The Ladle can moduleCall
functions in modules that have been authorized via governance. The Modules can inherit from LadleStorage to read and modify the Ladle storage, although modifying it is discouraged.
The Ladle uses the integrations and authorizations described above to provide collateralized debt features.
When a user wants to deposit collateral and borrow a fyToken, the Ladle will pour
:
- Take the collateral from the user to the appropriate Join.
- Register the position in the Cauldron.
- Mint the fyToken in the user’s account.
Similarly, a user can repay debt and withdraw collateral with pour
.
- Take the fyToken from the user, and burn it.
- Register the new position in the Cauldron.
- Transfer the collateral from the Join to the user.
Users can also pay their debt with the underlying asset for the debt, instead of with fyTokens, using close
. While the debt in fyToken terms only changes through borrowing actions, the debt in underlying terms changes according to the matching rate oracle. The Ladle will get the rate accrual from the oracle through the Cauldron, and adjust asset transfers, and record positions accordingly.
Users can move collateral and debt between Vaults through the Ladle using stir
. Combined with the creation and destruction of Vaults, this can be used to split a Vault in two or to merge two Vaults into one.
Other Features
- Serve: Borrow and trade the fyToken for underlying, effectively borrowing at a fixed rate.
- Roll: Migrate debt between two series.
- Repay: Trade underlying for fyToken, to pay the debt.
- Repay Vault: Trade underlying for fyToken, to pay exactly all the debt in a vault.
- RepayFromLadle: Use any existing fyToken in the Ladle to repay debt.
- CloseFromLadle: Use any existing base in the Ladle to repay debt.
- Redeem: Redeem fyToken in a fyToken contract for underlying.
- Transfer: Take tokens from the caller to a destination.
- Permit management: Execute a Dai or ERC2162 off-chain permit.
- Ether management: Convert to WETH, or from WETH.
Bundling of discrete governance actions into useful governance features.
Add asset: Adding an asset involves registering it in the Cauldron, deploying a Join for it, registering the Join in the Ladle, and permissioning the Ladle to use the Join. Make base: Making a base out of an existing asset involves setting an oracle for its borrowing rate in the Cauldron, and allowing the Witch to put base into its join during liquidations. Make ilk: Making an ilk out of an existing asset, for a given base, involves setting a spot oracle for the base/ilk pair in the Cauldron, setting debt limits in the Cauldron, and allowing the Witch to take such ilk out of its Join during liquidations. Add series: Adding a series is a costly process in terms of gas. It involves deploying the related fyToken, registering it in the Cauldron, and approving a number of ilks to be used as collateral when borrowing it. The fyToken also need to be given access to the Join for its underlying. The Ladle must be given permission to mint and burn the added fyToken. A Pool to trade between the fyToken and its underlying must be deployed, and registered in the Ladle.
YieldSpace is an automated liquidity provider that is designed to enable efficient trading between fyTokens and their underlying assets. You can read more about it in our YieldSpace Whitepaper to get a deeper understanding of the calculations and the overall mechanism. The Yield App integrates YieldSpace seamlessly into the user experience.
YieldSpace uses ABDK's Math64x64 library, which we have upgraded to 0.8.
YieldMath.sol uses Math64x64 to implement YieldSpace as described in the whitepaper. The functions implemented in YieldMath are buyBase, sellBase, buyFYToken and sellFYToken.
The Pool contracts are a DEX implementation very much in the style of Uniswap v2, but using YieldMath to calculate the trades.
Each Pool must keep a balance of the two assets it trades: One underlying asset and one fyToken. The marginal interest rate currently offered by the Pool is determined from the relative levels of the reserves of the underlying asset and the fyToken.
To build up these balances, liquidity providers can provide assets using mint
, receiving pool tokens in exchange. Pool token holders can burn
them to receive their assets back, plus a proportion of the pool growth that can be attributed to trading fees.
When providing liquidity, assets must be provided in the same proportion as the pool balances, thus a liquidity provider is required to provide precise amounts of underlying and fyToken. When a Pool has exactly zero fyToken, only underlying is provided to mint
. This is the state when a Pool is first deployed. The initial fyToken balance must come from someone selling fyToken to the Pool.
In a Pool, the fyToken balances must always be higher than the underlying balances for the curve not to go into negative territory. This means that there are always some fyTokens that cannot be purchased. An optimization already present in Yield v1 is that the supply of pool tokens is added to the fyToken balance when trading. These “virtual fyTokens” allow us to safely reduce the amount of fyToken that must be kept in the Pools.
Since providing both underlying and fyToken is inconvenient, the Pool implements a mintWithBase
function that allows the liquidity provider to provide only underlying, and a virtual trade will be done to convert a proportion of the underlying to fyToken inside the same pool. The amount of underlying to trade for fyToken must be calculated off-chain.
In a similar vein, a burnForBase
function allows liquidity providers to burn their pool tokens, and trade the fyToken received for underlying without any additional asset transfers, saving gas.
The Pool allows the user to sell assets (underlying or fyToken). In this trade, the amount being sold is known, but not the amount being received. Within the same transaction, sellBasePreview
or sellFYTokenPreview
can be called to know the exact amount to be received.
The Pool also allows the user to buy assets. In this trade, the amount being received is known, but not the amount taken by the pool. Within the same transaction, buyBasePreview
or .mdbuyFYTokenPreview
can be called to know the exact amount to be taken by the Pool.
It is important to note that the Pool doesn’t directly take any assets from anywhere. The assets must be in the Pool before the call to a trading function for the trade to happen. In the case of calls to buyBase
or buyFYToken
, where the input amount is not known, the user must either call the appropriate Preview
function in the same transaction, to transfer the exact amount to the Pool, or must transfer an amount judged sufficient for the trade to execute, and can call retrieveBase
or retrieveFYToken
afterwards (but in the same transaction) to recover any surplus.
The TS, G1, and G2 parameters determine the trading curve and the trading fees and can be set by the PoolFactory owner via the setParameter
function for newly deployed Pools.
Much like Uniswap v2 implements a Time-Weighted Average Price, Yield v2 implements a Time-Weighted Average Rate following the same pattern. Pool balances are stored along with the time that the TWAR was last updated, and the ratio of the cumulative balance. With this, the Pool can be used by external contracts as an Interest Rate Oracle between an underlying and a fyToken.
The PoolFactory is a permissioned contract that allows deploying Pools using CREATE2.
The PoolExtensions library includes functions to calculate the trading limits and invariant for a Pool. A PoolExtensionsWrapper is included to be able to call them externally.
The Strategy contracts allow liquidity providers to pool liquidity to the Strategy, which in turn provides liquidity to a YieldSpace Pool. Upon maturity, the Strategy rolls the liquidity to a new Pool at no cost to liquidity providers.
Liquidity providers mint Strategy tokens with Pool tokens of the pool that the Strategy is currently invested in, and receive a share of Strategy tokens proportional to their investment.
Liquidity providers can burn Strategy tokens of a proportional share of Pool tokens from the Strategy. These Pool tokens are from the current pool, not necessarily the same that they used to invest.
If the Strategy is not invested in any Pool, liquidity providers can burn their Strategy tokens for a proportional share of the underlying held by the Strategy.
A governance action sets the next pool to roll to. This action should be taken by a governor role through a long-delay Timelock, to give investors time to react in case they disagree with the next pool.
A permissioned startPool
function can be called if the Strategy is not currently investing in any pool, to invest in the next pool set by setNextPool
.
After the current pool matures, anyone can call endPool
to convert all the Strategy holdings into underlying.
The Yield v2 protocol doesn't directly reuse any other smart contract implementations. Any general use smart contracts needed were reimplemented and stored in the yield-utils-v2 repository.
The access control contract was adapted from OpenZeppelin's AccessControl.sol and is inherited from most other contracts in the Yield Protocol.
A role exists implicitly for each function in a contract, with the ROOT role as the admin for the role.
If the auth
modifier is present in a function, access must have been granted to the caller by an account with the admin role for the function role. This admin role will usually be ROOT, but that can be changed.
An admin
modifier exists to restrict functions to accounts bearing the admin
role of a given other role. This is not used outside AccessControl.sol.
An account belonging to the admin role for a function can grant and revoke memberships to the function role.
The ROOT role is special in that it is its own admin so that any member of ROOT can grant and revoke ROOT permissions on other accounts.
There is a special LOCK role, that is also an admin of itself, but has no members. By changing the admin role of a function role to LOCK, no further changes can ever be done to the function role membership, except users voluntarily renouncing to the function role.
The Yield Protocol ERC20 contracts borrow heavily from the DS-Token implementation. The ERC2162 extension is taken from WETH10, which was taken in turn from Yield v1.
The Yield Protocol uses its own implementation of a Timelock, derived from Compound’s original contract, but inheriting from AccessControl and implementing a different pattern to set the earliest time that an execution can be done.
The EmergencyBrake stores the instructions to remove the orchestration between contracts, which is intended to be used in emergency situations to easily isolate parts of the protocol.