LEVAMM
Source: AMM.vy on GitHub
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 timestampMIN_SAFE_DEBT,MAX_SAFE_DEBT(uint256, immutable) - Safe debt-to-collateral-value bounds: 1/16 and 8.5/16 whenLEVERAGEis 2
Immutable variables
LEVERAGE(uint256) - Leverage ratio set at deployment. The constructor checks onlyleverage > 10**18. The source comment states that only2 * 10**18is valid, andFactoryalways passes2 * 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 ratecollateral_amount(uint256) - Current LP token balancerate(uint256) - Current interest raterate_mul(uint256) - Rate multiplier for interest calculationminted(uint256) - Total crvUSD mintedredeemed(uint256) - Total crvUSD redeemedis_killed(bool) - Emergency kill switch status
Data structs
The LEVAMM uses these structs.
AMMState
collateral(uint256) - LP token amount held as collateraldebt(uint256) - crvUSD debt with accrued interestx0(uint256) - Virtual balance for maintaining leverage
Pair
collateral(uint256) - LP token amountdebt(uint256) - crvUSD debt amount
OraclizedValue
p_o(uint256) - Oracle price used for calculationvalue(uint256) - Calculated value in aggregated USD
Events
The LEVAMM emits these events.
TokenExchange: exchange() emits this event.
buyer(indexed address) - Address that initiated the exchangesold_id(uint256) - Index of token sold (0=stablecoin, 1=collateral)tokens_sold(uint256) - Amount of tokens soldbought_id(uint256) - Index of token bought (0=stablecoin, 1=collateral)tokens_bought(uint256) - Amount of tokens boughtfee(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 addedinvariant(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 removeddebt_change(uint256) - Amount of debt removed
SetRate: set_rate() emits this event.
rate(uint256) - New interest raterate_mul(uint256) - Rate multipliertime(uint256) - Timestamp
CollectFees: collect_fees() and exchange() emit this event.
amount(uint256) - Amount of fees collectednew_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 minandUnsafe maxcheck 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 nocheck_statelogic.
Returns:
uint256 - Output amount received after the swap.
Emits:
TokenExchange - Swap execution event.
The function takes these inputs:
| Input | Type | Description |
|---|---|---|
i | uint256 | Input token index (0=stablecoin, 1=LP token) |
j | uint256 | Output token index (must be opposite of i) |
in_amount | uint256 | Amount of input tokens to swap |
min_out | uint256 | Minimum acceptable output amount (slippage protection) |
_for | address | Recipient 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:
| String | Raised by | Condition |
|---|---|---|
Empty AMM | AMM.exchange | The LEVAMM has no collateral. |
Slippage | AMM.exchange | out_amount < min_out. |
Bad final state | AMM.exchange | x0 decreased. |
Unsafe min, Unsafe max | AMM.exchange | The trade leaves the safe region. |
slippage, donation above cap! | The Curve pool | The 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:
| Input | Type | Description |
|---|---|---|
i | uint256 | Input token index (0=stablecoin, 1=LP token) |
j | uint256 | Output token index (must be opposite of i) |
in_amount | uint256 | Amount 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:
| String | Raised by | Condition |
|---|---|---|
Amount too large | AMM.get_dy | i = 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:
| Input | Type | Description |
|---|---|---|
collateral | uint256 | Hypothetical collateral amount |
debt | uint256 | Hypothetical 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:
| Input | Type | Description |
|---|---|---|
collateral_amount | uint256 | Amount of LP collateral to add (deposit) or remove (withdrawal) |
borrowed_amount | uint256 | Amount of debt to add (deposit) or remove (withdrawal) |
is_deposit | bool | True 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:
| Input | Type | Description |
|---|---|---|
i | uint256 | Token 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:
| Input | Type | Description |
|---|---|---|
rate | uint256 | New 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:
| String | Raised by | Condition |
|---|---|---|
Access | AMM.set_rate | The caller is not LT. |
Rate too high | AMM.set_rate | rate 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:
| Input | Type | Description |
|---|---|---|
d_collateral | uint256 | LP tokens to add |
d_debt | uint256 | crvUSD 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:
| String | Raised by | Condition |
|---|---|---|
Access violation | AMM._deposit | The caller is not LT. |
Unsafe min, Unsafe max | AMM._deposit | The 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:
| Input | Type | Description |
|---|---|---|
frac | uint256 | Fraction 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:
| Input | Type | Description |
|---|---|---|
fee | uint256 | New 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:
| Input | Type | Description |
|---|---|---|
is_killed | bool | New 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)
Related
- LT: the market's vault contract, which owns this LEVAMM and calls its LT-only functions.
- VirtualPool: the flash-loan wrapper that routes arbitrage trades through
exchange. - CryptopoolLPOracle: the oracle behind
PRICE_ORACLE_CONTRACT. - Rebalance flow: the end-to-end arbitrage call sequence.
- LEVAMM and VirtualPool in Math primer: the full derivation of the invariant.