art_img
March 13, 2026

How YieldBasis really works

Statemind team
Statemind team

Introduction

YieldBasis enables users to provide BTC as liquidity in an AMM pool without impermanent loss (IL), while still earning trading fees.

YieldBasis is a novel DeFi protocol that implements a new leveraged liquidity primitive designed by Michael Egorov.

In this article, we will focus on the implementation of the protocol. For a detailed explanation of mathematics at the root of the YB protocol, please refer to the documentation and whitepaper.

The idea

Liquidity providers deposit BTC (for simplicity, anything else can be used), while the protocol borrows an equal USD value in crvUSD, pairing both into the Curve BTC/crvUSD pool. This keeps the position at ~2× compounding leverage (50% debt-to-value), so its value tracks BTC 1:1 (no IL) while earning trading fees.

p\sqrt{p} vs pp

For the classic AMM bonding curve, the price of liquidity is approximately the square root of the token price pLPpp_{LP} \propto\sqrt{p}. Which will always lose to holding if price changes: p<1/2+p/2\sqrt{p} < 1/2 + p/2.

Idea graph

To fight this, YieldBasis introduces the concept of compounding leverage, which is denoted as the number determining the fraction of debt relative to collateral size.

debt=Vcollateral(11L)\begin{align} debt = V_{collateral} * (1 - \frac{1}{L}) \end{align}

If one leverages a token at a price pp and keeps the loan value equal to (1), then the position will be leveraged at a constant LL. This makes the price of the whole position pp^* proportional to pLp^L.

ppLp^* \propto p^L

So the proposed solution is to square the square root 🙂. This is done by leveraging x2 the position in the pool, which makes the rate of position value change linear to the asset price.

proposed solution

As per the whitepaper x2 leverage also has a great property: we take debt equal to half of the liquidity, hence if leverage is kept constant, we will always have enough USD to close the position. From this, we can already express APR: we receive twice the rpoolr_{pool} pool returns, because we borrow the same value as we initially provided (e.g. BTC), we pay for the rborrowr_{borrow} borrowed amount, and we suffer from rlossr_{loss} losses to keep the leverage constant:

APR2rpool(rborrow+rloss)APR \approx 2*r_{pool} - (r_{borrow} + r_{loss})

Rebalancing the leverage

The special LEVAMM is designed to create an economic incentive for arbitrageurs to keep the leverage constant.

The LEVAMM tracks:

  • yy - reserves of cryptopool (BTC/crvUSD) LP tokens;
  • dd - debt of crvUSD;
  • pop_o - price of a cryptopool LP in USD.

In principle, LEVAMM is a classic constant product with xx0(po)dx \equiv x_0(p_o)-d:

(x0(po)d)y=I(po)\begin{align} (x_0(p_o)-d)y=I(p_o) \end{align}

As stated above, the debt must be equal to half of the leveraged position. To get the correct amount, we must use a price oracle. In this case, the LEVAMM will always use the LP cryptopool price — p0p_0.

=L1Lpo=12po\begin{align} \overset{-}{d} = \frac{L-1}{L}p_o\overset{-}{y} = \frac{1}{2}p_o\overset{-}{y} \end{align}

Here \overset{-}{d} and \overset{-}{y} are “ideal” debt and collateral reserves, which means they are taken when the market price is equal to the oracle price p=p0p=p_0.

From the p=x/yp = x/y property of constant-product AMM, we can get the definition of x0x_0 in terms of ideal values ( p=pop=p_o):

x0(po)=2L1Lpox_0(p_o) = \frac{2L-1}{L}p_o\overset{-}{y}

It's hard to put a physical meaning behind x0(po)x_0(p_o), but if talking plainly, x0dx_0-d acts like the effective value in USD. Which makes sense, because we want LEVAMM to trade in (LP, USD) pairs, hence the price ratio (x0d)/y(x_0-d)/y will denote price of LP in USD.

We can derive x0(po)x_0(p_o) for any values of yy and dd:

x0(p0)=poy+po2y24p0yd(L2L1)22(L2L1)2\begin{align} x_0(p_0) = \frac{p_o y + \sqrt{p_o^2 y^2 - 4p_0 y d (\frac{L}{2L-1})^2} }{2(\frac{L}{2L-1})^2} \end{align}

From which we can get the AMM value:

V=pod=1Lpo=x02L1\begin{align} V=\overset{-}{y}p_o-d =\frac{1}{L}\overset{-}{y}p_o=\frac{x_0}{2L-1} \end{align}

But how does this actually help with rebalancing?
The system itself offers us two sources of collateral:

  • LEVAMM with pAMMp_{AMM} price;
  • Cryptopool with pop_o price.

We can determine if we can profit from trading on this pool by looking at the LEVAMM price pAMMp_{AMM}:

pAMM=xy=x0dyp_{AMM} = \frac{x}{y}=\frac{x_0-d}{y}

1/2

po    DTV<50%p_{o} \nearrow \implies \text{DTV} < 50\%

When collateral grows in value (e.g. BTC price is rising), debt-to-value decreases, hence traders must add debt by selling collateral to the pool in exchange for USD. For this, LEVAMM will offer LP tokens for better price pAMM>pop_{AMM} > p_o. (+debt, +collateral).

2/2

po    DTV>50%p_{o} \searrow \implies \text{DTV} > 50\%

When collateral falls, LEVAMM becomes over-leveraged, hence arbitrageur must reduce debt by buying collateral from the pool by spending USD. For this, LEVAMM will offer USD for better price pAMM<pop_{AMM} < p_o. (-debt, -collateral).

When trading towards either side, you change both numerator and denominator with the same sign. Here additional spread is created due to dynamic x0(po)x_0(p_o), which reacts to the oracle price.

💡Debt & rebalancing risk. The system holds debt against collateral and relies on rebalancing to keep DTV near 50%. In stressed conditions (flash moves, tight pool limits, high gas), rebalancing can slip, raising DTV and costs. The AMM’s safety band blocks unsafe states, and emergency exits require the caller to fund any stable shortfall—these limit protocol-level bad debt, but users can still incur losses from price moves and execution costs.

Fee split

The main yield comes from trading fees in the cryptopool, while some additional fees are generated in the LEVAMM.

💡Cryptopool (CurveV2 AMM) is a rebalancing AMM, which allocates half of its own revenue to the repegging mechanism. The other half goes to the LPs.

For users, YieldBasis offers two choices of accruing rewards:

  • unstaked shares - earning trading fees;
  • staked shares - earning YB token emissions.

Fee split

The dynamic admin fee faf_a is adjusted based on staking participation:

fa=1(1fmin)1sTf_a=1-(1-f_{min})*\sqrt{1-\frac{s}{T}}

Where:

  • TT - total supply of shares
  • ss - staked supply of shares
  • fminf_{min} - minimum admin fee

From the formula we can see that more users stake their shares, more fees DAO receives.

The code

The “heart” of the protocol is two contracts:

  • LT — LT.vy;
  • LEVAMM — AMM.vy.

