Skip to main content

Withdraw flow

LT.withdraw unwinds a position in proportion and sends the volatile asset to the receiver. LT is the market's vault contract. In the same transaction, LT repays the debt of the leveraged AMM (LEVAMM) and burns the shares. Use this trace to set min_assets, predict the events, and explain a short payout.

Prerequisites​

Before you call LT.withdraw, complete these items:

  • Staked shares must leave the gauge before the holder calls LT.withdraw. To unstake, follow Unstake in the Stake and unstake flow.
  • Set min_assets from preview_withdraw. For the call pattern with preview_withdraw and min_assets, read Deposit and withdraw.

Steps​

The following figure shows the calls of one withdrawal, from the holder to the borrower-interest donation at the end:

Sequence of calls in LT.withdraw: LT updates its buckets, takes a pro-rata share of LEVAMM collateral and debt, pulls the LP from the LEVAMM, and removes it from the Curve pool for exactly the debt in crvUSD plus the rest as the asset; the pool calls update_pool_state on its policy if it has one. LT then burns the shares, repays the LEVAMM in crvUSD, sends the asset to the receiver, checkpoints the gauge, and donates any borrower interest.Sequence of calls in LT.withdraw: LT updates its buckets, takes a pro-rata share of LEVAMM collateral and debt, pulls the LP from the LEVAMM, and removes it from the Curve pool for exactly the debt in crvUSD plus the rest as the asset; the pool calls update_pool_state on its policy if it has one. LT then burns the shares, repays the LEVAMM in crvUSD, sends the asset to the receiver, checkpoints the gauge, and donates any borrower interest.

The withdrawal runs these steps in order:

  1. The holder calls LT.withdraw(shares, min_assets, receiver). receiver defaults to the caller. LT checks that shares is not 0, that neither the caller nor receiver is LT.staker(), and that the LEVAMM is not killed.
  2. LT reads the price with _price_oracle_w() and runs _calculate_values to bring the buckets up to date. The buckets are the staked, unstaked, and admin parts of the market value; LT.liquidity() holds their stored values. LT then checks the share remainder: after the burn, the share supply must be 0 or at least MIN_SHARE_REMAINDER.
  3. LT calls AMM._withdraw(frac). frac is the fraction of the LEVAMM position that shares claim. LT excludes a positive admin balance, the admin bucket, from the claim. The LEVAMM removes that fraction of its collateral and debt and returns both amounts.
  4. LT pulls the LP collateral from the LEVAMM and calls remove_liquidity_fixed_out(lp, 0, debt, 0) on the Curve pool. The pool returns exactly debt crvUSD and the rest of the LP value as the asset. On a v3.0.0 pool (markets 7 to 10) with a non-zero POLICY(), the pool calls update_pool_state on the policy. The policy does not check exits.
  5. LT burns the shares and updates the total and admin buckets. It checks the asset amount received against min_assets.
  6. LT sends the crvUSD to the LEVAMM, which repays the debt that step 3 removed. LT then sends the asset to receiver.
  7. LT calls _checkpoint_gauge(), emits Withdraw, and runs _distribute_borrower_fees, as in step 8 of the Deposit flow.

Withdrawal fee​

The removal takes more of one coin than the pool ratio. A yb-LP deposit or withdrawal pays half the pool fee rate on the imbalanced part. preview_deposit and preview_withdraw include this fee.

The fee rate depends on the policy that the pool runs:

  • On a pool with no policy, or one that runs YBTwocryptoPolicy, the fee is the native fee of the pool.
  • On a pool that runs the oracle-report policy (YBOraclizedPolicy), while the policy is not killed, the policy returns fallback_fee for every liquidity operation. fallback_fee is a field of config() on the policy.

The pools of markets 0 to 6 run older pool code without a policy hook. For the fee formula and the fee that each policy sets on a liquidity operation, read get_fee in Pool policy.

Why a withdrawal can return less than its Fundamental Value​

PPS is the price per share, LT.pricePerShare(), with 18 decimals. The Fundamental Value of a position is PPS × shares. Three independent causes can make a withdrawal pay less than PPS × shares:

  • Temporary Redemption Discount (TRD). TRD is the signed gap between Redemption Value (preview_withdraw) and Fundamental Value (pricePerShare()). During a TRD, preview_withdraw(shares) * 10**(18 - asset_decimals) < pricePerShare() * shares // 10**18. The factor scales the raw asset amount to the 18 decimals of PPS.
  • PPS drag. PPS drag is slow or negative Fundamental Value growth because the rebalancing cost outran fee income.
  • Watermark shortfall. The watermark (ideal_staked) is the staked value that the gauge must regain after losses. The gap below it is the watermark shortfall. A watermark shortfall applies to staked positions only; LT.liquidity() shows it as staked < ideal_staked.

For TRD and PPS drag, read Fundamental Value, Redemption Value, and TRD. For how later gains repay a staked loss, read Watermark and recovery.

Events emitted​

A withdrawal can emit these events:

  • LT.Transfer between LT.staker() and the zero address: only when the bucket update in step 2 changes the LT balance of a gauge
  • AMM.RemoveLiquidityRaw(collateral_change, debt_change)
  • Pool.RemoveLiquidityImbalance(provider indexed, lp_token_amount, token_amounts, approx_fee, price_scale): on markets 7 to 10, from remove_liquidity_fixed_out with a non-zero debt (RemoveLiquidityOne if the debt is 0)
  • LT.Transfer(sender, receiver=0x0, value): the burn
  • LT.Withdraw(sender indexed, receiver indexed, owner indexed, assets, shares)
  • AMM.CollectFees(amount, new_supply): in distribute_borrower_fees
  • LT.DistributeBorrowerFees(sender indexed, amount, min_amount, discount): only when LT holds crvUSD to donate

Reverts by step​

A withdrawal can revert at these steps. Each row names the step of the walk-through in which the revert occurs. For the full list of strings, read Error codes.

StepRevertCondition
1"Withdrawing nothing"shares == 0
1"Withdraw to/from staker"msg.sender == staker() or receiver == staker(). Unstake from the gauge first
1"We're dead. Use emergency_withdraw"AMM.is_killed()
2"Remainder too small"Before the burn, supply < MIN_SHARE_REMAINDER + shares and supply ≠ shares. Withdraw less, or withdraw all remaining shares
5"Slippage"crypto_received < min_assets
7"GAS"The first GaugeController.checkpoint call fails and less than 200,000 gas remains for the retry
7"slippage" or "donation above cap!"Markets 7 to 10: the borrower-interest donation at the end of the call fails in the Curve pool (from the Curve pool). The donation runs only when LT holds crvUSD

Gas​

Gas depends on the pool and its policy. To estimate it, call eth_estimateGas or simulate the transaction in the target block.