Skip to main content

Stake and unstake flow

A stake moves yb-LP from a wallet into the gauge (LiquidityGauge) of the market. LT, the market's vault contract, treats the gauge as its staker. LT splits the value of the market into buckets: staked, unstaked, and admin fees. Every transfer into or out of the gauge moves value between the unstaked and staked buckets of LT. Staking does not touch the Curve pool, so a pool policy has no effect on these flows. For how LT allocates a value change between the buckets, read _calculate_values() in LT.

Prerequisites​

Check these approvals before you call the gauge:

  • Before you stake, approve the gauge on LT for assets. The gauge pulls the yb-LP with LT.transferFrom.
  • To unstake the gauge shares of another owner, the caller needs an allowance on the gauge shares of owner.

Stake​

The following figure shows the calls of a stake through LiquidityGauge.deposit:

Sequence of calls in a stake: the user calls deposit on the gauge; the gauge calls LT.checkpoint_staker_rebase, checkpoints the receiver's reward integrals, and pulls yb-LP with LT.transferFrom, which raises staked and ideal_staked in LT; the gauge then mints gauge shares to the receiver, checks the minimum share supply, and calls GaugeController.emit.Sequence of calls in a stake: the user calls deposit on the gauge; the gauge calls LT.checkpoint_staker_rebase, checkpoints the receiver's reward integrals, and pulls yb-LP with LT.transferFrom, which raises staked and ideal_staked in LT; the gauge then mints gauge shares to the receiver, checks the minimum share supply, and calls GaugeController.emit.

The stake runs these steps in order:

  1. The user calls LiquidityGauge.deposit(assets, receiver). assets is an amount of yb-LP in LT share units, with 18 decimals.
  2. The gauge calls LT.checkpoint_staker_rebase(). The caller is the gauge, LT.staker(). While the LEVAMM is not killed, LT therefore runs _calculate_values at _price_oracle_w(). LT then writes the updated buckets, the total supply, and the LT balance of the gauge.
  3. The gauge checks assets against maxDeposit(receiver) and converts assets to gauge shares.
  4. The gauge runs _checkpoint_user(receiver), which updates the reward integrals of receiver for each reward token. For YB, it reads GaugeController.preview_emissions.
  5. The gauge pulls the yb-LP with LT.transferFrom(user, gauge, assets). In LT._transfer, the transfer goes to the gauge, so LT uses the values from step 2 and does not recompute them. LT then updates the staked bucket:
    • LT raises staked by total × assets / supply_tokens. total is the total position value, and supply_tokens is the share supply after the staker rebase, both from step 2.
    • LT raises ideal_staked in proportion to the LT balance of the gauge. When the gauge holds 10**10 share units or fewer, ideal_staked rises by the same value as staked.
  6. The gauge mints gauge shares to receiver and emits the ERC-4626 Deposit event.
  7. The gauge runs _check_min_shares(). The total gauge share supply must be 0 or at least MIN_SHARES.
  8. The gauge calls GaugeController.emit(), which sends the gauge its accrued YB emissions.

LiquidityGauge.mint(shares, receiver) takes a gauge share amount instead and follows the same path. Its cap string is "erc4626: mint more than maximum".

Unstake​

The following figure shows the calls of an unstake through LiquidityGauge.redeem:

Sequence of calls in an unstake: the user calls redeem on the gauge; the gauge calls LT.checkpoint_staker_rebase, checkpoints the owner's reward integrals, burns the owner's gauge shares, and sends yb-LP to the receiver with LT.transfer, which lowers staked and ideal_staked in LT; the gauge then checks the minimum share supply and calls GaugeController.emit.Sequence of calls in an unstake: the user calls redeem on the gauge; the gauge calls LT.checkpoint_staker_rebase, checkpoints the owner's reward integrals, burns the owner's gauge shares, and sends yb-LP to the receiver with LT.transfer, which lowers staked and ideal_staked in LT; the gauge then checks the minimum share supply and calls GaugeController.emit.

