Skip to main content

LEVAMM

Overview​

The LEVAMM (contract AMM, source file AMM.vy) is the market's leveraged automated market maker (AMM). This page is for developers who trade with the LEVAMM or read its state. The LEVAMM has two coins: coins(0) = STABLECOIN (crvUSD, 18 decimals) and coins(1) = COLLATERAL (the Curve pool LP token). It keeps leverage 2 between them. The system supports only leverage 2: Factory always deploys the LEVAMM with LEVERAGE = 2e18.

The LEVAMM tracks LP collateral and crvUSD debt, and prices trades from PRICE_ORACLE_CONTRACT. It charges a trading fee. It rejects a trade that leaves the safe region. In the safe region, the debt is between MIN_SAFE_DEBT and MAX_SAFE_DEBT times the collateral value (1/16 and 8.5/16 when LEVERAGE is 2).

Price source​

PRICE_ORACLE_CONTRACT is the market's CryptopoolLPOracle, not an external feed. The oracle computes the LP price from the Curve pool's virtual_price and price_scale, and the crvUSD aggregator price. price_scale is the pool's internal price. The oracle returns the LP price in aggregated USD: it converts crvUSD to USD with the price from the crvUSD aggregator.

The oracle has two price functions. price_w() also updates the aggregator state, and price() is a view. exchange() and _deposit() call price_w(). View functions call price().

This contract calls the oracle price p_o. On Twocrypto v3.0.0 pools, a pool policy can steer price_scale. A change in price_scale changes p_o in this contract. It changes x0, the virtual balance that maintains the leverage. It also changes quotes, the safe-debt checks (Unsafe min, Unsafe max), and value_oracle().

For how much a price_scale change moves the LP price, read Pool policy and price_scale in CryptopoolLPOracle. For the pool code versions that have a policy hook, read Pool policy and LP allowlist in Factory. For the policy contracts, read Pool policy.

Interest on the debt​

The debt grows over time at an interest rate that LT, the market's vault contract, sets. The LEVAMM realizes accrued interest and sends it to LT. It does this in collect_fees(). exchange() runs the same logic after each swap. LT then donates the interest to the Curve pool (distribute_borrower_fees).

Use by VirtualPool​

VirtualPool routes swaps through the LEVAMM, so users can trade between crvUSD and the market's asset without LP tokens. Flash loans cover the temporary balances, so only VirtualPool holds LP during the call. For the swap flow, read VirtualPool.


Public variables​

The LEVAMM has these variables for contract state and configuration.

Values with no getter

These values are not public. The contract has no getter for them:

  • MAX_FEE (uint256) - Maximum allowed trading fee (10^17 = 10%)
  • MAX_RATE (uint256) - Maximum allowed interest rate (10^18 / (365 * 86400) = 100% annual percentage rate (APR))
  • rate_time (uint256, storage) - Last rate update timestamp
  • MIN_SAFE_DEBT, MAX_SAFE_DEBT (uint256, immutable) - Safe debt-to-collateral-value bounds: 1/16 and 8.5/16 when LEVERAGE is 2

Immutable variables

  • LEVERAGE (uint256) - Leverage ratio set at deployment. The constructor checks only leverage > 10**18. The source comment states that only 2 * 10**18 is valid, and Factory always passes 2 * 10**18.
  • LT_CONTRACT (address) - LT contract address (immutable)
  • COLLATERAL (IERC20) - LP token contract address (immutable)
  • STABLECOIN (IERC20) - crvUSD contract address (immutable)
  • PRICE_ORACLE_CONTRACT (PriceOracle) - Price oracle contract address (immutable)

State variables

  • fee (uint256) - Current trading fee rate
  • collateral_amount (uint256) - Current LP token balance
  • rate (uint256) - Current interest rate
  • rate_mul (uint256) - Rate multiplier for interest calculation
  • minted (uint256) - Total crvUSD minted
  • redeemed (uint256) - Total crvUSD redeemed
  • is_killed (bool) - Emergency kill switch status

Data structs​

The LEVAMM uses these structs.

AMMState

  • collateral (uint256) - LP token amount held as collateral
  • debt (uint256) - crvUSD debt with accrued interest
  • x0 (uint256) - Virtual balance for maintaining leverage

Pair

  • collateral (uint256) - LP token amount
  • debt (uint256) - crvUSD debt amount