The LT.vy is an entry-point for liquidity provision into the YB protocol; it acts as an owner of the AMM.vy contract.
The LEVAMM is the rebalancing AMM, the _deposit() and _withdraw() are callable only by LT, while exchange() method is open to any arbitrager.

the code

AMM

The LEVAMM contract is pretty straightforward; it works just like any other constant-product AMM with a few caveats.

x0(po)x_0(p_o)

The x0(p0)x_0(p_0) is essential for leverage AMM as it defines the curve and the value of the LEVAMM.

@internal
@view
def get_x0(p_oracle: uint256, collateral: uint256, debt: uint256, safe_limits: bool) -> uint256:
    # Safe limits:
    # debt >= 0
    # debt <= coll_value * 10**18 // (4 * LEV_RATIO)  ( == 9 / 16 * coll_value)
    # debt in equilibrium = coll_value * (LEVERAGE - 1.0) / LEVERAGE  ( == 1/2 * coll_value)
    # When L=2, critical value of debt corresponds to p_amm = 9/16 * p_o
    # Just in case, we limit between (1/16 .. 8.5/16) which is a somewaht tighter range

		# p_o * y
    coll_value: uint256 = p_oracle * collateral * COLLATERAL_PRECISION // 10**18

    if safe_limits:
        assert debt >= coll_value * MIN_SAFE_DEBT // 10**18, "Unsafe min"
        assert debt <= coll_value * MAX_SAFE_DEBT // 10**18, "Unsafe max"
		# per whitepaper:
		# D = (p_o * y)^2 - 4 * p_o * y * d * (L / (2*L-1))^2
    D: uint256 = coll_value**2 - 4 * coll_value * LEV_RATIO // 10**18 * debt
    # (p_o * y + sqrt(D)) / (2 * (L / (2*L-1))^2)
    return (coll_value + self.sqrt(D)) * 10**18 // (2 * LEV_RATIO)

Where LEV_RATIO is an immutable variable:

denominator: uint256 = 2 * leverage - 10**18
LEV_RATIO = leverage**2 * 10**18 // denominator**2

The safety limits are introduced so AMM cannot enter an unfavourable state, where it can become stuck by exchanges and interest charged on debt.

The minimum safe limit is just that we can't have negative debt. The maximum is derived from (4):

po2y24p0yd(L2L1)20poy4(2L1L)2dL=2    poy916dp_o^2 y^2 - 4p_0 y d (\frac{L}{2L-1})^2 \ge 0 \\ \frac{p_o y}{4} (\frac{2L-1}{L})^2 \ge d \\ \text{L=2} \implies p_o y \frac{9}{16} \ge d

The subtle, but quite important nuance, if you look closely at the units in xox_o calculation, you may see that p_oracle * collateral is in USD units, while debt is in stablecoin units (not using crvUSD/USD oracle here). This means YB acts as an additional CDP for crvUSD, helping it keep the peg.

Debt

Stablecoins are allocated (from Curve directly) to the LEVAMM contract and then lent to create new cryptopool positions. The resulting debt accrues interest at a borrowing rate set by admin. Accrued interest is collected and contributed to the cryptopool to fund rebalancing.

💡The new version of cryptopool has been developed along the YieldBasis protocol and supports donations to aid in rebalancing of the pool.

For details, see Curve Cryptopool with Donations.”

@internal
@view
def _rate_mul() -> uint256:
    """
    @notice Rate multiplier which is 1.0 + integral(rate, dt)
    @return Rate multiplier in units where 1.0 == 1e18
    """
    return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)

@external
@view
def get_rate_mul() -> uint256:
    """
    @notice Rate multiplier which is 1.0 + integral(rate, dt)
    @return Rate multiplier in units where 1.0 == 1e18
    """
    return self._rate_mul()

@internal
@view
def _debt() -> uint256:
    return self.debt * self._rate_mul() // self.rate_mul

@internal
def _debt_w() -> uint256:
    rate_mul: uint256 = self._rate_mul()
    debt: uint256 = self.debt * rate_mul // self.rate_mul
    self.rate_mul = rate_mul
    self.rate_time = block.timestamp
    return debt

The rate_mul factor represents interest that has already accrued up to the last update. The current debt is calculated by dividing self.rate_mul because the debt has already consisted of previous accruals.

On every function execution, we must query for the actual debt value, which includes the growth.

Deposit

The _deposit() function is plain and simple, calculates new values, and returns the pool value to the LT. Accessible only for LT.

@external
def _deposit(d_collateral: uint256, d_debt: uint256) -> OraclizedValue:
    assert msg.sender == LT_CONTRACT, "Access violation"
    assert not self.is_killed

    p_o: uint256 = extcall PRICE_ORACLE_CONTRACT.price_w()
    collateral: uint256 = self.collateral_amount  # == y_initial
    debt: uint256 = self._debt_w() # get debt with growth

    debt += d_debt
    collateral += d_collateral
    self.minted += d_debt # calculates how much debt entered the system 

    self.debt = debt
    self.collateral_amount = collateral
    # Assume that transfer of collateral happened already (as a result of exchange)

    value_after: uint256 = self.get_x0(p_o, collateral, debt, True) * 10**18 // (2 * LEVERAGE - 10**18)  # Value in fiat

    log AddLiquidityRaw(token_amounts=[d_collateral, d_debt], invariant=value_after, price_oracle=p_o)
    return OraclizedValue(p_o=p_o, value=value_after)

Here, _deposit() does not enforce restrictions on inputs d_collateral and d_debt, but the resulting x0x_0 must abide by safety limits.

Withdraw

Calculate new debt and collateral values by provided fraction, return the difference. Accessible only for LT.

@external
def _withdraw(frac: uint256) -> Pair:
    assert msg.sender == LT_CONTRACT, "Access violation"

    collateral: uint256 = self.collateral_amount  # == y_initial
    debt: uint256 = self._debt_w() # get debt with growth

    d_collateral: uint256 = collateral * frac // 10**18
    d_debt: uint256 = math._ceil_div(debt * frac, 10**18) # avoid dust leftovers

    self.collateral_amount -= d_collateral
    self.debt = debt - d_debt
    self.redeemed += d_debt # calculates how much debt has exited the system

    log RemoveLiquidityRaw(collateral_change=d_collateral, debt_change=d_debt)

    return Pair(collateral=d_collateral, debt=d_debt)

The LEVAMM allows only proportional withdrawals to avoid any unfair disadvantages for LPs (can’t take less debt, does not change debt-to-value). Generally speaking, frac here is user’s LT shares fraction.
Notice, how _withdraw() does not account for any of the fees, all related calculations are performed in the LT contact.

Exchange (keeping the leverage)

The center of the repegging algorithm is the exchange(), It implements a constant-product like swap mechanism, adapting to the specifics of the leveraging.

