Deposit and withdraw
This page tells an integrator how to deposit to and withdraw from LT. It gives the signatures, the calculation of debt, the preview functions, the error strings and the events.
LT.vy is ERC-4626-like, not ERC-4626-compliant:
- It emits the ERC-4626
DepositandWithdrawevents. - Its
deposittakes an explicitdebtargument. - It does not implement
convertToAssets,convertToShares,mint,redeem,previewMintorpreviewRedeem. - It implements
IERC20.
LT allows ERC-20 transfers to and from the staker (LT.staker()). These transfers move value between the unstaked and staked buckets. Only three functions reject the staker:
depositreverts ifreceiveris the staker.withdrawreverts if the caller orreceiveris the staker.emergency_withdrawreverts ifownerorreceiveris the staker.
For ERC-4626 accounting, use the LiquidityGauge (the staker). On the gauges of markets 3 and later, previewMint is not callable. On the gauges of markets 0 to 2, it is callable.
For the onchain sequence of each call, see Action Flow: Deposit and Action Flow: Withdraw.
Deposit
Signature
def deposit(
assets: uint256,
debt: uint256,
min_shares: uint256,
receiver: address = msg.sender
) -> uint256
assets: amount of the volatile asset (for example WBTC) that you deposit.debt: amount of crvUSD that the leveraged AMM (LEVAMM) lends to the deposit.LTadds[debt, assets]to the Curve pool. On a pool without an oracle policy, use the asset value:assets * pool.price_oracle() // 10**asset_decimals. On a pool that runs the oracle-report policy,debtmust match the LEVAMM book ratio withinratio_tolerance. See Compute debt on a pool with an oracle policy.min_shares: the minimum shares to receive. This protects against slippage and sandwich trades.receiver: the address that gets the yb-LP shares. It must not beLT.staker(). For an atomic deposit and stake, useStakeZap.deposit_and_stake.
Compute debt on a pool without an oracle policy
pool.price_oracle() is the Curve pool's internal exponential moving average (EMA) price, in crvUSD per whole asset with 18 decimals. LTMigrator uses this price only when it moves a position between two different Curve pools. On the same pool, it keeps the debt structure of the position. HybridVault.assets_for_crvusd() uses price_oracle only as a sizing ratio. CryptopoolLPOracle.price() returns the LP-token price in USD, not the asset price. Do not use it for debt.
debt = assets * pool.functions.price_oracle().call() // 10**asset_decimals
Example: an 18-decimal asset, assets = 1e18, and price_oracle() = 100_000e18 give debt = 100_000e18 crvUSD. The deposit adds both the asset and the borrowed crvUSD to the Curve pool. The borrowed leg is about equal to the asset value. The LEVAMM then holds LP worth about twice the debt (leverage 2).
This rule applies to these pools:
- Pools with no policy hook (older Twocrypto code).
- Pools that run a policy other than
YBOraclizedPolicy. - A pool whose
YBOraclizedPolicyis killed.
To see which policy a pool runs, see Contract addresses or call POLICY() on the pool.
Compute debt on a pool with an oracle policy
A pool that runs YBOraclizedPolicy calls the policy during add_liquidity, unless the policy is killed. The policy checks each LT deposit before the LEVAMM books it (YBOraclizedPolicy._update_lt_gate):
added_debt = growth of the pool's crvUSD balance (= debt; the pool admin_fee is 0)
added_lp = growth of pool.user_supply() (= LP minted by add_liquidity)
proportional = added_lp * AMM.get_debt() // AMM.collateral_amount()
revert "LT deviation" if |added_debt - proportional| * 1e18 > proportional * ratio_tolerance
ratio_tolerance is at most 10 bp (1e15). To read it, call policy.config(). The check does not apply when the LEVAMM book is empty (collateral_amount() == 0). The check does not apply to withdrawals, swaps and donations.
A debt from the asset price misses the proportional debt by the LEVAMM's distance from 50% debt-to-value. For example, suppose that the LEVAMM debt-to-value is 50.175%, which is 0.35% above 50% in relative terms. A debt from the asset price is then about 35 bp below the proportional debt, and the deposit reverts.
LT.preview_deposit does not run this check. A debt that fails the check still returns a share estimate. The check also applies to HybridVault.deposit and LTMigrator, which call LT.deposit with a debt that they compute.
To compute debt:
- Read
pool.POLICY(). If the call reverts (older Twocrypto code), use the price rule in the section before. - If
POLICY()is the zero address, use the price rule. - If
POLICY()is aYBTwocryptoPolicy, use the price rule. To identify it, compare the address with the table in Contract addresses. AYBTwocryptoPolicyhas nois_killed()orFEED_ID()function, so these calls revert. - Read
policy.is_killed(). If it isTrue, use the price rule. - Read
amm.get_debt()andamm.collateral_amount(). - Solve
debt = calc_token_amount([debt, assets], True) * get_debt() // collateral_amount()with the function that follows. - Simulate
lt.deposit(...)witheth_callin the most recent block. - Send the transaction.
# Test on a mainnet fork before use.
def policy_debt(pool, amm, assets, asset_decimals):
book_debt = amm.functions.get_debt().call()
book_coll = amm.functions.collateral_amount().call()
def gap(d): # deposit debt minus proportional debt
lp = pool.functions.calc_token_amount([d, assets], True).call()
return d - lp * book_debt // book_coll
d0 = assets * pool.functions.price_scale().call() // 10**asset_decimals
d1 = d0 * 101 // 100
for _ in range(6): # secant steps; gap(d) is close to linear in d
g0, g1 = gap(d0), gap(d1)
if g1 == 0 or g1 == g0:
break
d0, d1 = d1, d1 - g1 * (d1 - d0) // (g1 - g0)
return d1
Interest accrues on get_debt() between the quote and the block of inclusion. The tolerance absorbs a short delay. If the deposit reverts with LT deviation, compute debt again in the most recent block.
Call pattern
# 1. Compute debt (see the two sections before)
debt = deposit_debt(pool, amm, assets, asset_decimals) # defined in the Developer quickstart
# 2. Preview the shares (reverts "Debt too high" over the deposit cap)
shares_estimate = lt.functions.preview_deposit(assets, debt).call()
# With raise_overflow=False, preview_deposit skips the cap check and returns the share estimate.
# LTMigrator and HybridVault use this form.
# 3. Approve, simulate, deposit
min_shares = shares_estimate * 9_990 // 10_000
asset.functions.approve(lt.address, assets).transact({"from": user})
lt.functions.deposit(assets, debt, min_shares, receiver).call({"from": user})
shares = lt.functions.deposit(assets, debt, min_shares, receiver).transact({"from": user})
Error modes
| Error | Cause |
|---|---|
"Deposit to staker" | receiver == LT.staker(). Use another receiver, or use StakeZap. |
"Debt too high" | The LEVAMM value after the deposit exceeds AMM.max_debt() / 2. Deposit less, or wait for more crvUSD allocation. |
"Unsafe min" | After the deposit, LEVAMM debt is below 1/16 of the collateral value. |
"Unsafe max" | After the deposit, LEVAMM debt is above 8.5/16 of the collateral value. |
"LT deviation" | Pool runs YBOraclizedPolicy and the policy is not killed: debt differs from the proportional debt by more than ratio_tolerance. The pool policy raises it inside add_liquidity. |
"Slippage" | shares < min_shares. Quote again with preview_deposit and send again. |
"Remainder too small" | Total supply after the mint would be below MIN_SHARE_REMAINDER (1e6). Deposit more. |
"!wl" | The Curve pool LP allowlist is on and does not list the LT. |
"slippage", "donation above cap!" (Curve pool) | The borrower-fee donation at the end of the call failed. Try again later. See LT: distribute_borrower_fees. |
Gas
Gas use depends on the pool and the policy. Estimate it with eth_estimateGas or a simulation.
Withdraw
Signature
def withdraw(
shares: uint256,
min_assets: uint256,
receiver: address = msg.sender
) -> uint256
Returns the amount of the asset that receiver gets.
If the pool has an active oracle policy, the policy returns fallback_fee for every liquidity operation. LT.withdraw uses remove_liquidity_fixed_out, which charges fallback_fee / 2 × Sdiff / S of the LP amount, plus NOISE_FEE (0.1 bp). Sdiff / S measures the imbalance of the withdrawal: 0 when balanced, 1 for one coin. The native fee curve does not apply. preview_withdraw includes this fee.
Call pattern
# If staked, unstake first (redeem gauge shares, not a computed asset amount)
staked = gauge.functions.balanceOf(user).call()
if staked > 0:
gauge.functions.redeem(staked, user, user).transact({"from": user})
# Quote the redemption (includes TRD)
expected_assets = lt.functions.preview_withdraw(shares).call()
min_assets = expected_assets * 9_990 // 10_000
# Withdraw with a slippage bound
assets = lt.functions.withdraw(shares, min_assets, receiver).transact({"from": user})
Error modes
| Error | Cause |
|---|---|
"Withdrawing nothing" | shares == 0. |
"Withdraw to/from staker" | msg.sender or receiver equals LT.staker(). Unstake from the gauge first. |
"We're dead. Use emergency_withdraw" | AMM.is_killed() == True. Use emergency_withdraw; repay the debt part from your wallet. |
"Slippage" | crypto_received < min_assets. Quote again and send again. |
"Remainder too small" | Before the burn, supply minus shares would be below MIN_SHARE_REMAINDER. Withdraw less, or withdraw all remaining shares. |
"slippage", "donation above cap!" (Curve pool) | The borrower-fee donation at the end of the call failed. Try again later. See LT: distribute_borrower_fees. |
preview_withdraw and pricePerShare
The two functions use different reference prices:
pricePerShare()returnsv.total × 1e18 / v.supply_tokensfrom_calculate_valuesat the oracle price. On markets 3 to 10, the oracle price isprice_scaletimes the crvUSD price. On markets 0 to 2, it is the pool'sprice_oracletimes the crvUSD price.pricePerShare()is the fundamental value per share and does not include the temporary redemption discount (TRD). For a position, multiply byshares / 1e18.preview_withdraw(shares)runscalc_withdraw_fixed_outagainst the live Curve pool state. It includes TRD.
TRD = preview_withdraw(1e18) / pricePerShare() − 1 for one share. For a position: preview_withdraw(shares) × 1e18 / (pricePerShare() × shares) − 1. TRD is negative during a discount and positive during a premium. See Temporary Redemption Discount.
Gas
Gas use depends on the pool and the policy. Estimate it with eth_estimateGas or a simulation.
Staked positions
To withdraw yb-LP that is staked in LiquidityGauge, use one of the two procedures.
With StakeZap:
- Approve
StakeZapfor the gauge shares. - Call
StakeZap.withdraw_and_unstake(gauge, shares, min_assets, receiver).sharesis the number of gauge shares.
With two calls:
- Call
gauge.redeem(shares, receiver, owner). The gauge sends yb-LP toreceiver. Claim accrued rewards separately. - Call
LT.withdrawas described before.
Use redeem with the share balance. Do not call withdraw(previewRedeem(balance), …): withdraw rounds the burnt shares up and can revert.
Preview reliability
The preview functions are view calls on the current state. In the same block and with the same state, preview_withdraw matches execution. Across blocks the state can change: price_scale moves, other users trade, deposit or withdraw.
preview_deposit does not check the oracle policy deposit ratio. Simulate deposit with eth_call before you send it.
For flows that are sensitive to maximal extractable value (MEV):
- Quote in the simulation.
- Set tight
min_sharesormin_assetsbounds. - Quote again if large trades on the pool are pending.
Events
| Event | When |
|---|---|
Deposit(sender indexed, owner indexed, assets, shares) | Every deposit |
Withdraw(sender indexed, receiver indexed, owner indexed, assets, shares) | Every withdraw |
Transfer(sender indexed, receiver indexed, value) | ERC-20 transfers, mints and burns of yb-LP, and staked-bucket changes |
DistributeBorrowerFees(sender indexed, amount, min_amount, discount) | When LT donates borrower interest to the Curve pool |
Related
- Core: LT: full contract reference.
- Pool policy: fee and
price_scalepolicies on the Curve pools. - Action Flow: Deposit: what
LTdoes onchain. - Action Flow: Withdraw: the onchain withdraw sequence.
- Quote & Route: shared preview patterns.
- Error codes: full error-string catalog.
- Protocol: Temporary Redemption Discount