The unstake runs these steps in order:

  1. The user calls LiquidityGauge.redeem(shares, receiver, owner).
  2. The gauge calls LT.checkpoint_staker_rebase(), as in step 2 of the stake.
  3. The gauge checks shares against maxRedeem(owner), the gauge share balance of owner.
  4. The gauge converts shares to yb-LP and runs _checkpoint_user(owner).
  5. If the caller is not owner, the gauge spends the caller's allowance on the gauge shares of owner. The gauge then burns the gauge shares of owner.
  6. The gauge sends the yb-LP with LT.transfer(receiver, assets). In LT._transfer, the transfer leaves the gauge. LT lowers staked by total × assets / supply_tokens and lowers ideal_staked in the same ratio as the LT balance of the gauge. The gauge emits the ERC-4626 Withdraw event.
  7. The gauge runs _check_min_shares(), as in step 7 of the stake.
  8. The gauge calls GaugeController.emit().

If LT.is_killed() is True, redeem lets the emergency admin withdraw for another owner. The call must use receiver == owner, or it reverts with "receiver".

withdraw(assets, receiver, owner) takes an amount of yb-LP instead of gauge shares and follows the same path. It has no killed-market branch.

While the LEVAMM is not killed, step 2 runs _calculate_values, which reads AMM.value_oracle(). If value_oracle() reverts, the unstake reverts too, and the staked shares stay in the gauge. They can leave when the oracle works again or after the market is killed. After a kill, checkpoint_staker_rebase() does nothing, so the unstake does not read the oracle.

To leave LT after the unstake, follow the Withdraw flow. LT.withdraw reverts with "Withdraw to/from staker" while the shares are still in the gauge.

Stake or unstake in one call​

StakeZap combines a deposit with a stake, or an unstake with a withdrawal, in one transaction. Before you call it, give the zap the approvals in Approvals on the StakeZap page. The zap has two functions:

  • deposit_and_stake(gauge, assets, debt, min_shares, receiver) runs the Deposit flow with the zap as the LT receiver. It then stakes the minted yb-LP for receiver.
  • withdraw_and_unstake(gauge, shares, min_assets, receiver) redeems the gauge shares of the caller to the zap. It then withdraws from LT to receiver.

deposit_and_stake passes debt to LT.deposit, so compute debt as in Compute debt before the call on the Deposit flow page. On a pool that runs the oracle-report policy, a debt from the asset price can revert with "LT deviation". Each call reverts with the strings of the steps it runs.

Events emitted​

A stake or an unstake can emit these events:

  • Gauge: ERC-4626 Deposit or Withdraw, and ERC-20 Transfer of gauge shares.
  • LT: Transfer between the user and the gauge (LT.staker()).
  • LT: Transfer between the gauge and the zero address, when the rebase in step 2 changes the LT balance of the gauge.
  • YB: Transfer from the GaugeController to the gauge, when emit() sends emissions.

Reverts by step​

The tables in this section list only the reverts of these flows. For the full revert list of each gauge function, read the LiquidityGauge reference.

The stake can revert at these steps:

StepRevertCondition
2The revert of the oracleThe LEVAMM is not killed, and AMM.value_oracle() reverts (in LT.checkpoint_staker_rebase)
3"erc4626: deposit more than maximum"assets is above maxDeposit(receiver). The gauge sets this maximum to max_value(uint256)
5No messageThe gauge allowance on LT, or the yb-LP balance of the user, is below assets
7"erc4626: leave MIN_SHARES"The gauge share supply after the call is above 0 and below MIN_SHARES. MIN_SHARES is 10**12 raw units, which is 0.000001 gauge shares at 18 decimals

The unstake can revert at these steps:

StepRevertCondition
2The revert of the oracleThe LEVAMM is not killed, and AMM.value_oracle() reverts (in LT.checkpoint_staker_rebase)
3"erc4626: redeem more than maximum"shares is above the gauge share balance of owner. For withdraw, the string is "erc4626: withdraw more than maximum", and the limit is maxWithdraw(owner)
3"receiver"LT.is_killed() is True, the caller is Factory.emergency_admin(), and receiver ≠ owner
7"erc4626: leave MIN_SHARES"The gauge share supply after the call is above 0 and below MIN_SHARES
  • LT: checkpoint_staker_rebase and the liquidity buckets.
  • LiquidityGauge: the full gauge reference, including rewards.
  • StakeZap: the one-call deposit-and-stake path and its approvals.
  • Watermark and recovery: what ideal_staked means for a staker.
  • Stake yb-LP: the user steps in the app.