@external
@nonreentrant
def exchange(i: uint256, j: uint256, in_amount: uint256, min_out: uint256, _for: address = msg.sender) -> uint256:
    """
    @notice Exchanges two coins, callable by anyone
    @param i Index of input coin (0 = stablecoin, 1 = LP token collateral)
    @param j Output coin index
    @param in_amount Amount of input coin to swap
    @param min_out Minimal amount to get as output
    @param _for Address to send coins to
    @return Amount of coins given in/out
    """
    assert (i == 0 and j == 1) or (i == 1 and j == 0)
    assert not self.is_killed

    collateral: uint256 = self.collateral_amount  # == y_initial
    assert collateral > 0, "Empty AMM"
    debt: uint256 = self._debt_w() # debt + new interest
    p_o: uint256 = extcall PRICE_ORACLE_CONTRACT.price_w()
    x0: uint256 = self.get_x0(p_o, collateral, debt, False) # x0 is also the pool value
    x_initial: uint256 = x0 - debt
    
    out_amount: uint256 = 0
    # swap fees
    fee: uint256 = self.fee

    if i == 0:  # Trader buys collateral from us
		    # -debt, -collateral
        x: uint256 = x_initial + in_amount
        y: uint256 = math._ceil_div(x_initial * collateral, x)
        out_amount = (collateral - y) * (10**18 - fee) // 10**18
        assert out_amount >= min_out, "Slippage"
        debt -= in_amount
        collateral -= out_amount
        self.redeemed += in_amount # calculates how much debt exited the system
        assert extcall STABLECOIN.transferFrom(msg.sender, self, in_amount, default_return_value=True)
        assert extcall COLLATERAL.transfer(_for, out_amount, default_return_value=True)
		
    else:  # Trader sells collateral to us
		    # +debt, +collateral
        y: uint256 = collateral + in_amount
        x: uint256 = math._ceil_div(x_initial * collateral, y)
        out_amount = (x_initial - x) * (10**18 - fee) // 10**18
        assert out_amount >= min_out, "Slippage"
        debt += out_amount
        self.minted += out_amount # calculates how much debt entered the system
        collateral = y
        assert extcall COLLATERAL.transferFrom(msg.sender, self, in_amount, default_return_value=True)
        assert extcall STABLECOIN.transfer(_for, out_amount, default_return_value=True)
        
    # This call also will not allow to get too close to the untradable region
    assert self.get_x0(p_o, collateral, debt, True) >= x0, "Bad final state"
    # dont let the pool value decrease --^
    self.collateral_amount = collateral
    self.debt = debt

    log TokenExchange(buyer=msg.sender, sold_id=i, tokens_sold=in_amount,
                      bought_id=j, tokens_bought=out_amount, fee=fee, price_oracle=p_o)

    if LT_CONTRACT != empty(address) and LT_CONTRACT.is_contract:
        # collect the fees charged as interest
        self._collect_fees()
        # distribute borrower fees to the cryptopool
        extcall LT(LT_CONTRACT).distribute_borrower_fees() 

    return out_amount

The swap logic is something you might see anywhere else with a good old constant-product curve xy=kxy=k:

ynew=xyx+ΔxΔy=(yynew)(1fee)y_{new} = \frac{x*y}{x+\Delta x} \\ \Delta y = (y-y_{new})*(1-fee)

The swap does not allow for value to decrease and, just like in _deposit(), the protocol should enforce safety limits for the resulting x0x_0.

Fees

In the code snippets above, you might see minted and redeemed variables:

  • minted captures the debt before any interest is applied, is increased on every new debt entering the system — _deposit() and exchange();
  • redeemed captures the debt after the interest is paid, is increased on every debt exiting the system - _withdraw() and exchange().

Those variables can show how much interest is already repaid:

interest = debt + redeemed - minted\text{interest = debt + redeemed - minted}

LEVAMM also charges swap fees, which are not accounted for in the minted nor redeemed, which means they are scattered as additional value for LT shares holders.

The _collect_fees() is intended to retrieve exactly the debt interest:

@external
@view
def accumulated_interest() -> uint256:
    """
    @notice Calculate the amount of fees obtained from the interest
    """
    minted: uint256 = self.minted
    return unsafe_sub(max(self._debt() + self.redeemed, minted), minted)

@internal
def _collect_fees() -> uint256:
    """
    @notice Collect the fees charged as interest.
    """
    assert not self.is_killed

    debt: uint256 = self._debt_w() # new debt
    self.debt = debt
    minted: uint256 = self.minted
    to_be_redeemed: uint256 = debt + self.redeemed
    # Difference between to_be_redeemed and minted amount is exactly due to interest charged
    # Everything we minted is either
    # 1. minted_d + debt_increase = debt
    # 2. minted_r + redeemed_increase = self.redeemed
    # hence debt + self.redeemed - self.minted = increase
    if to_be_redeemed > minted:
		    # we take out the interest, hence equalize variables
        self.minted = to_be_redeemed
        to_be_redeemed = unsafe_sub(to_be_redeemed, minted)  # Now this is the fees to charge
        stables_in_amm: uint256 = staticcall STABLECOIN.balanceOf(self)
        # take out min(stables_in_amm, to_be_redeemed)
        if stables_in_amm < to_be_redeemed:
            self.minted -= (to_be_redeemed - stables_in_amm)
            to_be_redeemed = stables_in_amm
        assert extcall STABLECOIN.transfer(LT_CONTRACT, to_be_redeemed, default_return_value=True)
        log CollectFees(amount=to_be_redeemed, new_supply=debt)
        return to_be_redeemed
    else:
        log CollectFees(amount=0, new_supply=debt)
        return 0

In rare cases, when there are not enough stablecoins to retrieve interest, LEVAMM allows for partial retrieval.
As stated in the idea section, those “fees” are used to refuel the cryptopool rebalancing mechanism.

LT

The LT contract is the market’s share token. Through the methods of this contract, users deposit and withdraw from the cryptopool. It is responsible for stablecoin and fee management.

Staker

The staker itself is a LiquidityGauge contract, which manages the rewards distribution for LT stakers. The staker interactions are cooked into regular transfers, mint/burn paths, so staking effects happen automatically during routine calls.

⚠️Users must interact with LiquidityGauge contract to correctly account for staked shares. Directly transferring shares to the staker address will result in loss of funds.

@internal
def _transfer(_from: address, _to: address, _value: uint256):
    assert _to not in [self, empty(address)]

    staker: address = self.staker
    staker_used: bool = (staker != empty(address) and staker in [_from, _to])
    if staker_used:
			...

    self.balanceOf[_from] -= _value
    self.balanceOf[_to] += _value

    if staker_used and msg.sender != staker:
        self._checkpoint_gauge()

    log IERC20.Transfer(sender=_from, receiver=_to, value=_value)