OraclizedValue

  • p_o (uint256) - Oracle price used for calculation
  • value (uint256) - Calculated value in aggregated USD

Events​

The LEVAMM emits these events.

TokenExchange: exchange() emits this event.

  • buyer (indexed address) - Address that initiated the exchange
  • sold_id (uint256) - Index of token sold (0=stablecoin, 1=collateral)
  • tokens_sold (uint256) - Amount of tokens sold
  • bought_id (uint256) - Index of token bought (0=stablecoin, 1=collateral)
  • tokens_bought (uint256) - Amount of tokens bought
  • fee (uint256) - Fee rate applied to this swap (1e18-based)
  • price_oracle (uint256) - Oracle price observed for the swap

AddLiquidityRaw: _deposit() emits this event.

  • token_amounts (uint256[2]) - Amounts of tokens added
  • invariant (uint256) - Value of the LEVAMM position after the deposit, in aggregated USD at the oracle price (x0 / (2L − 1))
  • price_oracle (uint256) - Oracle price at time of addition

RemoveLiquidityRaw: _withdraw() emits this event.

  • collateral_change (uint256) - Amount of collateral removed
  • debt_change (uint256) - Amount of debt removed

SetRate: set_rate() emits this event.

  • rate (uint256) - New interest rate
  • rate_mul (uint256) - Rate multiplier
  • time (uint256) - Timestamp

CollectFees: collect_fees() and exchange() emit this event.

  • amount (uint256) - Amount of fees collected
  • new_supply (uint256) - New supply (the debt after the interest accrual)

SetFee: set_fee() emits this event.

  • fee (uint256) - New trading fee

SetKilled: set_killed() emits this event.

  • is_killed (bool) - New kill switch status

Functions​

A function section lists the main revert strings of that function. For every LEVAMM revert string in one table, read Error codes: AMM.vy. exchange() comes first, then the view functions, then collect_fees(). The functions that only LT can call come last, from set_rate() on.

exchange()​

Description: Main trading function: swaps between crvUSD and the LP token. The state-update check keeps the leverage constant.

The safe-limit check differs by market:

  • Markets 7 to 10 (the source below): the LEVAMM relaxes the Unsafe min and Unsafe max check when the trade moves the collateral-to-debt ratio toward 2.
  • Markets 0 to 6: the LEVAMM checks both limits on every trade. Their exchange() has no check_state logic.

Returns: uint256 - Output amount received after the swap.

Emits: TokenExchange - Swap execution event.

The function takes these inputs:

InputTypeDescription
iuint256Input token index (0=stablecoin, 1=LP token)
juint256Output token index (must be opposite of i)
in_amountuint256Amount of input tokens to swap
min_outuint256Minimum acceptable output amount (slippage protection)
_foraddressRecipient address for output tokens (defaults to msg.sender)
Contract source: exchange()

The contract source gives this function body:

@external
@nonreentrant
def exchange(i: uint256, j: uint256, in_amount: uint256, min_out: uint256, _for: address = msg.sender) -> uint256:
assert (i == 0 and j == 1) or (i == 1 and j == 0)
assert not self.is_killed

collateral: uint256 = self.collateral_amount
assert collateral > 0, "Empty AMM"
debt: uint256 = self._debt_w()
p_o: uint256 = extcall PRICE_ORACLE_CONTRACT.price_w()
x0: uint256 = self.get_x0(p_o, collateral, debt, False)
x_initial: uint256 = x0 - debt

out_amount: uint256 = 0
fee: uint256 = self.fee

coll_vs_debt_before: uint256 = unsafe_div(p_o * collateral * COLLATERAL_PRECISION, debt)
if debt == 0:
coll_vs_debt_before = max_value(uint256)

if i == 0: # Trader buys collateral from us
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
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
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
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)

coll_vs_debt_after: uint256 = unsafe_div(p_o * collateral * COLLATERAL_PRECISION, debt)
if debt == 0:
coll_vs_debt_after = max_value(uint256)

check_state: bool = True # Init here separately
# 2 * 10**18 is the equilibrium collateral/debt ratio LEVERAGE / (LEVERAGE - 10**18),
# which equals 2.0 only because the system fixes LEVERAGE = 2 * 10**18 (see __init__).
# For any other leverage this threshold would misclassify trades; deploying such an AMM
# is unsupported and not reachable through the Factory.
if coll_vs_debt_after > 2 * 10**18:
if coll_vs_debt_before > coll_vs_debt_after:
# We improved -> relax the check
check_state = False
else:
if coll_vs_debt_before < coll_vs_debt_after:
# We improved -> relax the check
check_state = False

