LT (Leveraged Token)
The source code of the LT.vy contract can be found on GitHub. The contract is written with Vyper version 0.4.3.
Overview
The LT contract is the market’s leveraged-liquidity share token (ERC-20, 18 decimals; symbol yb-asset). Users deposit the asset token (e.g., BTC) while the contract borrows a matching amount of stablecoin liquidity from the market (via its allocation) to mint Curve LP and hand the LP into the LEVAMM. In return, users receive LT shares that represent a pro-rata claim on the AMM position’s value. Withdrawals burn shares and unwind through the AMM and Curve pool back to asset (and any required stablecoin settlement). The contract exposes standard deposit/withdraw flows with slippage guards and a fair, oracle-anchored price-per-share.
LT also manages the yb markets: it owns the LEVAMM instance, forwards interest-rate and fee updates to the AMM, manages the stablecoin allocation to the AMM (allocate_stablecoins), and periodically sweeps borrower interest collected in the AMM into the Curve pool as LP tokens (distribute_borrower_fees).
An optional staker address can be set once to integrate with gauges/rewards, and admin fees are tracked and withdrawable to the protocol’s fee receiver. Safety is enforced with access controls (admin/factory admins), non-reentrancy on state-changing entry points, kill/unkill passthrough to the AMM, and oracle-based checks to keep the position within safe bounds.
Deployed by the Factory: LT instances are cloned from the configured implementation using Vyper’s create_from_blueprint(asset_token, stablecoin, cryptopool, admin_or_factory), then connected to the market’s LEVAMM and pricing oracle during market creation.
Function Documentation
Public Variables
The LT contract exposes several public variables for reading contract state and configuration.
Constants
CRYPTOPOOL_N_COINS(uint256) - Number of coins in the crypto pool (2)FEE_CLAIM_DISCOUNT(uint256) - Fee claim discount (10^16 = 1%)MIN_SHARE_REMAINDER(uint256) - Minimum shares to leave if > 0 (10^6)SQRT_MIN_UNSTAKED_FRACTION(int256) - Minimum unstaked fraction (10^14 = 1e-4)MIN_STAKED_FOR_FEES(int256) - Minimum staked amount for fees (10^16)
Immutable Variables
CRYPTOPOOL(CurveCryptoPool) - Curve crypto pool contract (immutable)STABLECOIN(IERC20) - Stablecoin contract (immutable)ASSET_TOKEN(IERC20) - Asset token contract (immutable)
State Variables
admin(address) - Admin addressamm(LevAMM) - AMM contract addressagg(PriceOracle) - Price aggregator contractstaker(address) - Staker contract addressliquidity(LiquidityValues) - Liquidity state structallowance(HashMap[address, HashMap[address, uint256]]) - ERC20 allowancesbalanceOf(HashMap[address, uint256]) - ERC20 balancestotalSupply(uint256) - Total token supplydecimals(uint8) - Token decimals (18)stablecoin_allocation(uint256) - Stablecoin allocation limitstablecoin_allocated(uint256) - Currently allocated stablecoins
Data Structs
AMMState
collateral(uint256) - LP token amount held as collateraldebt(uint256) - Interest-accrued stablecoin debtx0(uint256) - Virtual balance for maintaining 2x leverage
Pair
collateral(uint256) - LP token amountdebt(uint256) - Stablecoin debt amount
OraclizedValue
p_o(uint256) - Oracle price used for calculationvalue(uint256) - Calculated value in stablecoin terms
LiquidityValues
admin(int256) - Admin fees (can be negative)total(uint256) - Total position valueideal_staked(uint256) - Ideal staked amountstaked(uint256) - Current staked amount
LiquidityValuesOut
admin(int256) - Admin fees (can be negative)total(uint256) - Total position valueideal_staked(uint256) - Ideal staked amountstaked(uint256) - Current staked amountstaked_tokens(uint256) - Staked token balance after reductionssupply_tokens(uint256) - Total supply after reductionstoken_reduction(int256) - Token reduction amount
Events
SetStaker (Emitted when a new staker contract is set)
staker(address) - New staker contract address
WithdrawAdminFees (Emitted when admin fees are withdrawn)
receiver(address) - Address that received the freshly minted yb-LP shares (the factoryfee_receiver)amount(uint256) - Amount of yb-LP minted to the receiver
AllocateStablecoins (Emitted when stablecoins are allocated to the AMM)
allocator(indexed address) - Address that allocated stablecoinsstablecoin_allocation(uint256) - Total allocation limitstablecoin_allocated(uint256) - Currently allocated amount
DistributeBorrowerFees (Emitted when borrower fees are distributed to the Curve pool)
sender(indexed address) - Caller that triggered the distribution (e.g. AMM on swap, LT on deposit/withdraw, or any keeper)amount(uint256) - Amount of crvUSD donated into the Cryptoswap poolmin_amount(uint256) - Minimum LP-token amount accepted (computed from the donation discount andCRYPTOPOOL.lp_price())discount(uint256) - Discount applied to the distribution
Deposit (Emitted when assets are deposited and shares are minted)
sender(address) - Address that initiated the depositowner(address) - Address that receives the sharesassets(uint256) - Amount of assets depositedshares(uint256) - Amount of shares minted
Withdraw (Emitted when shares are burned and assets are withdrawn)
sender(address) - Address that initiated the withdrawalreceiver(address) - Address that receives the assetsowner(address) - Address that owns the sharesassets(uint256) - Amount of assets withdrawnshares(uint256) - Amount of shares burned
SetAdmin (Emitted when the admin address is changed)
admin(address) - New admin address
_calculate_values()
Description:
Internal function that calculates the current liquidity values, including admin fees, token reductions, and position metrics. This is the core function for determining LT token values and managing fee distributions.
Returns:
LiquidityValuesOut - Struct containing detailed position metrics including:
| Field | Type | Description |
|---|---|---|
admin | int256 | Admin fees (can be negative) |
total | uint256 | Total position value |
ideal_staked | uint256 | Ideal staked amount |
staked | uint256 | Current staked amount |
staked_tokens | uint256 | Staked token balance after reductions |
supply_tokens | uint256 | Total supply after reductions |
token_reduction | int256 | Token reduction amount |
| Input | Type | Description |
|---|---|---|
p_o | uint256 | Oracle price for calculations |
_amm_value | uint256 | Optional pre-read AMM value (default 0). When non-zero, the function uses the supplied value instead of calling amm.value_oracle(). emergency_withdraw uses this when the AMM is killed or the oracle has reverted, so the rebase can still complete from a soft-read value. |
📄 View Source Code
@view
def _calculate_values(p_o: uint256, _amm_value: uint256 = 0) -> LiquidityValuesOut:
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
f_a: int256 = convert(
10**18 - (10**18 - self._min_admin_fee()) * isqrt(convert(10**36 - staked * 10**36 // supply, uint256)) // 10**18,
int256)
amm_value: uint256 = _amm_value
if amm_value == 0:
amm_value = (staticcall self.amm.value_oracle()).value
cur_value: int256 = convert(amm_value * 10**18 // p_o, int256)
prev_value: int256 = convert(prev.total, int256)
value_change: int256 = cur_value - (prev_value + prev.admin)
v_st: int256 = convert(prev.staked, int256)
v_st_ideal: int256 = convert(prev.ideal_staked, int256)
# ideal_staked is set when some tokens are transferred to staker address
# Admin fees are earned only when all losses are paid off
dv_use_36: int256 = 0
v_st_loss: int256 = max(v_st_ideal - v_st, 0)
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)
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 staked 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)
# dv_s is guaranteed to be <= dv_use
# if staked < supply (not exactly 100.0% staked) - dv_s is strictly < dv_use
dv_s_36: int256 = self.mul_div_signed(dv_use_36, staked, supply)
if dv_use_36 > 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)
# 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
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)
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
)
deposit()
Description:
Method to deposit assets (e.g., BTC) to receive shares (e.g., yield-bearing BTC). This function implements the ERC4626 deposit standard and handles the complex liquidity calculations.
Returns:
uint256 - Amount of shares minted to the receiver.
Emits:
Deposit - Deposit event with sender, owner, assets, and shares amounts.
| Input | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets to deposit |
debt | uint256 | Amount of debt for AMM to take (approximately BTC * btc_price) |
min_shares | uint256 | Minimal amount of shares to receive (slippage protection) |
receiver | address | Receiver of the shares (defaults to msg.sender) |
📄 View Source Code
@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
assert extcall STABLECOIN.transferFrom(amm.address, self, debt, default_return_value=True)
assert extcall ASSET_TOKEN.transferFrom(msg.sender, self, assets, default_return_value=True)
lp_tokens: uint256 = extcall CRYPTOPOOL.add_liquidity([debt, assets], 0, amm.address, False)
p_o: uint256 = self._price_oracle_w()
supply: uint256 = self.totalSupply
shares: uint256 = 0
liquidity_values: LiquidityValuesOut = empty(LiquidityValuesOut)
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
assert staticcall amm.max_debt() // 2 >= v.value, "Debt too high"
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
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
if staker != empty(address):
self.balanceOf[staker] = liquidity_values.staked_tokens
self._log_token_reduction(staker, liquidity_values.token_reduction)
shares = supply * value_after // value_before - supply
else:
shares = value_after
self.liquidity.ideal_staked = 0
self.liquidity.staked = 0
self.liquidity.total = shares + supply
self.liquidity.admin = 0
if self.balanceOf[staker] > 0:
log IERC20.Transfer(sender=staker, receiver=empty(address), value=self.balanceOf[staker])
self.balanceOf[staker] = 0
assert shares + supply >= MIN_SHARE_REMAINDER, "Remainder too small"
assert shares >= min_shares, "Slippage"
self._mint(receiver, shares)
self._checkpoint_gauge()
log Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares)
self._distribute_borrower_fees(FEE_CLAIM_DISCOUNT)
return shares
withdraw()
Description:
Method to withdraw assets (e.g., BTC) by spending shares (e.g., yield-bearing BTC). This function implements the ERC4626 withdraw standard.
Returns:
uint256 - Amount of assets received by the receiver.
Emits:
Withdraw - Withdrawal event with sender, receiver, owner, assets, and shares amounts.
| Input | Type | Description |
|---|---|---|
shares | uint256 | Shares to withdraw |
min_assets | uint256 | Minimal amount of assets to receive (slippage protection) |
receiver | address | Receiver of the assets (defaults to msg.sender) |
📄 View Source Code
@external
@nonreentrant
def withdraw(shares: uint256, min_assets: uint256, receiver: address = msg.sender) -> uint256:
assert shares > 0, "Withdrawing nothing"
staker: address = self.staker
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
liquidity_values: LiquidityValuesOut = self._calculate_values(self._price_oracle_w())
supply: uint256 = liquidity_values.supply_tokens
self.liquidity.admin = liquidity_values.admin
self.liquidity.staked = liquidity_values.staked
self.totalSupply = supply
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)
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)
self._burn(msg.sender, shares)
self.liquidity.total = liquidity_values.total * (supply - shares) // supply
if liquidity_values.admin < 0:
self.liquidity.admin = liquidity_values.admin * convert(supply - shares, int256) // convert(supply, int256)
assert crypto_received >= min_assets, "Slippage"
assert extcall STABLECOIN.transfer(amm.address, withdrawn.debt, default_return_value=True)
assert extcall ASSET_TOKEN.transfer(receiver, crypto_received, default_return_value=True)
self._checkpoint_gauge()
log Withdraw(sender=msg.sender, receiver=receiver, owner=msg.sender, assets=crypto_received, shares=shares)
self._distribute_borrower_fees(FEE_CLAIM_DISCOUNT)
return crypto_received
preview_deposit()
Description:
Returns the amount of shares which can be obtained upon depositing assets, including slippage. This is a view function that simulates the deposit without executing it.
Returns:
uint256 - Amount of shares that would be minted for the given assets and debt.
| Input | Type | Description |
|---|---|---|
assets | uint256 | Amount of crypto to deposit |
debt | uint256 | Amount of stables to borrow for MMing (approx same value as crypto) |
raise_overflow | bool | If True (default), reverts with "Debt too high" when projected debt would exceed AMM.max_debt() / 2. If False, returns 0 instead — used by LTMigrator and HybridVault for soft quotes. |
📄 View Source Code
@external
@view
@nonreentrant
def preview_deposit(assets: uint256, debt: uint256, raise_overflow: bool = True) -> uint256:
lp_tokens: uint256 = staticcall CRYPTOPOOL.calc_token_amount([debt, assets], True)
supply: uint256 = self.totalSupply
p_o: uint256 = self._price_oracle()
amm: LevAMM = self.amm
amm_max_debt: uint256 = staticcall amm.max_debt() // 2
if supply > 0:
liquidity: LiquidityValuesOut = self._calculate_values(p_o)
if liquidity.total > 0:
v: OraclizedValue = staticcall amm.value_change(lp_tokens, debt, True)
if raise_overflow:
if amm_max_debt < v.value:
raise "Debt too high"
value_after: uint256 = convert(convert(v.value * 10**18 // p_o, int256) - liquidity.admin, uint256)
return liquidity.supply_tokens * value_after // liquidity.total - liquidity.supply_tokens
v: OraclizedValue = staticcall amm.value_oracle_for(lp_tokens, debt)
if raise_overflow:
if amm_max_debt < v.value:
raise "Debt too high"
return v.value * 10**18 // p_o
preview_withdraw()
Description:
Returns the amount of assets which can be obtained upon withdrawing from tokens. This is a view function that simulates the withdrawal without executing it.
Returns:
uint256 - Amount of assets that would be received for the given shares.
| Input | Type | Description |
|---|---|---|
tokens | uint256 | Amount of shares to withdraw |
📄 View Source Code
@external
@view
@nonreentrant
def preview_withdraw(tokens: uint256) -> uint256:
v: LiquidityValuesOut = self._calculate_values(self._price_oracle())
state: AMMState = staticcall self.amm.get_state()
admin_balance: uint256 = convert(max(v.admin, 0), uint256)
frac: uint256 = 10**18 * v.total // (v.total + admin_balance) * tokens // v.supply_tokens
withdrawn_lp: uint256 = state.collateral * frac // 10**18
withdrawn_debt: uint256 = state.debt * frac // 10**18
return staticcall CRYPTOPOOL.calc_withdraw_fixed_out(withdrawn_lp, 0, withdrawn_debt)
set_amm()
Description:
Sets the AMM contract address. This function can only be called once and only by the admin. It also sets the price aggregator from the AMM's price oracle. AMM can only be changed by the admin of this contract or the admin of the Factory.
Returns:
No return value (void function).
| Input | Type | Description |
|---|---|---|
amm | LevAMM | AMM contract address |
📄 View Source Code
@external
@nonreentrant
def set_amm(amm: LevAMM):
self._check_admin()
assert self.amm == empty(LevAMM), "Already set"
assert staticcall amm.STABLECOIN() == STABLECOIN.address
assert staticcall amm.COLLATERAL() == CRYPTOPOOL.address
assert staticcall amm.LT_CONTRACT() == self
self.amm = amm
self.agg = PriceOracle(staticcall (staticcall amm.PRICE_ORACLE_CONTRACT()).AGG())
set_admin()
Description:
Sets a new admin address. This function can only be called by the current admin. It performs sanity checks if the new admin is a contract. New admin can only be changed by the current admin of this contract or the admin of the Factory.
Returns:
No return value (void function).
Emits:
SetAdmin - Admin change event with new admin address.
| Input | Type | Description |
|---|---|---|
new_admin | address | New admin address |
📄 View Source Code
@external
@nonreentrant
def set_admin(new_admin: address):
self._check_admin()
if new_admin.is_contract:
check_address: address = staticcall Factory(new_admin).admin()
check_address = staticcall Factory(new_admin).emergency_admin()
check_address = staticcall Factory(new_admin).fee_receiver()
check_uint: uint256 = staticcall Factory(new_admin).min_admin_fee()
self.admin = new_admin
log SetAdmin(admin=new_admin)
set_rate()
Description:
Sets the interest rate in the AMM. This function can only be called by the admin and forwards the call to the AMM contract. Interest rate can only be changed by the admin of this contract or the admin of the Factory.
Returns:
No return value (void function).
| Input | Type | Description |
|---|---|---|
rate | uint256 | New interest rate |
📄 View Source Code
@external
@nonreentrant
def set_rate(rate: uint256):
self._check_admin()
extcall self.amm.set_rate(rate)
set_amm_fee()
Description:
Sets the trading fee in the AMM. This function can only be called by the admin and forwards the call to the AMM contract. AMM fee can only be changed by the admin of this contract or the admin of the Factory.
Returns:
No return value (void function).
| Input | Type | Description |
|---|---|---|
fee | uint256 | New trading fee |
📄 View Source Code
@external
@nonreentrant
def set_amm_fee(fee: uint256):
self._check_admin()
extcall self.amm.set_fee(fee)
allocate_stablecoins()
Description:
Allocates stablecoins to the AMM for this pool. This method must be used once the contract has received allocation of stablecoins. Allocation can only be handled by the admin of this contract or the admin of the Factory.
Returns:
No return value (void function).
Emits:
AllocateStablecoins - Allocation event with allocator address, allocation amount, and allocated amount.
| Input | Type | Description |
|---|---|---|
limit | uint256 | Limit to allocate for this pool (max uint256 = do not change) |
📄 View Source Code
@external
@nonreentrant
def allocate_stablecoins(limit: uint256 = max_value(uint256)):
allocator: address = self.admin
allocation: uint256 = limit
allocated: uint256 = self.stablecoin_allocated
if limit == max_value(uint256):
allocation = self.stablecoin_allocation
else:
self._check_admin()
self.stablecoin_allocation = limit
extcall self.amm.check_nonreentrant()
if allocation > allocated:
# Bring in only what the allocator can actually transfer
to_transfer: uint256 = min(allocation - allocated, staticcall STABLECOIN.balanceOf(allocator))
assert extcall STABLECOIN.transferFrom(allocator, self.amm.address, to_transfer, default_return_value=True)
allocated += to_transfer
self.stablecoin_allocated = allocated
elif allocation < allocated:
lp_price: uint256 = extcall (staticcall self.amm.PRICE_ORACLE_CONTRACT()).price_w()
# Safe lower limit for deflating the market: 3/4 of LP-priced collateral value.
# Equilibrium leaves a 50% surplus (coefficient 1); the edge case is 1/2; 3/4
# is the conservative cap that still lets governance deallocate.
safe_lower_limit: uint256 = lp_price * (staticcall self.amm.collateral_amount()) * 3 // 4 // 10**18
to_transfer: uint256 = min(
min(allocated - allocation, allocated - safe_lower_limit),
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)
pricePerShare()
Description:
Non-manipulatable "fair price per share" oracle of the yb-asset token.
Returns:
uint256 - Price per share in 1e18 units.
📄 View Source Code
@external
@view
def pricePerShare() -> uint256:
# Short-circuit BEFORE the heavy _calculate_values call when supply is empty.
if self.totalSupply == 0:
return 10**18
else:
v: LiquidityValuesOut = self._calculate_values(self._price_oracle())
return v.total * 10**18 // v.supply_tokens
symbol()
Description:
Returns the token symbol in the format 'yb-asset'. This implements the ERC20 symbol standard.
Returns:
String[32] - Token symbol (e.g., "yb-BTC").
📄 View Source Code
@external
@view
def symbol() -> String[32]:
return concat('yb-', staticcall IERC20Slice(ASSET_TOKEN.address).symbol())
name()
Description:
Returns the token name in the format 'Yield Basis liquidity for asset'. This implements the ERC20 name standard.
Returns:
String[58] - Token name (e.g., "Yield Basis liquidity for BTC").
📄 View Source Code
@external
@view
def name() -> String[58]:
return concat('Yield Basis liquidity for ', staticcall IERC20Slice(ASSET_TOKEN.address).symbol())
emergency_withdraw() and preview_emergency_withdraw()
Description:
The only exit path while the AMM is killed (We're dead. Use emergency_withdraw). Bypasses the oracle-driven _calculate_values rebase: the caller settles debt from their own wallet and the AMM hands back its raw (collateral, debt) pair. Convexity of the bonding curve ensures the worst an attacker can do is lose value, so no min_* slippage parameter is exposed.
Three auth branches, distinguished by whether admin is a contract (factory) and whether the AMM is currently killed:
| Caller | receiver constraint | When allowed |
|---|---|---|
owner of the position | any | Always — full self-service exit. |
emergency_admin (factory only) | receiver == owner | While AMM is killed OR value_oracle() reverts. Can sweep stuck positions but must send proceeds to the position owner. |
admin (factory only) | receiver == owner | Same rules as emergency_admin. |
| Any other contract caller | — | Reverts "owner" unless the caller IS the owner. |
| Any caller while AMM is live | — | Reverts "Not killed". Use the normal withdraw path. |
The return tuple is (asset_out, stables_to_return). If stables_to_return < 0, the contract pulls that much crvUSD from msg.sender via transferFrom to settle the debt leg — approve crvUSD to the LT before calling.
preview_emergency_withdraw(shares) -> (uint256, int256) is the matching view. It does not enforce any auth check — quote the position from any address.
Returns:
(asset_received, stables_to_return): int256 on the second leg so the contract can signal that the caller must bring stables (negative value) versus that they will receive stables back (positive).
| Input | Type | Description |
|---|---|---|
shares | uint256 | yb-LP shares to redeem |
receiver | address | Destination for the asset leg (and stables leg if positive). Default msg.sender. |
owner | address | Position owner whose shares are burned. Default msg.sender. |
withdraw_admin_fees()
Description:
Permissionless trigger that materialises pending admin fees as yb-LP shares and mints them to the factory's fee_receiver. Anyone can call it as long as LT.admin resolves to a factory contract (admin.is_contract must be true and Factory(admin).fee_receiver() must be set). Keepers and MEV searchers run this on a schedule to keep the FeeDistributor topped up.
Mechanism:
- Runs
_calculate_values(self._price_oracle_w())to refresh buckets — admin must be non-negative (Loss made admin fee negative). - Computes
to_mint = supply_tokens × (total + admin) / total − supply_tokens. - Mints
to_mintyb-LP tofee_receiver, setsliquidity.admin = 0, and updatesliquidity.totalaccordingly. - Emits
WithdrawAdminFees(receiver, amount).
Reverts:
| String | Cause |
|---|---|
Need factory | admin is an EOA (no fee_receiver). |
Killed | AMM is killed — wait for unkill or use emergency_withdraw. |
No fee_receiver | Factory exists but fee_receiver is the zero address. |
Staker=fee_receiver | Sanity check: cannot mint to the staker contract. |
Loss made admin fee negative | Admin bucket is currently in deficit. Wait for fees to recover. |
No min_amount or min_assets parameter — the mint is deterministic from the current bucket state.
updated_balances()
Description:
View helper used by LiquidityGauge._total_assets() to compute its strict ERC-4626 totalAssets(). Runs _calculate_values against the current oracle without writing.
Returns:
(supply_tokens, staked_tokens) — post-rebase supply and the staker's post-rebase share count. When totalSupply == 0, returns (0, 0).
Used by portfolio trackers and LP-as-collateral integrations that read gauge balances and need to translate them back to LT-share equivalents.
@external
@view
def updated_balances() -> (uint256, uint256):
if self.totalSupply > 0:
lv: LiquidityValuesOut = self._calculate_values(self._price_oracle())
return (lv.supply_tokens, lv.staked_tokens)
else:
return (0, 0)
distribute_borrower_fees()
Description:
Permissionless. Sweeps accrued borrower interest out of LEVAMM and donates the resulting crvUSD into the underlying Cryptoswap pool via add_liquidity([amount, 0], min_amount, empty(address), donation=True). The donation flows into the pool's donation-protected buffer and unlocks over the pool's donation window rather than minting fresh LP tokens to a user.
Triggered automatically at the end of every deposit and withdraw with the default FEE_CLAIM_DISCOUNT (1%). External callers can run it more often than that — useful when the pool's reserve is thin and the operator wants to top it up immediately rather than wait for the next user op.
The discount parameter caps how much price-scale movement is acceptable on the Cryptoswap side: min_amount = (1e18 - discount) × crvusd_amount / CRYPTOPOOL.lp_price(). Larger discounts allow the donation to land even when the pool is mid-rebalance. Only admin can pass a discount greater than the default 1%.
Reverts: the underlying Cryptoswap add_liquidity reverts (typically Slippage) when the donation cannot meet min_amount. Try again with a larger discount, or wait for the pool to settle.
Emits DistributeBorrowerFees(sender, amount, min_amount, discount).
Admin-only setters (governance)
These are called only by the LT's admin — either an EOA owner or (via _check_admin) a factory contract whose admin() matches the caller. Integrators usually do not call these directly; documented here for governance audit and indexing.
| Function | Effect | Emits |
|---|---|---|
set_amm(amm) | One-shot. Sets the AMM contract address; rejects subsequent calls with "Already set". | — |
set_admin(new_admin) | Transfers admin to a new EOA or factory. Sanity-checks the candidate's admin/emergency_admin/fee_receiver/gauge_controller/min_admin_fee getters if it is a contract. | SetAdmin |
set_rate(rate) | Forwards to AMM.set_rate. Bounded by MAX_RATE (100% APR). | SetRate (from AMM) |
set_amm_fee(fee) | Forwards to AMM.set_fee. Bounded by MAX_FEE (10%). | SetFee (from AMM) |
set_staker(staker) | One-shot. Wires the LiquidityGauge address. Any pre-existing balance at the staker address is swept to fee_receiver. | SetStaker |
set_killed(is_killed) | Kills or unkills the AMM. Callable by LT admin, factory admin, or emergency_admin. | SetKilled (from AMM) |
Other public views
| Function | Returns |
|---|---|
is_killed() | True if the AMM is currently killed (proxies AMM.is_killed). Use this before deciding whether to call withdraw versus emergency_withdraw. |
min_admin_fee() | Current minimum admin-fee floor (Factory.min_admin_fee() when admin is a factory, else 0). |
checkpoint_staker_rebase() | Only meaningful when called by the staker contract (LiquidityGauge does this before every gauge op). Forces a _calculate_values write so subsequent ops see fresh buckets. Silent no-op for other callers. |