If staker is not used, then this is just a regular token transfer of LT shares. Let's dive into the staker functionality.

    if staker_used:
        assert _from != _to
        liquidity: LiquidityValuesOut = empty(LiquidityValuesOut)

				# staker is LiquidityGauge with deposit and withdrawal functions
        if msg.sender == staker or staticcall self.amm.is_killed():
            # If sender is staker - it's mint/redeem/deposit/withdraw
            # where we did checkpoint_staker_rebase() in the same tx
            liquidity.ideal_staked = self.liquidity.ideal_staked
            liquidity.staked = self.liquidity.staked
            liquidity.total = self.liquidity.total
            liquidity.supply_tokens = self.totalSupply
            liquidity.staked_tokens = self.balanceOf[staker]
        else:
            liquidity = self._calculate_values(self._price_oracle_w())
            self.liquidity.admin = liquidity.admin
            self.liquidity.total = liquidity.total
            self.totalSupply = liquidity.supply_tokens
            self.balanceOf[staker] = liquidity.staked_tokens
            self._log_token_reduction(staker, liquidity.token_reduction)

The LiquidityGauge offers an interface for user interactions via mint()/redeem()/deposit()/withdraw() functions, which update the state by calling LT.checkpoint_staker_rebase() (invokes _calculate_values()) so the state is always up-to-date. If amm.is_killed() then LT.checkpoint_staker_rebase() skips an update as intended.

The staked shares must not earn trading fees, but they must suffer losses (else everyone would stake during loss periods). For that reason, an ideal_staked is introduced, It is used to denote the maximum value of staked shares. The lost value will be replenished over time on new earnings. If there is no lost value then ideal_staked == staked.

value

        if _from == staker:
            # Reduce the staked part
            # change by 0 if no supply_tokens or stake_tokens found
            liquidity.staked -= unsafe_div(liquidity.total * _value, liquidity.supply_tokens)
            liquidity.ideal_staked = unsafe_div(liquidity.ideal_staked * (liquidity.staked_tokens - _value), liquidity.staked_tokens)
        elif _to == staker:
            # Increase the staked part
            d_staked_value: uint256 = liquidity.total * _value // liquidity.supply_tokens
            liquidity.staked += d_staked_value
            if liquidity.staked_tokens > 10**10:
                liquidity.ideal_staked = liquidity.ideal_staked * (liquidity.staked_tokens + _value) // liquidity.staked_tokens
            else:
                # To exclude division by zero and numerical noise errors
                liquidity.ideal_staked += d_staked_value

        self.liquidity.staked = liquidity.staked
        self.liquidity.ideal_staked = liquidity.ideal_staked

We increase the target value by adding the converted proportion of the total value, because the total may contain additional yield.

Videal_stakednew=Videal_staked+VtotalsStotalV_{ideal\_staked}^{new} = V_{ideal\_staked} + V_{total} * \frac{s}{S_{total}}

And decrease the target by the proportion of unstaked shares, because the staked maximum value is fixed and cannot increase.

Videal_stakednew=Videal_stakedSstakedsSstakedV_{ideal\_staked}^{new} = V_{ideal\_staked} * \frac{S_{staked}-s}{S_{staked}}

_calculate_values()

The core function of the LT contract is the _calculate_values(p_o).

The function updates local state, based on LEVAMM changes, charges admin fees, and maintains the fee splitting invariant.

Some confusion might arise when looking at the price oracle pop_o variable, so we must clarify:

  • AMM uses pop_o as the price of an LP (BTC/crvUSD) to the USD;
  • LT uses pop_o as the price of an asset (BTC) to the USD.

Let’s omit fee splitting-related logic for now and see what happens in code:

@internal
@view
def _calculate_values(p_o: uint256) -> LiquidityValuesOut:
    # struct LiquidityValues:
	  #  admin: int256
	  #  total: uint256
	  #  ideal_staked: uint256
	  #  staked: uint256
    prev: LiquidityValues = self.liquidity
    staker: address = self.staker
    staked: int256 = 0
    if staker != empty(address):
        staked = convert(self.balanceOf[self.staker], int256)
    supply: int256 = convert(self.totalSupply, int256)
	  # staked is guaranteed to be <= supply

		# calculating admin fee
    f_a: int256 = convert(
    10**18 - (10**18 - self._min_admin_fee()) * isqrt(convert(10**36 - staked * 10**36 // supply, uint256)) // 10**18,
    int256)
 
    cur_value: int256 = convert((staticcall self.amm.value_oracle()).value * 10**18 // p_o, int256)
    prev_value: int256 = convert(prev.total, int256)
    value_change: int256 = cur_value - (prev_value + prev.admin)

The cur_value=VAMMpocur\_value = \frac{V_{AMM}}{p_o} (and other values in this context) denotes the LEVAMM value in depositable asset (e.g. BTC: USD/USDBTC=BTCUSD/\frac{USD}{BTC} = BTC).

Note that value variables are int256, so value_change can be negative when losses occur (which is normal).

		...
    dv_use_36: int256 = ... # value delta
    prev.admin += (value_change - dv_use_36 // 10**18)
		new_total_value_36: int256 = max(prev_value * 10**18 + dv_use_36, 0)
		...
		return LiquidityValuesOut(
        admin=prev.admin,
        total=convert(new_total_value_36 // 10**18, uint256),
        ...
    )

Pretty simple so far:

  1. Get current value from LEVAMM (self.amm.value_oracle()).
  2. Calculate the value change.
  3. Charge the admin fee on the value change.
  4. Calculate the new total value.
  5. Return an updated state.

Now, let’s dive into the fee-splitting logistics.

Per design, staked shares must not receive yield from trading, but rather from YB token (governance token) emissions. As stated before, staked shares must suffer losses.

    # current staked value
    v_st: int256 = convert(prev.staked, int256)
    # maximum staked value
    v_st_ideal: int256 = convert(prev.ideal_staked, int256)
    
    dv_use_36: int256 = 0 # value delta
    # v_st_ideal == v_st if no loss
    v_st_loss: int256 = max(v_st_ideal - v_st, 0) # staked loss
    if staked >= MIN_STAKED_FOR_FEES:
        if value_change > 0:
            # Admin fee is only charged once the loss if fully paid off
            v_loss: int256 = min(value_change, v_st_loss * supply // staked)
            # if v_loss = value_change, then we cant cover the loss yet
            # hence dv_use_36 = v_loss * 10**18
            # if there is no loss, then v_loss = 0
            # hence dv_use_36 = value_change * (10**18 - f_a) - charging admin fees
            dv_use_36 = v_loss * 10**18 + (value_change - v_loss) * (10**18 - f_a)
        else:
            # Admin doesn't pay for value loss
            dv_use_36 = value_change * 10**18
    else:
        # If stakeda part is small - positive admin fees are charged on profits and negative on losses
        dv_use_36 = value_change * (10**18 - f_a)
    
    prev.admin += (value_change - dv_use_36 // 10**18)

If enough tokens are staked:

  1. If there is a profit, we don't charge admin fees until the loss is covered (if any).
  2. If there is a loss, then there is no loss for the admin.

On the contrary, if not enough is staked, the admin shares profit/loss.

The constant MIN_STAKED_FOR_FEES is set at 10**16, which represents a relatively small amount. Typically, the admin will not incur direct losses due to this logic. This approach is implemented to prevent divisions by small values in the calculation of v_st_loss * supply // staked.

If loss is persistent, users will socialize admin losses regardless, more on that in the Withdrawals section.

    # dv_s is guaranteed to be <= dv_use
    # if staked < supply (not exactly 100.0% staked) - dv_s is strictly < dv_use
    # how much of delta is applicable to staked 
    dv_s_36: int256 = self.mul_div_signed(dv_use_36, staked, supply)
    # reduce the staked value if negative, otherwise increase must be limited
    if dv_use_36 > 0:
		    # the staked value (new_staked_value_36) can't surpass ideal
		    # we limit the change by max(v_st_ideal - v_st, 0)
        dv_s_36 = min(dv_s_36, v_st_loss * 10**18)

    # new_staked_value is guaranteed to be <= new_total_value
    new_total_value_36: int256 = max(prev_value * 10**18 + dv_use_36, 0)
    new_staked_value_36: int256 = max(v_st * 10**18 + dv_s_36, 0)

The code above implements restrictions on the staked value, which is enforced by the invariant staked_value <= staked_ideal. If the market has never suffered a loss, this would just enforce the said invariant; however, if a loss occurred beforehand, we must replenish it.

    # Solution of:
    # staked - token_reduction       new_staked_value
    # -------------------------  =  -------------------
    # supply - token_reduction         new_total_value
    #
    # the result:
    #                      new_total_value * staked - new_staked_value * supply
    # token_reduction  =  ------------------------------------------------------
    #                               new_total_value - new_staked_value
    #
    # When eps = (supply - staked) / supply << 1, it comes down to:
    # token_reduction = value_change / total_value * (1.0 - min_admin_fee) / sqrt(eps) * supply
    # So when eps < 1e-8 - we'll limit token_reduction

    # If denominator is 0 -> token_reduction = 0 (not a revert)

    token_reduction: int256 = new_total_value_36 - new_staked_value_36  # Denominator
    token_reduction = self.mul_div_signed(new_total_value_36, staked, token_reduction) - self.mul_div_signed(new_staked_value_36, supply, token_reduction)

    max_token_reduction: int256 = abs(value_change * supply // (prev_value + value_change + 1) * (10**18 - f_a) // SQRT_MIN_UNSTAKED_FRACTION)

    # let's leave at least 1 LP token for staked and for total
    if staked > 0:
        token_reduction = min(token_reduction, staked - 1)
    if supply > 0:
        token_reduction = min(token_reduction, supply - 1)
    # But most likely it's this condition to apply
    if token_reduction >= 0:
        token_reduction = min(token_reduction, max_token_reduction)
    else:
        token_reduction = max(token_reduction, -max_token_reduction)
    # And don't allow negatives if denominator was too small
    if new_total_value_36 - new_staked_value_36 < 10**4 * 10**18:
        token_reduction = max(token_reduction, 0)
    
    # Supply changes each time:
    # value split reduces the amount of staked tokens (but not others),
    # and this also reduces the supply of LP tokens

    return LiquidityValuesOut(
        admin=prev.admin,
        total=convert(new_total_value_36 // 10**18, uint256),
        ideal_staked=prev.ideal_staked,
        staked=convert(new_staked_value_36 // 10**18, uint256),
        staked_tokens=convert(staked - token_reduction, uint256),
        supply_tokens=convert(supply - token_reduction, uint256),
        token_reduction=token_reduction
    )

The LP tokens of the YB market are value-accruing tokens; hence, every positive yield is shared among all the holders. But by the fee splitting design, the staked shares must not receive trading fees. This is solved by introducing token_reduction mechanic, which is used to reduce the amount of LP shares of a staker. Virtually, staker still receives a positive yield, but their shares are cut.

SstakedδsStotalδs=VstakedVtotalδs=SstakedVtotalStotalVstakedVtotalVstaked\frac{S_{staked}-\delta s} {S_{total} - \delta s} = \frac{V_{staked}}{V_{total}} \\ \delta s = \frac{S_{staked} * V_{total} - S_{total} * V_{staked}}{V_{total} - V_{staked}}

There is also a cap to the reduction in the form of max_token_reduction, which guards the protocol in extreme cases when almost or all user shares are staked.

To get a deeper look into the maximum token reduction, see Appendix.

The _calculate_values() plays a central part in the protocol and must be invoked before any state-utilizing action, which are:

  • preview functions such as preview_deposit(), preview_withdraw() and preview_emergency_withdraw();
  • deposit()
  • withdraw() and emergency_withdraw()
  • pricePerShare()
  • withdraw_admin_fees()
  • transfers if staking is involved

Those are pretty much all the functions the contract has 🙂.

deposit()

The deposit() function is the entry point for liquidity providers.

@external
@nonreentrant
def deposit(assets: uint256, debt: uint256, min_shares: uint256, receiver: address = msg.sender) -> uint256:
    """
		...
    """
    staker: address = self.staker
    assert receiver != staker, "Deposit to staker"

    amm: LevAMM = self.amm
    # taking a loan of stablecoins
    assert extcall STABLECOIN.transferFrom(amm.address, self, debt, default_return_value=True)
    # transfer assets from user
    assert extcall ASSET_TOKEN.transferFrom(msg.sender, self, assets, default_return_value=True)
    # receive cryptopool LP
    lp_tokens: uint256 = extcall CRYPTOPOOL.add_liquidity([debt, assets], 0, amm.address, False)
    # query Asset price (e.g. BTC) 
    p_o: uint256 = self._price_oracle_w()

    supply: uint256 = self.totalSupply
    shares: uint256 = 0

    liquidity_values: LiquidityValuesOut = empty(LiquidityValuesOut)
    # update the state if not the first deposit
    if supply > 0:
        liquidity_values = self._calculate_values(p_o)
        
    v: OraclizedValue = extcall amm._deposit(lp_tokens, debt)
    value_after: uint256 = v.value * 10**18 // p_o

The interesting part here is that by depositing into the cryptopool, we may change the price of an asset, hence changing the value of the leverage LEMAMM. This is considered a yield for existing users.

    # Value is measured in USD
    # Do not allow value to become larger than HALF of the available stablecoins after the deposit
    # If value becomes too large - we don't allow to deposit more to have a buffer when the price rises
    assert staticcall amm.max_debt() // 2 >= v.value, "Debt too high"

where amm.max_debt() is STABLECOIN.balanceOf(self) + self._debt() - the LEVAMM stablecoin capacity. This check exists to safeguard the market from insolvency in a sudden collateral price shift. If the price rises, the protocol must be able to sell its collateral for stablecoin.

    if supply > 0 and liquidity_values.total > 0:
        supply = liquidity_values.supply_tokens
        self.liquidity.admin = liquidity_values.admin
        value_before: uint256 = liquidity_values.total
        # new total value, must exclude admin fees
        value_after = convert(convert(value_after, int256) - liquidity_values.admin, uint256)
        self.liquidity.total = value_after
        self.liquidity.staked = liquidity_values.staked
        self.totalSupply = liquidity_values.supply_tokens  # will be increased by mint
        if staker != empty(address):
            self.balanceOf[staker] = liquidity_values.staked_tokens
            self._log_token_reduction(staker, liquidity_values.token_reduction)
        # ideal_staked is only changed when we transfer coins to staker
        # shares minted for user
        # this also ensures that value cannot decrease, cuz shares are uint256
        shares = supply * value_after // value_before - supply

This part is responsible for the user shares calculation and the total value adjustment. User receives shares by how much their deposit increased the total position value.

    else:
        # Initial value/shares ratio is EXACTLY 1.0 in collateral units
        # Value is measured in USD
        shares = value_after
        # self.liquidity.admin is 0 at start but can be rolled over if everything was withdrawn
        self.liquidity.ideal_staked = 0         # Likely already 0 since supply was 0
        self.liquidity.staked = 0               # Same: nothing staked when supply is 0
        self.liquidity.total = shares + supply  # 1 share = 1 crypto at first deposit
        self.liquidity.admin = 0                # if we had admin fees - give them to the first depositor; simpler to handle
        if self.balanceOf[staker] > 0:
            log IERC20.Transfer(sender=staker, receiver=empty(address), value=self.balanceOf[staker])
        self.balanceOf[staker] = 0

We can enter this part of code if supply == 0 or liquidity_values.total == 0, which can happen for variety of precision losses. An inconspicuous self.liquidity.total = shares + supply tells us that sometimes we can have a non-zero supply with zero value.

    assert shares + supply >= MIN_SHARE_REMAINDER, "Remainder too small"
    assert shares >= min_shares, "Slippage"

The pool must have at least MIN_SHARE_REMAINDER amount of shares to avoid possible manipulations of pricePerShare(). Check out our audit report to get more context 🙂.

    # mint actual tokens
    self._mint(receiver, shares)
    # Checkpoint a gauge if any to prevent flash loan attacks on reward distribution
    self._checkpoint_gauge()
    log Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares)
    # donate fees to cryptopool rebalancing if any
    self._distribute_borrower_fees(FEE_CLAIM_DISCOUNT)
    return shares

You may notice, that deposit() function does not enforce restrictions on neither assets nor debt input variables. Because the only invariant that plays a role here is value_after >= value_before, which means users can actually arbitrage LEVAMM via this function, and even assets = 0 is allowed :).

Withdrawals

There are two different methods:

  1. The withdraw() is what should be used in normal circumstances. Uses CRYPTOPOOL.remove_liquidity_fixed_out() to receive the exact amount of stables to cover the debt. Cannot be used if AMM is killed state.
  2. The emergency_withdraw() works when killed. Uses CRYPTOPOOL.remove_liquidity() to receive underlying assets in a balanced way. The user can provide additional stablecoins to cover the debt if the pool cannot return enough.

Let's have a look at the base withdraw() function first.

@external
@nonreentrant
def withdraw(shares: uint256, min_assets: uint256, receiver: address = msg.sender) -> uint256:
    """
		...
    """
    assert shares > 0, "Withdrawing nothing"

    staker: address = self.staker
    # this can break staker logic, better to restrict such behaviour
    assert staker not in [msg.sender, receiver], "Withdraw to/from staker"

    assert not (staticcall self.amm.is_killed()), "We're dead. Use emergency_withdraw"

    amm: LevAMM = self.amm
    # update local amm state
    liquidity_values: LiquidityValuesOut = self._calculate_values(self._price_oracle_w())
    supply: uint256 = liquidity_values.supply_tokens
    self.liquidity.admin = liquidity_values.admin
    # self.liquidity.total = liquidity_values.total  no need to update since we will record this value later
    self.liquidity.staked = liquidity_values.staked
    self.totalSupply = supply
		# leave at least MIN_SHARE_REMAINDER or withdraw everything
    assert supply >= MIN_SHARE_REMAINDER + shares or supply == shares, "Remainder too small"

    if staker != empty(address):
        self.balanceOf[staker] = liquidity_values.staked_tokens
        self._log_token_reduction(staker, liquidity_values.token_reduction)

Here withdraw() enforces the invariant, the market must have at least MIN_SHARE_REMAINDER shares or none at all.

    admin_balance: uint256 = convert(max(liquidity_values.admin, 0), uint256)

    withdrawn: Pair = extcall amm._withdraw(10**18 * liquidity_values.total // (liquidity_values.total + admin_balance) * shares // supply)
    assert extcall CRYPTOPOOL.transferFrom(amm.address, self, withdrawn.collateral, default_return_value=True)
    crypto_received: uint256 = extcall CRYPTOPOOL.remove_liquidity_fixed_out(withdrawn.collateral, 0, withdrawn.debt, 0)

The liquidity_values.total does not include admin fees, but total LEVAMM value does.

If there were no admin fees at all, then SStotal\frac{S}{S_{total}} is what we would withdraw from the LEVAMM, but thats not the case here, so we must rescale the fraction with the VtotalVtotal+Vadmin\frac{V_{total}}{V_{total}+V_{admin}}.

The fraction that we withdraw from the AMM :

fracno_admin=VtotalVtotal+VadminSStotalfrac_{no\_admin}=\frac{V_{total}}{V_{total}+V_{admin}} * \frac{S}{S_{total}}

We don’t account for negative admin value Vadmin>0V_{admin} > 0.

If users want to withdraw from the market after some losses have occurred, they will socialize the admin losses too.

    self._burn(msg.sender, shares)  # Changes self.totalSupply
    # proportionally decrease total value
    self.liquidity.total = liquidity_values.total * (supply - shares) // supply
    if liquidity_values.admin < 0:
		    # socializing here
        # If admin fees are negative - we are skipping them, so reduce proportionally
        self.liquidity.admin = liquidity_values.admin * convert(supply - shares, int256) // convert(supply, int256)

As said before, the emergency_withdraw() offers another withdrawal path for users. It can be called in killed state, as well as in normal.

@external
@nonreentrant
def emergency_withdraw(shares: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> (uint256, int256):
    """
		...
    """
    staker: address = self.staker
    assert staker not in [owner, receiver], "Withdraw to/from staker"

    supply: uint256 = 0
    lv: LiquidityValuesOut = empty(LiquidityValuesOut)
    amm: LevAMM = self.amm
    killed: bool = staticcall amm.is_killed()

    if killed:
        # If killed:
        admin: address = self.admin
        if admin.is_contract:
            if msg.sender == staticcall Factory(admin).emergency_admin():
                # Only emergency admin is allowed to withdraw for others, but then only transfer to themselves
                assert receiver == owner, "receiver"
            else:
                # If sender is a different smart contract - it must be the owner
                assert owner == msg.sender, "owner"
        else:
            if msg.sender == admin:
                # If admin is EOA - it can withdraw for receivers when killed, but only send to themselves
                assert receiver == owner, "receiver"
            else:
                # If EOA caller is not admin - it must be the owner
                assert owner == msg.sender, "owner"
				# do not update amm state if is paused
        supply = self.totalSupply

    else:
        # If not killed - only owner is allowed to work with their LP
        assert owner == msg.sender, "Not killed"

        lv = self._calculate_values(self._price_oracle_w())
        supply = lv.supply_tokens
        self.liquidity.admin = lv.admin
        self.liquidity.total = lv.total
        self.liquidity.staked = lv.staked
        self.totalSupply = supply
        if staker != empty(address):
            self.balanceOf[staker] = lv.staked_tokens
            self._log_token_reduction(staker, lv.token_reduction)

Here, YieldBasis introduces an unusual decision to allow DAO withdrawals of user positions during the killed state. This is an additional safety measure, allowing rescue of funds by the DAO itself, if swift action is required. It is somehow convenient for users as DAO will cover the required debt.

Another difference in emergency withdrawal is the lack of state updates (_calculate_values() calls) if LEVAMM is killed.

    # leave at least MIN_SHARE_REMAINDER or withdraw everything
    assert supply >= MIN_SHARE_REMAINDER + shares or supply == shares, "Remainder too small"

    frac: uint256 = 10**18 * shares // supply
    frac_clean: int256 = convert(frac, int256)
    # if admin fees are positive, then rescale amm fraction
    if lv.admin > 0 and lv.total != 0:
        frac = frac * lv.total // (convert(lv.admin, uint256) + lv.total)
    
    withdrawn_levamm: Pair = extcall amm._withdraw(frac)

The logic of LEVAMM withdrawal is the same for both LT withdrawal methods: leave some value for admin fees, socialize losses otherwise.

    # transfer LP tokens from AMM
    assert extcall CRYPTOPOOL.transferFrom(amm.address, self, withdrawn_levamm.collateral, default_return_value=True)
    # withdraw LP tokens in a proportional way from the cryptopool
    withdrawn_cswap: uint256[2] = extcall CRYPTOPOOL.remove_liquidity(withdrawn_levamm.collateral, [0, 0])
    # remove_liquidity does not enforce required amount of stable return
    # the difference of actual stablecoins received from the debt owed to the amm
    stables_to_return: int256 = convert(withdrawn_cswap[0], int256) - convert(withdrawn_levamm.debt, int256)

    self._burn(owner, shares)

		# decreasing total value and socializing losses
    self.liquidity.total = self.liquidity.total * (supply - shares) // supply
    if self.liquidity.admin < 0 or killed:
        self.liquidity.admin = self.liquidity.admin * (10**18 - frac_clean) // 10**18
	
		# cryptopool returned more stables than debt owed
    if stables_to_return > 0:
        assert extcall STABLECOIN.transfer(receiver, convert(stables_to_return, uint256), default_return_value=True)
    # not enough stables to cover the debt, user must provide additional value
    elif stables_to_return < 0:
        assert extcall STABLECOIN.transferFrom(msg.sender, self, convert(-stables_to_return, uint256), default_return_value=True)
    assert extcall STABLECOIN.transfer(amm.address, withdrawn_levamm.debt, default_return_value=True)
    assert extcall ASSET_TOKEN.transfer(receiver, withdrawn_cswap[1], default_return_value=True)
    
    if not killed:
        self._checkpoint_gauge()

    return (withdrawn_cswap[1], stables_to_return)

The main difference in withdrawal methods is the interactions with the underlying cryptopool.

The regular withdraw() enforces the stablecoin amount withdrawn from the cryptopool via remove_liquidity_fixed_out(), which might reduce the assets received by the user, but we will always cover the debt. Caveat is, remove_liquidity_fixed_out will not always work for reasons related to the cryptopool implementation ( D and y newton calculations, for example).

The remove_liquidity() on the other hand is more reliable, as it returns both coins in proportion and does not require additional calculations within the pool. Sometimes, returned stablecoins are not enough to cover the debt; for that reason emergency_withdraw() allows users providing additional amount.

allocate_stablecoins()

This method is required for stablecoin allocations to the LEVAMM contract, which later used to give out loans to users.

@external
@nonreentrant
def allocate_stablecoins(limit: uint256 = max_value(uint256)):
    """
    @notice This method has to be used once this contract has received allocation of stablecoins
    @param limit Limit to allocate for this pool from this allocator. Max uint256 = do not change
    """
    allocator: address = self.admin
    allocation: uint256 = limit
    # how much is already allocated to the amm
    allocated: uint256 = self.stablecoin_allocated
    if limit == max_value(uint256):
		    # do not change the target
		    # this execution path is not guarded by role
        allocation = self.stablecoin_allocation
    else:
        self._check_admin()
        # the allocation target to new limit
        self.stablecoin_allocation = limit

    extcall self.amm.check_nonreentrant()

    if allocation > allocated:
        # Assume that allocator has everything
        assert extcall STABLECOIN.transferFrom(allocator, self.amm.address, allocation - allocated, default_return_value=True)
        self.stablecoin_allocated = allocation
        
    elif allocation < allocated:
		    # cannot retrieve stables, if target amount does not cover whole amm position
		    # this check is similar to one in deposit() function
        lp_price: uint256 = extcall (staticcall self.amm.PRICE_ORACLE_CONTRACT()).price_w()
        assert allocation >= lp_price * (staticcall self.amm.collateral_amount()) // 10**18, "Not enough stables"
       
        # cant transfer what one does not have ¯\_(ツ)_/¯
        to_transfer: uint256 = min(allocated - allocation, staticcall STABLECOIN.balanceOf(self.amm.address))
        allocated -= to_transfer
        assert extcall STABLECOIN.transferFrom(self.amm.address, allocator, to_transfer, default_return_value=True)
        self.stablecoin_allocated = allocated

    log AllocateStablecoins(allocator=allocator, stablecoin_allocation=allocation, stablecoin_allocated=allocated)

An allocator can either provide new funds to the LEVAMM or retrieve previously added stables.
This function ensures that if the limit is lowered, then the LEVAMM contract will have the required amount of stablecoins to keep operations running.

The second execution path allows any user to trigger the stablecoin retrieval. This can happen if the LEVAMM contract has acquired new stablecoins via exchanges or withdrawals.

withdraw_admin_fees()

It is always interesting how protocol manages fees, especially if the admin is DAO :).

@external
@nonreentrant
def withdraw_admin_fees():
    """
    @notice Withdraw admin fees and distribute to the DAO
    """
    admin: address = self.admin
    assert admin.is_contract, "Need factory"
    assert not staticcall self.amm.is_killed(), "Killed"
    extcall self.amm.check_nonreentrant()

		# no fee_receiver is LT state
    fee_receiver: address = staticcall Factory(admin).fee_receiver()
    assert fee_receiver != empty(address), "No fee_receiver"

		# cannot mint shares to staker without logic contained in _transfer
    staker: address = self.staker
    assert fee_receiver != staker, "Staker=fee_receiver"

    v: LiquidityValuesOut = self._calculate_values(self._price_oracle_w())
    assert v.admin >= 0, "Loss made admin fee negative"
    self.totalSupply = v.supply_tokens
    # Mint YB tokens to fee receiver and burn the untokenized admin buffer at the same time
    # fee_receiver is just a normal user
    new_total: uint256 = v.total + convert(v.admin, uint256)
    to_mint: uint256 = v.supply_tokens * new_total // v.total - v.supply_tokens
    self._mint(fee_receiver, to_mint)
    self.liquidity.total = new_total
    self.liquidity.admin = 0
    self.liquidity.staked = v.staked
    if staker != empty(address):
        self.balanceOf[staker] = v.staked_tokens
        self._log_token_reduction(staker, v.token_reduction)

    log WithdrawAdminFees(receiver=fee_receiver, amount=to_mint)

The minting logic is the same as in deposit() methods, imagine admin value is a deposit, and tokens are minted proportionally to the value growth.

pricePerShare()

This function can display the cost of our shares in USD.

@external
@view
def pricePerShare() -> uint256:
    """
    Non-manipulatable "fair price per share" oracle
    """
    v: LiquidityValuesOut = self._calculate_values(self._price_oracle())
    if v.supply_tokens == 0:
        return 10**18
    else:
        return v.total * 10**18 // v.supply_tokens

The docstring says this price is non-manipulatable:

  1. Uses the EMA price oracle of the cryptopool, which in itself is very resilient to sudden price movements.
  2. One can try to manipulate the value or shares, but on deposit or withdrawal, those variables are proportional; hence, the price won't change. Alternatively, an attacker could try using amm.exchange() to inflate the value, without touching the supply, which is not economically feasible, while also considering the safety measures we discussed earlier.

_price_oracle()

Since the deployment, the protocol has migrated to the newer version with a few tweaks. One notable change is in the price oracle used throughout the LT contract.

@internal
@view
def _price_oracle() -> uint256:
    return staticcall CRYPTOPOOL.price_scale() * staticcall self.agg.price() // 10**18

LEVAMM will always use the cryptopool EMA

The previous version used the price_oracle() to fetch the price of an underlying cryptopool. Where:

  • price_oracle() - ema of a cryptopool price.
  • price_scale() - cryptopool liquidity concentration price.

Simulations run by Egorov has showed that price_scale() oracle gives better results, which makes sense, since price_scale() represents the current effective center of liquidity and valuation.

From a security perspective, this sounds good too, as price_scale() follows the price_oracle() in a bounded fashion.

💡For details, see Curve Cryptopool with Donations.”

The future

The LLamaRisk research has shown that the introduction of YB into the ecosystem has increased stress on the crvUSD PegKeepers.
The new (in development) HybridVaults aim to enhance the scaling capabilities of the YB protocol by enabling users to enter markets through their contribution to the crvUSD peg.

The logic behind HybridVaults is somewhat simple and nicely explained in yeildbasis X post.

Conclusion

YieldBasis offers innovation in liquidity providing, delivering hold-like exposure that also earns fees. The YieldBasis can be considered a battle-tested protocol, and the team is clearly focused on improving stability and yield.

It’s a thoughtfully engineered system, and it’s worth digging into the mechanics to appreciate the design and its consequences. The protocol is well-documented, but we wanted to contribute to the community by highlighting the elegance of its smart contracts and protocol engineering.

Reference

  1. https://github.com/yield-basis/yb-core
  2. https://github.com/yield-basis/yb-paper
  3. https://docs.yieldbasis.com/
  4. https://github.com/statemindio/public-audits/tree/main/Yield Basis

Appendix

A deeper look into the maximum token reduction

Let's have a closer look at what can happen in edge cases:

δs=SstakedVtotalStotalVstakedVtotalVstaked==StotalSstakedStotalVtotalVstakedVtotalVstaked\delta s = \frac{S_{staked} * V_{total} - S_{total} * V_{staked}}{V_{total} - V_{staked}} = \\ = S_{total} *\frac{\frac{S_{staked}}{S_{total} } * V_{total} - V_{staked}}{V_{total} - V_{staked}}

If sharesstakedsharestotalshares_{staked} \rightarrow shares_{total}, then we will have

δsStotalVtotalVstakedVtotalVstaked=Stotal\delta s \approx S_{total} * \frac{{V_{total} - V_{staked}}}{{V_{total} - V_{staked}}} = S_{total}

which will nuke all existing shares, breaking the market (considering VtotalVstakedV_{total} \neq V_{staked}). To prevent such misfortune, the cap was introduced in the form of the following formula:

δsmax=SΔVVtotal(1fa)epsmin\delta s_{max} = S *\frac{|\Delta V|}{V_{total}} * \frac{(1-f_a)}{\sqrt{eps_{min}}}

Let's digest how this formula limits the reduction impact. Have a look at the original formula:

δs=StotalSstakedStotalVtotalVstakedVtotalVstaked==StotalfVtotalVstakedVtotalVstaked\delta s = S_{total} *\frac{\frac{S_{staked}}{S_{total} } * V_{total} - V_{staked}}{V_{total} - V_{staked}} = \\ = S_{total} *\frac{f * V_{total} - V_{staked}}{V_{total} - V_{staked}}

where:

  • f=sharesstakedsharestotalf = \frac{shares_{staked}}{shares_{total}}
  • eps=(sharestotalsharesstaked)sharestotal=1feps = \frac{(shares_{total} - shares_{staked})}{shares_{total}} = 1 - f

To limit the δs\delta s effect, we put an upper bound on the numerator and a lower bound on the denominator.

Because token reduction enforces the proportion SstakedStotal=VstakedVtotal\frac{S_{staked}}{S_{total}} = \frac{V_{staked}}{V_{total}}, after the last token reduction, we have useful equality Vtotalprevf=VstakedprevV_{total}^{prev} * f = V_{staked}^{prev} , then for the numerator:

fVtotalVstaked=f(Vtotalprev+dv_use)Vstakedprevdv_s==fdv_usedv_sf * V_{total} - V_{staked} = f*(V_{total}^{prev} + dv\_use) - V_{staked}^{prev} - dv\_s = \\ = f*dv\_use - dv\_s

From here, we can set an upper bound:

fdv_usedv_sfdv_usefΔV(1fa)ΔV(1fa)|f*dv\_use - dv\_s| \leq f*|dv\_use| \leq f*|\Delta V|*(1-f_a) \leq |\Delta V|*(1-f_a)

And a lower bound for the denominator:

VtotalVstakedVtotalepsV_{total}-V_{staked} \geq V_{total} * eps

But, actually, the square root of epseps is used, which makes lower-bound a bit larger in practice, which makes the lower bound shrink slower to get even more restricted maximum reduction.
Another safety measure is using epsmin\sqrt{eps_{min}} instead of dynamic eps\sqrt{eps}, by this we set an absolute minimum of the denominator, enabling a hard limit of reduction.

Share this article
More from blog

Smart contract audit and blockchain security