ERC-5115 - SY Token

Created 2022-05-30
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This standard proposes an API for wrapped yield-bearing tokens within smart contracts. It is an extension on the ERC-20 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.

ERC-4626 tackled a significant part of this fragmentation by standardizing the interfaces for vaults, a major category among various yield generating mechanisms.

In this ERC, we’re extending the coverage to include assets beyond ERC-4626’s reach, namely:

While ERC-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:

Use Cases

This ERC 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:

The ERC 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:

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:

SY Definitions:

On top of the definitions above for GYGPs, we need to define 2 more concepts:

Interface

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

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$.

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$.

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.

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.

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.

function yieldToken() external view returns (address);

This read-only method returns the underlying yield-bearing token (representing a GYGP) address.

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.

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.

Events

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.

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.

"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

ERC-20 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 ERC-20 use cases.

ERC-165 can optionally be implemented should you want integrations to detect the IStandardizedYield interface implementation.

ERC-2612 can optionally be implemented in order to improve the UX of approving SY tokens on various integrations.

Backwards Compatibility

This ERC is fully backwards compatible as its implementation extends the functionality of ERC-20, 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.