350 lines
20 KiB
Markdown
350 lines
20 KiB
Markdown
|
---
|
|||
|
eip: 5115
|
|||
|
title: SY Token
|
|||
|
description: Interface for wrapped yield-bearing tokens.
|
|||
|
author: Vu Nguyen (@mrenoon), Long Vuong (@UncleGrandpa925), Anton Buenavista (@ayobuenavista)
|
|||
|
discussions-to: https://ethereum-magicians.org/t/eip-5115-super-composable-yield-token-standard/9423
|
|||
|
status: Draft
|
|||
|
type: Standards Track
|
|||
|
category: ERC
|
|||
|
created: 2022-05-30
|
|||
|
requires: 20
|
|||
|
---
|
|||
|
|
|||
|
## Abstract
|
|||
|
|
|||
|
This standard proposes an API for wrapped yield-bearing tokens within smart contracts. It is an extension on the [EIP-20](./eip-20.md) token that provides basic functionality for transferring, depositing, withdrawing tokens, as well as reading balances.
|
|||
|
|
|||
|
## Motivation
|
|||
|
|
|||
|
Yield generating mechanisms are built in all shapes and sizes, necessitating a manual integration every time a protocol builds on top of another protocol’s yield generating mechanism.
|
|||
|
|
|||
|
[EIP-4626](./eip-4626.md) tackled a significant part of this fragmentation by standardizing the interfaces for vaults, a major category among various yield generating mechanisms.
|
|||
|
|
|||
|
In this EIP, we’re extending the coverage to include assets beyond EIP-4626’s reach, namely:
|
|||
|
|
|||
|
- yield-bearing assets that have different input tokens used for minting vs accounting for the pool value.
|
|||
|
- This category includes AMM liquidity tokens (which are yield-bearing assets that yield swap fees) since the value of the pool is measured in “liquidity units” (for example, $\sqrt k$ in UniswapV2, as defined in UniswapV2 whitepaper) which can’t be deposited in (as they are not tokens).
|
|||
|
- This extends the flexibility in minting the yield-bearing assets. For example, there could be an ETH vault that wants to allow users to deposit cETH directly instead of ETH, for gas efficiency or UX reasons.
|
|||
|
- Assets with reward tokens by default (e.g. COMP rewards for supplying in Compound). The reward tokens are expected to be sold to compound into the same asset.
|
|||
|
- This EIP can be extended further to include the handling of rewards, such as the claiming of accrued multiple rewards tokens.
|
|||
|
|
|||
|
While EIP-4626 is a well-designed and suitable standard for most vaults, there will inevitably be some yield generating mechanisms that do not fit into their category (LP tokens for instance). A more flexible standard is required to standardize the interaction with all types of yield generating mechanisms.
|
|||
|
|
|||
|
Therefore, we are proposing Standardized Yield (SY), a flexible standard for wrapped yield-bearing tokens that could cover most mechanisms in DeFi. We foresee that:
|
|||
|
|
|||
|
- EIP-4626 will still be a popular vault standard, that most vaults should adopt.
|
|||
|
- SY tokens can wrap over most yield generating mechanisms in DeFi, including EIP-4626 vaults for projects built on top of yield-bearing tokens.
|
|||
|
- Whoever needs the functionalities of SY could integrate with the existing SY tokens or write a new SY (to wrap over the target yield-bearing token).
|
|||
|
- Reward handling can be extended from the SY token.
|
|||
|
|
|||
|
### Use Cases
|
|||
|
|
|||
|
This EIP is designed for flexibility, aiming to accommodate as many yield generating mechanisms as possible. Particularly, this standard aims to be generalized enough that it supports the following use cases and more:
|
|||
|
|
|||
|
- Money market supply positions
|
|||
|
- Lending DAI in Compound, getting DAI interests and COMP rewards
|
|||
|
- Lending ETH in BenQi, getting ETH interests and QI + AVAX rewards
|
|||
|
- Lending USDC in Aave, getting USDC interests and stkAAVE rewards
|
|||
|
- AMM liquidity provision
|
|||
|
- Provide ETH + USDC to ETHUSDC pool in SushiSwap, getting swap fees in more ETH+USDC
|
|||
|
- Provide ETH + USDC to ETHUSDC pool in SushiSwap and stake it in Sushi Onsen, getting swap fees and SUSHI rewards
|
|||
|
- Provide USDC+DAI+USDT to 3crv pool and stake it in Convex, getting 3crv swap fees and CRV + CVX rewards
|
|||
|
- Vault positions
|
|||
|
- Provide ETH into Yearn EIP-4626 vault, where the vault accrues yield from Yearn’s ETH strategy
|
|||
|
- Provide DAI into Harvest and staking it, getting DAI interests and FARM rewards
|
|||
|
- Liquid staking positions
|
|||
|
- Holding stETH (in Lido), getting yields in more stETH
|
|||
|
- Liquidity mining programs
|
|||
|
- Provide USDC in Stargate, getting STG rewards
|
|||
|
- Provide LOOKS in LooksRare, getting LOOKS yield and WETH rewards
|
|||
|
- Rebasing tokens
|
|||
|
- Stake OHM into sOHM/gOHM, getting OHM rebase yield
|
|||
|
|
|||
|
The EIP hopes to minimize, if not possibly eliminate, the use of customized adapters in order to interact with many different forms of yield-bearing token mechanisms.
|
|||
|
|
|||
|
## Specification
|
|||
|
|
|||
|
### Generic Yield Generating Pool
|
|||
|
|
|||
|
We will first introduce Generic Yield Generating Pool (GYGP), a model to describe most yield generating mechanisms in DeFi. In every yield generating mechanism, there is a pool of funds, whose value is measured in **assets**. There are a number of users who contribute liquidity to the pool, in exchange for **shares** of the pool, which represents units of ownership of the pool. Over time, the value (measured in **assets**) of the pool grows, such that each **share** is worth more **assets** over time. The pool could earn a number of **reward tokens** over time, which are distributed to the users according to some logic (for example, proportionally the number of **shares**).
|
|||
|
|
|||
|
Here are the more concrete definitions of the terms:
|
|||
|
|
|||
|
#### GYGP Definitions:
|
|||
|
|
|||
|
- **asset**: Is a unit to measure the value of the pool. At time *t*, the pool has a total value of *TotalAsset(t)* **assets**.
|
|||
|
- **shares**: Is a unit that represents ownership of the pool. At time *t*, there are *TotalShares(t)* **shares** in total.
|
|||
|
- **reward tokens**: Over time, the pool earns $n_{rewards}$ types of reward tokens $(n_{rewards} \ge 0)$. At time *t*, $TotalRewards_i(t)$ is the amount of **reward token *i*** that has accumulated for the pool up until time *t*.
|
|||
|
- **exchange rate**: At time *t*, the **exchange rate** *ExchangeRate(t)* is simply how many **assets** each **shares** is worth $ExchangeRate(t) = \frac{TotalAsset(t)}{TotalShares(t)}$
|
|||
|
- **users**: At time *t*, each user *u* has $shares_u(t)$ **shares** in the pool, which is worth $asset_u(t) = shares_u(t) \cdot ExchangeRate(t)$ **assets**. Until time *t*, user *u* is entitled to receive a total of $rewards_{u_i}(t)$ **reward token *i***. The sum of all users’ shares, assets and rewards should be the same as the total shares, assets and rewards of the whole pool.
|
|||
|
|
|||
|
#### State changes:
|
|||
|
|
|||
|
1. A user deposits $d_a$ **assets** into the pool at time $t$ ($d_a$ could be negative, which means a withdraw from the pool). $d_s = d_a / ExchangeRate(t)$ new **shares** will be created and given
|
|||
|
to user (or removed and burned from the user when $d_a$ is negative).
|
|||
|
2. The pool earns $d_a$ (or loses $−d_a$ if $d_a$ is negative) **assets** at time $t$. The **exchange rate** simply increases (or decreases if $d_a$ is negative) due to the additional assets.
|
|||
|
3. The pool earns $d_r$ **reward token** $i$. Every user will receive a certain amount of **reward token** $i$.
|
|||
|
|
|||
|
#### Examples of GYGPs in DeFi:
|
|||
|
|
|||
|
| Yield generating mechanism | Asset | Shares | Reward tokens | Exchange rate |
|
|||
|
| --- | --- | --- | --- | --- |
|
|||
|
| Supply USDC in Compound | USDC | cUSDC | COMP | USDC value per cUSDC, increases with USDC supply interests |
|
|||
|
| ETH liquid staking in Lido | stETH | wstETH | None | stETH value per wstETH, increases with ETH staking rewards |
|
|||
|
| Stake LOOKS in LooksRare Compounder | LOOKS | shares (in contract) | WETH | LOOKS value per shares, increases with LOOKS rewards |
|
|||
|
| Stake APE in $APE Compounder | sAPE | shares (in contract) | APE | sAPE value per shares, increases with APE rewards |
|
|||
|
| Provide ETH+USDC liquidity on Sushiswap | ETHUSDC liquidity (a pool of x ETH + y USDC has sqrt(xy) ETHUSDC liquidity) | ETHUSDC Sushiswap LP (SLP) token | None | ETHUSDC liquidity value per ETHUSDC SLP, increases due to swap fees |
|
|||
|
| Provide ETH+USDC liquidity on Sushiswap and stake into Onsen | ETHUSDC liquidity (a pool of x ETH + y USDC has sqrt(xy) ETHUSDC liquidity) | ETHUSDC Sushiswap LP (SLP) token | SUSHI | ETHUSDC liquidity value per ETHUSDC SLP, increases due to swap fees |
|
|||
|
| Provide BAL+WETH liquidity in Balancer (80% BAL, 20% WETH) | BALWETH liquidity (a pool of x BAL + y WETH has x^0.8*y^0.2 BALWETH liquidity) | BALWETH Balancer LP token | None | BALWETH liquidity per BALWETH Balancer LP token, increases due to swap fees |
|
|||
|
| Provide USDC+USDT+DAI liquidity in Curve | 3crv pool’s liquidity (amount of D per 3crv token) | 3crv token | CRV | 3crv pool’s liquidity per 3crv token, increases due to swap fees |
|
|||
|
| Provide FRAX+USDC liquidity in Curve then stake LP in Convex | BALWETH liquidity (a pool of x BAL + y WETH has x^0.8*y^0.2 BALWETH liquidity) | BALWETH Balancer LP token | None | BALWETH liquidity per BALWETH Balancer LP token, increases due to swap fees |
|
|||
|
|
|||
|
|
|||
|
### Standardized Yield Token Standard
|
|||
|
|
|||
|
#### Overview:
|
|||
|
|
|||
|
Standardized Yield (SY) is a token standard for any yield generating mechanism that conforms to the GYGP model. Each SY token represents **shares** in a GYGP and allows for interacting with the GYGP via a standard interface.
|
|||
|
|
|||
|
All SY tokens:
|
|||
|
|
|||
|
- **MUST** implement **`EIP-20`** to represent shares in the underlying GYGP.
|
|||
|
- **MUST** implement EIP-20’s optional metadata extensions `name`, `symbol`, and `decimals`, which **SHOULD** reflect the underlying GYGP’s accounting asset’s `name`, `symbol`, and `decimals`.
|
|||
|
- **MAY** implement [EIP-2612](./eip-2612.md) to improve the UX of approving SY tokens on various integrations.
|
|||
|
- **MAY** revert on calls to `transfer` and `transferFrom` if a SY token is to be non-transferable.
|
|||
|
- The EIP-20 operations `balanceOf`, `transfer`, `totalSupply`, etc. **SHOULD** operate on the GYGP “shares”, which represent a claim to ownership on a fraction of the GYGP’s underlying holdings.
|
|||
|
|
|||
|
#### SY Definitions:
|
|||
|
|
|||
|
On top of the definitions above for GYGPs, we need to define 2 more concepts:
|
|||
|
|
|||
|
- **input tokens**: Are tokens that can be converted into assets to enter the pool. Each SY can accept several possible input tokens $tokens_{in_{i}}$
|
|||
|
|
|||
|
- **output tokens**: Are tokens that can be redeemed from assets when exiting the pool. Each SY can have several possible output tokens $tokens_{out_{i}}$
|
|||
|
|
|||
|
#### Interface
|
|||
|
|
|||
|
```solidity
|
|||
|
interface IStandardizedYield {
|
|||
|
event Deposit(
|
|||
|
address indexed caller,
|
|||
|
address indexed receiver,
|
|||
|
address indexed tokenIn,
|
|||
|
uint256 amountDeposited,
|
|||
|
uint256 amountSyOut
|
|||
|
);
|
|||
|
|
|||
|
event Redeem(
|
|||
|
address indexed caller,
|
|||
|
address indexed receiver,
|
|||
|
address indexed tokenOut,
|
|||
|
uint256 amountSyToRedeem,
|
|||
|
uint256 amountTokenOut
|
|||
|
);
|
|||
|
|
|||
|
function deposit(
|
|||
|
address receiver,
|
|||
|
address tokenIn,
|
|||
|
uint256 amountTokenToDeposit,
|
|||
|
uint256 minSharesOut,
|
|||
|
bool depositFromInternalBalance
|
|||
|
) external returns (uint256 amountSharesOut);
|
|||
|
|
|||
|
function redeem(
|
|||
|
address receiver,
|
|||
|
uint256 amountSharesToRedeem,
|
|||
|
address tokenOut,
|
|||
|
uint256 minTokenOut,
|
|||
|
bool burnFromInternalBalance
|
|||
|
) external returns (uint256 amountTokenOut);
|
|||
|
|
|||
|
function exchangeRate() external view returns (uint256 res);
|
|||
|
|
|||
|
function getTokensIn() external view returns (address[] memory res);
|
|||
|
|
|||
|
function getTokensOut() external view returns (address[] memory res);
|
|||
|
|
|||
|
function yieldToken() external view returns (address);
|
|||
|
|
|||
|
function previewDeposit(address tokenIn, uint256 amountTokenToDeposit)
|
|||
|
external
|
|||
|
view
|
|||
|
returns (uint256 amountSharesOut);
|
|||
|
|
|||
|
function previewRedeem(address tokenOut, uint256 amountSharesToRedeem)
|
|||
|
external
|
|||
|
view
|
|||
|
returns (uint256 amountTokenOut);
|
|||
|
|
|||
|
function name() external view returns (string memory);
|
|||
|
|
|||
|
function symbol() external view returns (string memory);
|
|||
|
|
|||
|
function decimals() external view returns (uint8);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
#### Methods
|
|||
|
|
|||
|
```solidity
|
|||
|
function deposit(
|
|||
|
address receiver,
|
|||
|
address tokenIn,
|
|||
|
uint256 amountTokenToDeposit,
|
|||
|
uint256 minSharesOut,
|
|||
|
bool depositFromInternalBalance
|
|||
|
) external returns (uint256 amountSharesOut);
|
|||
|
```
|
|||
|
|
|||
|
This function will deposit *amountTokenToDeposit* of input token $i$ (*tokenIn*) to mint new SY shares.
|
|||
|
|
|||
|
If *depositFromInternalBalance* is set to *false*, msg.sender will need to initially deposit *amountTokenToDeposit* of input token $i$ (*tokenIn*) into the SY contract, then this function will convert the *amountTokenToDeposit* of input token $i$ into $d_a$ worth of **asset** and deposit this amount into the pool for the *receiver*, who will receive *amountSharesOut* of SY tokens (**shares**). If *depositFromInternalBalance* is set to *true*, then *amountTokenToDeposit* of input token $i$ (*tokenIn*) will be taken from receiver directly (as msg.sender), and will be converted and shares returned to the receiver similarly to the first case.
|
|||
|
|
|||
|
This function should revert if $amountSharesOut \lt minSharesOut$.
|
|||
|
|
|||
|
- **MUST** emit the `Deposit` event.
|
|||
|
- **MUST** support EIP-20’s `approve` / `transferFrom` flow where `tokenIn` are taken from receiver directly (as msg.sender) or if the msg.sender has EIP-20 approved allowance over the input token of the receiver.
|
|||
|
- **MUST** revert if $amountSharesOut \lt minSharesOut$ (due to deposit limit being reached, slippage, or the user not approving enough `tokenIn` **to the SY contract, etc).
|
|||
|
- **MAY** be payable if the `tokenIn` depositing asset is the chain's native currency (e.g. ETH).
|
|||
|
|
|||
|
```solidity
|
|||
|
function redeem(
|
|||
|
address receiver,
|
|||
|
uint256 amountSharesToRedeem,
|
|||
|
address tokenOut,
|
|||
|
uint256 minTokenOut,
|
|||
|
bool burnFromInternalBalance
|
|||
|
) external returns (uint256 amountTokenOut);
|
|||
|
```
|
|||
|
|
|||
|
This function will redeem the $d_s$ shares, which is equivalent to $d_a = d_s \times ExchangeRate(t)$ assets, from the pool. The $d_a$ assets is converted into exactly *amountTokenOut* of output token $i$ (*tokenOut*).
|
|||
|
|
|||
|
If *burnFromInternalBalance* is set to *false*, the user will need to initially deposit *amountSharesToRedeem* into the SY contract, then this function will burn the floating amount $d_s$ of SY tokens (**shares**) in the SY contract to redeem to output token $i$ (*tokenOut*). This pattern is similar to UniswapV2 which allows for more gas efficient ways to interact with the contract. If *burnFromInternalBalance* is set to *true*, then this function will burn *amountSharesToRedeem* $d_s$ of SY tokens directly from the user to redeem to output token $i$ (*tokenOut*).
|
|||
|
|
|||
|
This function should revert if $amountTokenOut \lt minTokenOut$.
|
|||
|
|
|||
|
- **MUST** emit the `Redeem` event.
|
|||
|
- **MUST** support EIP-20’s `approve` / `transferFrom` flow where the shares are burned from receiver directly (as msg.sender) or if the msg.sender has EIP-20 approved allowance over the shares of the receiver.
|
|||
|
- **MUST** revert if $amountTokenOut \lt minTokenOut$ (due to redeem limit being reached, slippage, or the user not approving enough `amountSharesToRedeem` to the SY contract, etc).
|
|||
|
|
|||
|
```solidity
|
|||
|
function exchangeRate() external view returns (uint256 res);
|
|||
|
```
|
|||
|
|
|||
|
This method updates and returns the latest **exchange rate**, which is the **exchange rate** from SY token amount into asset amount, scaled by a fixed scaling factor of 1e18.
|
|||
|
|
|||
|
- **MUST** return $ExchangeRate(t_{now})$ such that $ExchangeRate(t_{now}) \times syBalance / 1e18 = assetBalance$.
|
|||
|
- **MUST NOT** include fees that are charged against the underlying yield token in the SY contract.
|
|||
|
|
|||
|
```solidity
|
|||
|
function getTokensIn() external view returns (address[] memory res);
|
|||
|
```
|
|||
|
|
|||
|
This read-only method returns the list of all input tokens that can be used to deposit into the SY contract.
|
|||
|
|
|||
|
- **MUST** return EIP-20 token addresses.
|
|||
|
- **MUST** return at least one address.
|
|||
|
- **MUST NOT** revert.
|
|||
|
|
|||
|
```solidity
|
|||
|
function getTokensOut() external view returns (address[] memory res);
|
|||
|
```
|
|||
|
|
|||
|
This read-only method returns the list of all output tokens that can be converted into when exiting the SY contract.
|
|||
|
|
|||
|
- **MUST** return EIP-20 token addresses.
|
|||
|
- **MUST** return at least one address.
|
|||
|
- **MUST NOT** revert.
|
|||
|
|
|||
|
```solidity
|
|||
|
function yieldToken() external view returns (address);
|
|||
|
```
|
|||
|
|
|||
|
This read-only method returns the underlying yield-bearing token (representing a GYGP) address.
|
|||
|
|
|||
|
- **MUST** return a token address that conforms to the EIP-20 interface, or zero address
|
|||
|
- **MUST NOT** revert.
|
|||
|
- **MUST** reflect the exact underlying yield-bearing token address if the SY token is a wrapped token.
|
|||
|
- **MAY** return 0x or zero address if the SY token is natively implemented, and not from wrapping.
|
|||
|
|
|||
|
```solidity
|
|||
|
function previewDeposit(address tokenIn, uint256 amountTokenToDeposit)
|
|||
|
external
|
|||
|
view
|
|||
|
returns (uint256 amountSharesOut);
|
|||
|
```
|
|||
|
|
|||
|
This read-only method returns the amount of shares that a user would have received if they deposit *amountTokenToDeposit* of *tokenIn*.
|
|||
|
|
|||
|
- **MUST** return less than or equal of *amountSharesOut* to the actual return value of the `deposit` method, and **SHOULD NOT** return greater than the actual return value of the `deposit` method.
|
|||
|
- **MUST NOT** revert.
|
|||
|
|
|||
|
```solidity
|
|||
|
function previewRedeem(address tokenOut, uint256 amountSharesToRedeem)
|
|||
|
external
|
|||
|
view
|
|||
|
returns (uint256 amountTokenOut);
|
|||
|
```
|
|||
|
|
|||
|
This read-only method returns the amount of *tokenOut* that a user would have received if they redeem *amountSharesToRedeem* of *tokenOut*.
|
|||
|
|
|||
|
- **MUST** return less than or equal of *amountTokenOut* to the actual return value of the `redeem` method, and **SHOULD NOT** return greater than the actual return value of the `redeem` method.
|
|||
|
- **MUST NOT** revert.
|
|||
|
|
|||
|
#### Events
|
|||
|
|
|||
|
```solidity
|
|||
|
event Deposit(
|
|||
|
address indexed caller,
|
|||
|
address indexed receiver,
|
|||
|
address indexed tokenIn,
|
|||
|
uint256 amountDeposited,
|
|||
|
uint256 amountSyOut
|
|||
|
);
|
|||
|
```
|
|||
|
|
|||
|
`caller` has converted exact *tokenIn* tokens into SY (shares) and transferred those SY to `receiver`.
|
|||
|
|
|||
|
- **MUST** be emitted when input tokens are deposited into the SY contract via `deposit` method.
|
|||
|
|
|||
|
```solidity
|
|||
|
event Redeem(
|
|||
|
address indexed caller,
|
|||
|
address indexed receiver,
|
|||
|
address indexed tokenOut,
|
|||
|
uint256 amountSyToRedeem,
|
|||
|
uint256 amountTokenOut
|
|||
|
);
|
|||
|
```
|
|||
|
|
|||
|
`caller` has converted exact SY (shares) into input tokens and transferred those input tokens to `receiver`.
|
|||
|
|
|||
|
- **MUST** be emitted when input tokens are redeemed from the SY contract via `redeem` method.
|
|||
|
|
|||
|
**"SY" Word Choice:**
|
|||
|
|
|||
|
"SY" (pronunciation: */sʌɪ/*), an abbreviation of Standardized Yield, was found to be appropriate to describe a broad universe of standardized composable yield-bearing digital assets.
|
|||
|
|
|||
|
## Rationale
|
|||
|
|
|||
|
[EIP-20](./eip-20.md) is enforced because implementation details such as transfer, token approvals, and balance calculation directly carry over to the SY tokens. This standardization makes the SY tokens immediately compatible with all EIP-20 use cases.
|
|||
|
|
|||
|
[EIP-165](./eip-165.md) can optionally be implemented should you want integrations to detect the IStandardizedYield interface implementation.
|
|||
|
|
|||
|
[EIP-2612](./eip-2612.md) can optionally be implemented in order to improve the UX of approving SY tokens on various integrations.
|
|||
|
|
|||
|
## Backwards Compatibility
|
|||
|
|
|||
|
This EIP is fully backwards compatible as its implementation extends the functionality of [EIP-20](./eip-20.md), however the optional metadata extensions, namely `name`, `decimals`, and `symbol` semantics MUST be implemented for all SY token implementations.
|
|||
|
|
|||
|
## Security Considerations
|
|||
|
|
|||
|
Malicious implementations which conform to the interface can put users at risk. It is recommended that all integrators (such as wallets, aggregators, or other smart contract protocols) review the implementation to avoid possible exploits and users losing funds.
|
|||
|
|
|||
|
`yieldToken` must strongly reflect the address of the underlying wrapped yield-bearing token. For a native implementation wherein the SY token does not wrap a yield-bearing token, but natively represents a GYGP share, then the address returned MAY be a zero address. Otherwise, for wrapped tokens, you may introduce confusion on what the SY token represents, or may be deemed malicious.
|
|||
|
|
|||
|
## Copyright
|
|||
|
|
|||
|
Copyright and related rights waived via [CC0](../LICENSE.md).
|