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 .
The two prices
LEVAMM holds Curve LP as collateral () and owes crvUSD as debt (). Its invariant links those to an "x0" quantity derived from the oracle price ():
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 newx0is strictly greater than the oldx0.
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
At equilibrium, collateral value equals twice the debt ( 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_debttoward 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
| Condition | Behaviour |
|---|---|
| AMM killed | exchange reverts (assert not self.is_killed) |
Empty AMM (collateral_amount = 0) | Reverts "Empty AMM" |
Resulting x0 < prior x0 | Reverts "Bad final state" |
coll_vs_debt outside safe bounds and moving away from L=2 | Reverts "Unsafe min" / "Unsafe max" |
min_out unmet | Reverts "Slippage" |
| VirtualPool flash loan not set on Factory | flashLoan 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.