# This call also will not allow to get too close to the untradable region
assert self.get_x0(p_o, collateral, debt, check_state) >= x0, "Bad final state"
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:
self._collect_fees()
extcall LT(LT_CONTRACT).distribute_borrower_fees()

return out_amount

exchange() can revert with these strings:

StringRaised byCondition
Empty AMMAMM.exchangeThe LEVAMM has no collateral.
SlippageAMM.exchangeout_amount < min_out.
Bad final stateAMM.exchangex0 decreased.
Unsafe min, Unsafe maxAMM.exchangeThe trade leaves the safe region.
slippage, donation above cap!The Curve poolThe borrower-fee donation that follows the swap fails. For the causes, read distribute_borrower_fees() in LT.

An index pair other than (0, 1) or (1, 0), and a killed LEVAMM, revert without a message.


get_dy()​

Description: Returns the expected output for a given input, after the fee. It is a view function.

Returns: uint256 - Expected output amount, after the LEVAMM fee. Both coins have 18 decimals: crvUSD (index 0) and the Curve LP token (index 1; Factory.add_market() requires pool.decimals() == 18). The fee is 1e18-based.

The function takes these inputs:

InputTypeDescription
iuint256Input token index (0=stablecoin, 1=LP token)
juint256Output token index (must be opposite of i)
in_amountuint256Amount of input tokens to swap

For example, a return value of 498_000_000_000_000_000 is 0.498 LP tokens, because both coins use 18 decimals. The fee is already deducted. The amount of LP that a given crvUSD input buys depends on the pool state, so quote it with get_dy in the block you trade.

Contract source: get_dy()

The contract source gives this function body:

@external
@view
def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256:
assert (i == 0 and j == 1) or (i == 1 and j == 0)

p_o: uint256 = staticcall PRICE_ORACLE_CONTRACT.price()
collateral: uint256 = self.collateral_amount
debt: uint256 = self._debt()
x_initial: uint256 = self.get_x0(p_o, collateral, debt, False) - debt

if i == 0: # Buy collateral
assert in_amount <= debt, "Amount too large"
x: uint256 = x_initial + in_amount
y: uint256 = math._ceil_div(x_initial * collateral, x)
return (collateral - y) * (10**18 - self.fee) // 10**18

else: # Sell collateral
y: uint256 = collateral + in_amount
x: uint256 = math._ceil_div(x_initial * collateral, y)
return (x_initial - x) * (10**18 - self.fee) // 10**18

get_dy() can revert with this string:

StringRaised byCondition
Amount too largeAMM.get_dyi = 0 and in_amount is more than the debt.

get_p()​

Description: Returns the LEVAMM price of LP tokens in crvUSD.

Returns: uint256.

Contract source: get_p()

The contract source gives this function body:

@external
@view
def get_p() -> uint256:
p_o: uint256 = staticcall PRICE_ORACLE_CONTRACT.price()
collateral: uint256 = self.collateral_amount
debt: uint256 = self._debt()
return (self.get_x0(p_o, collateral, debt, False) - debt) * (10**18 // COLLATERAL_PRECISION) // collateral

get_state()​

Description: Returns the current LEVAMM state (collateral, debt, x0).

Returns: AMMState.

Contract source: get_state()

The contract source gives this function body:

@external
@view
def get_state() -> AMMState:
p_o: uint256 = staticcall PRICE_ORACLE_CONTRACT.price()
state: AMMState = empty(AMMState)
state.collateral = self.collateral_amount
state.debt = self._debt()
state.x0 = self.get_x0(p_o, state.collateral, state.debt, False)
return state

get_debt()​

Description: Returns the current debt in crvUSD, including accrued interest.

Returns: uint256.

Contract source: get_debt()

The contract source gives this function body and the internal function that it calls:

@external
@view
def get_debt() -> uint256:
return self._debt()

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

get_rate_mul()​

Description: Returns the rate multiplier, 1.0 + integral(rate, dt).

Returns: uint256 - Rate multiplier (1.0 == 1e18).

Contract source: get_rate_mul()

The contract source gives this function body and the internal function that it calls:

@external
@view
def get_rate_mul() -> uint256:
return self._rate_mul()

@internal
@view
def _rate_mul() -> uint256:
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)

