Skip to main content

Rebalance Flow

LEVAMM does not rebalance itself. It posts quotes via its invariant. When an arbitrageur finds the quote profitable versus Cryptoswap spot, their trade moves LEVAMM's state — and by construction that state drift is toward L=2L = 2.

The two prices

LEVAMM holds Curve LP as collateral (yy) and owes crvUSD as debt (dd). Its invariant links those to an "x0" quantity derived from the oracle price (pop_o):

x0(po,y,d)=poy+(poy)24LEV_RATIOpoyd2LEV_RATIOx_0(p_o, y, d) = \frac{p_o \cdot y + \sqrt{(p_o \cdot y)^2 - 4 \cdot \text{LEV\_RATIO} \cdot p_o \cdot y \cdot d}}{2 \cdot \text{LEV\_RATIO}}

From AMM.vy::get_x0 (lines 142–157). The AMM then quotes at get_p() = (x0 − d) / y scaled to collateral precision.

When get_p() drifts from Cryptoswap's implied LP price, an arb can flash-loan crvUSD, trade through LEVAMM in one direction, round-trip through Cryptoswap in the other, and pocket the gap minus fees and flash premium.

The call path

The arb invokes VirtualPool.exchange, which internally flash-borrows crvUSD, trades against LEVAMM, and unwinds through Cryptoswap. Reference: VirtualPool.vy::exchange (lines 182–210) and ::onFlashLoan (lines 136–179).

crvUSD → asset (buy direction)

sequenceDiagram
participant Arb as Arbitrageur
participant VP as VirtualPool
participant FL as crvUSD FlashLender
participant AMM as LEVAMM
participant Pool as Cryptoswap

Arb->>VP: exchange(0, 1, in_amount, min_out)
VP->>FL: flashLoan(maxFlashLoan)
FL-->>VP: flash crvUSD
VP->>AMM: exchange(0, 1, in_amount + flash)
AMM-->>VP: LP tokens
VP->>Pool: remove_liquidity(LP)
Pool-->>VP: crvUSD + asset
VP->>FL: repay (from crvUSD balance)
VP->>Arb: asset (out_amount)

asset → crvUSD (sell direction)

sequenceDiagram
participant Arb as Arbitrageur
participant VP as VirtualPool
participant FL as crvUSD FlashLender
participant AMM as LEVAMM
participant Pool as Cryptoswap

Arb->>VP: exchange(1, 0, in_amount, min_out)
VP->>FL: flashLoan(maxFlashLoan)
FL-->>VP: flash crvUSD
VP->>Pool: add_liquidity([flash, asset])
Pool-->>VP: LP tokens
VP->>AMM: exchange(1, 0, LP)
AMM-->>VP: crvUSD
VP->>FL: repay
VP->>Arb: crvUSD (remainder)

What the trade changes on LEVAMM

The AMM.exchange call (lines 286–365) updates stored state:

  • collateral_amount (y) — increases on asset-in direction, decreases on asset-out.
  • debt — mirror change after accrual via _debt_w().
  • A fee is carved from the output (out = raw_out × (1 − fee) / 1e18). The fee is not sent elsewhere; it stays in the AMM, which means the new x0 is strictly greater than the old x0.

The invariant check at line 353 enforces this directly:

assert self.get_x0(p_o, collateral, debt, check_state) >= x0, "Bad final state"

x0 after the trade must equal or exceed x0 before. That is how fees are captured and how the AMM refuses trades that would leave it worse off.

Why this drifts toward L=2L = 2

At equilibrium, collateral value equals twice the debt (poyCOLLATERAL_PRECISION=2dp_o \cdot y \cdot \text{COLLATERAL\_PRECISION} = 2 \cdot d in 1e18 units). The AMM tracks deviation as coll_vs_debt = p_o × y / d and applies the invariant check asymmetrically (AMM.vy lines 337–351):

  • If the trade moves coll_vs_debt toward 2, the strict check is relaxed (check_state = False).
  • If the trade moves it away, the strict safe-bounds check runs.

Arbs only trade when the quote-vs-spot gap exceeds costs. That gap opens precisely when oracle price has moved and the AMM is off-equilibrium. The profitable arbs are the ones that reduce that offset. The AMM doesn't pull the system back — it tilts the field so the profitable path for arbs is the restoring one.

Post-trade hooks

If LT_CONTRACT is set and is a contract, the exchange ends by calling _collect_fees() and LT.distribute_borrower_fees() (AMM.vy lines 361–363). Accrued interest flows from AMM's stablecoin balance into LT's accounting on every user-facing swap. Not a gas-optional path — it runs unconditionally.

Interest accrual and rate setting

Interest accrues on the crvUSD CDP line inside LEVAMM via a compounding multiplier:

# AMM.vy
def _rate_mul() -> uint256:
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)

Admin sets the rate through LT, which forwards to AMM (LT.set_rate → AMM.set_rate), bounded by MAX_RATE (≤ 100% APR). The realized rate updates on every _collect_fees() call, which runs on every rebalance exchange.

Refueling loop

When LT receives stablecoin fees from AMM, it donates them back into the Cryptoswap pool rather than skimming. This concentrates pool liquidity without minting LP to LT:

# LT.vy
def _distribute_borrower_fees(discount: uint256):
amount = STABLECOIN.balanceOf(self)
if amount > 0:
min_amount = (10**18 - discount) * amount // CRYPTOPOOL.lp_price()
CRYPTOPOOL.add_liquidity([amount, 0], min_amount, empty(address), True) # donation=True

donation=True means no LP minted; the tokens just deepen the pool. 100% of borrower interest cycles back into the pool where it benefits all Cryptoswap LPs — including LEVAMM as the dominant holder. See User: Refueling & Donations for the economic framing.

Gas and failure modes

ConditionBehaviour
AMM killedexchange reverts (assert not self.is_killed)
Empty AMM (collateral_amount = 0)Reverts "Empty AMM"
Resulting x0 < prior x0Reverts "Bad final state"
coll_vs_debt outside safe bounds and moving away from L=2Reverts "Unsafe min" / "Unsafe max"
min_out unmetReverts "Slippage"
VirtualPool flash loan not set on FactoryflashLoan call reverts

Gas: one flash loan (crvUSD), one AMM.exchange (includes _debt_w, price_w, invariant solve, fee collect, and LT hook), one CurveCryptoPool.remove_liquidity or add_liquidity. Typically 400–600k on mainnet depending on Cryptoswap state.