value_oracle()​

Description: Returns the value of the LEVAMM position at the oracle price, in aggregated USD. The oracle price comes from virtual_price, price_scale, and the aggregator price, not from spot balances. The pool moves price_scale at most once per block, in the first operation of that block that calls tweak_price. So value_oracle() can change between two transactions of the same block.

Returns: OraclizedValue.

Contract source: value_oracle()

The contract source gives this function body:

@external
@view
def value_oracle() -> OraclizedValue:
p_o: uint256 = staticcall PRICE_ORACLE_CONTRACT.price()
collateral: uint256 = self.collateral_amount
debt: uint256 = self._debt()
return OraclizedValue(p_o=p_o, value=self.get_x0(p_o, collateral, debt, False) * 10**18 // (2 * LEVERAGE - 10**18))

value_oracle_for()​

Description: Returns the value, in aggregated USD at the oracle price, of a hypothetical position with the given collateral and debt.

Returns: OraclizedValue.

The function takes these inputs:

InputTypeDescription
collateraluint256Hypothetical collateral amount
debtuint256Hypothetical debt amount
Contract source: value_oracle_for()

The contract source gives this function body:

@external
@view
def value_oracle_for(collateral: uint256, debt: uint256) -> OraclizedValue:
p_o: uint256 = staticcall PRICE_ORACLE_CONTRACT.price()
return OraclizedValue(p_o=p_o, value=self.get_x0(p_o, collateral, debt, False) * 10**18 // (2 * LEVERAGE - 10**18))

value_change()​

Description: Returns the value of the LEVAMM position, in aggregated USD at the oracle price, after a hypothetical deposit or withdrawal.

Returns: OraclizedValue - (oracle_price, value_after): the oracle price, and the position value after the change.

The function takes these inputs:

InputTypeDescription
collateral_amountuint256Amount of LP collateral to add (deposit) or remove (withdrawal)
borrowed_amountuint256Amount of debt to add (deposit) or remove (withdrawal)
is_depositboolTrue for a deposit, False for a withdrawal
Contract source: value_change()

The contract source gives this function body:

@external
@view
def value_change(collateral_amount: uint256, borrowed_amount: uint256, is_deposit: bool) -> OraclizedValue:
p_o: uint256 = staticcall PRICE_ORACLE_CONTRACT.price()
collateral: uint256 = self.collateral_amount
debt: uint256 = self._debt()

if is_deposit:
collateral += collateral_amount
debt += borrowed_amount
else:
collateral -= collateral_amount
debt -= borrowed_amount

x0_after: uint256 = self.get_x0(p_o, collateral, debt, is_deposit)

return OraclizedValue(
p_o = p_o,
value = x0_after * 10**18 // (2 * LEVERAGE - 10**18))

max_debt()​

Description: Returns the maximum debt that the LEVAMM can take: its crvUSD balance plus its current debt.

Returns: uint256.

Contract source: max_debt()

The contract source gives this function body:

@external
@view
def max_debt() -> uint256:
return staticcall STABLECOIN.balanceOf(self) + self._debt()

accumulated_interest()​

Description: Returns the accrued interest that the LEVAMM has not collected.

Returns: uint256.

Contract source: accumulated_interest()

The contract source gives this function body:

@external
@view
def accumulated_interest() -> uint256:
minted: uint256 = self.minted
return unsafe_sub(max(self._debt() + self.redeemed, minted), minted)

outdated_debt()​

Description: Debt amount without applying the rate multiplier (pre-interest-accrual).

Returns: uint256.

Contract source: outdated_debt()

The contract source gives this function body:

@external
@view
def outdated_debt() -> uint256:
return self.debt

coins()​

Description: Returns the token address at an index: 0 is crvUSD, and 1 is the LP token.

The function takes this input:

InputTypeDescription
iuint256Token index
Contract source: coins()

The contract source gives this function body:

@external
@view
def coins(i: uint256) -> IERC20:
return [STABLECOIN, COLLATERAL][i]

check_nonreentrant()​

Description: A view function with the @nonreentrant modifier and an empty body. LT calls it in allocate_stablecoins() and withdraw_admin_fees() to check the nonreentrancy lock of the LEVAMM.

Contract source: check_nonreentrant()

The contract source gives this function body:

@external
@nonreentrant
@view
def check_nonreentrant():
pass

collect_fees()​

Description: Realizes accrued interest and transfers the available crvUSD to LT. The function has no caller check, so anyone can call it. LT calls it in distribute_borrower_fees() when the caller is not the LEVAMM. exchange() runs the same internal function after each swap. A killed LEVAMM reverts the call without a message.

Returns: uint256 - Amount of fees collected.

Emits: CollectFees.

Contract source: collect_fees()

The contract source gives this function body and the internal function that it calls:

@external
@nonreentrant
def collect_fees() -> uint256:
return self._collect_fees()

@internal
def _collect_fees() -> uint256:
assert not self.is_killed

debt: uint256 = self._debt_w()
self.debt = debt
minted: uint256 = self.minted
to_be_redeemed: uint256 = debt + self.redeemed

if to_be_redeemed > minted:
self.minted = to_be_redeemed
to_be_redeemed = unsafe_sub(to_be_redeemed, minted)
stables_in_amm: uint256 = staticcall STABLECOIN.balanceOf(self)
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

set_rate()​

Description: Only LT can call this function. It sets the interest rate.

Returns: uint256 - Rate multiplier.

Emits: SetRate.

The function takes this input:

InputTypeDescription
rateuint256New rate (int(fraction * 1e18) per second)
Contract source: set_rate()

The contract source gives this function body:

@external
@nonreentrant
def set_rate(rate: uint256) -> uint256:
assert msg.sender == LT_CONTRACT, "Access"
assert rate <= MAX_RATE, "Rate too high"
rate_mul: uint256 = self._rate_mul()
self.debt = self.debt * rate_mul // self.rate_mul
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate=rate, rate_mul=rate_mul, time=block.timestamp)
return rate_mul

set_rate() can revert with these strings:

StringRaised byCondition
AccessAMM.set_rateThe caller is not LT.
Rate too highAMM.set_raterate is more than MAX_RATE.

_deposit()​

Description: Only LT can call this function. It deposits LP tokens and borrows crvUSD.

Returns: OraclizedValue.

Emits: AddLiquidityRaw.

The function takes these inputs:

InputTypeDescription
d_collateraluint256LP tokens to add
d_debtuint256crvUSD to borrow
Contract source: _deposit()

The contract source gives this function body:

@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
debt: uint256 = self._debt_w()

debt += d_debt
collateral += d_collateral
self.minted += d_debt

self.debt = debt
self.collateral_amount = collateral

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

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

_deposit() can revert with these strings:

StringRaised byCondition
Access violationAMM._depositThe caller is not LT.
Unsafe min, Unsafe maxAMM._depositThe LEVAMM debt-to-collateral ratio leaves its safe bounds.

A killed LEVAMM reverts without a message.


_withdraw()​

Description: Only LT can call this function. It withdraws LP tokens and repays crvUSD in proportion. A caller other than LT gets the revert Access violation.

Returns: Pair - (collateral_withdrawn, debt_repaid).

Emits: RemoveLiquidityRaw.

The function takes this input:

InputTypeDescription
fracuint256Fraction of position to withdraw (1e18 units)
Contract source: _withdraw()

The contract source gives this function body:

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

collateral: uint256 = self.collateral_amount
debt: uint256 = self._debt_w()

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

self.collateral_amount -= d_collateral
self.debt = debt - d_debt
self.redeemed += d_debt

log RemoveLiquidityRaw(collateral_change=d_collateral, debt_change=d_debt)

return Pair(collateral=d_collateral, debt=d_debt)

set_fee()​

Description: Only LT can call this function. It updates the trading fee. A caller other than LT gets the revert Access. A fee above MAX_FEE reverts without a message.

Emits: SetFee.

The function takes this input:

InputTypeDescription
feeuint256New trading fee (must be ≤ MAX_FEE)
Contract source: set_fee()

The contract source gives this function body:

@external
def set_fee(fee: uint256):
assert msg.sender == LT_CONTRACT, "Access"
assert fee <= MAX_FEE
self.fee = fee
log SetFee(fee=fee)

set_killed()​

Description: Only LT can call this function. It sets the emergency kill switch. A caller other than LT gets the revert Access.

Emits: SetKilled.

The function takes this input:

InputTypeDescription
is_killedboolNew kill switch status
Contract source: set_killed()

The contract source gives this function body:

@external
def set_killed(is_killed: bool):
assert msg.sender == LT_CONTRACT, "Access"
self.is_killed = is_killed
log SetKilled(is_killed=is_killed)