Mayavi
Protocol coverage·6 min read

Curve StableSwap Depeg Cascade: From Oracle Shock to Liquidation Wave

Curve StableSwap is the AMM that lives on the credibility wall — invariant tuned for pegged pairs, fragile in exactly the regime where everyone needs it to work. Modeling a depeg cascade through Curve requires the StableSwap invariant, an oracle shock, and an agent population that responds. Here is the slice.

Part of the Protocol coverage series.

Curve StableSwap is the only adapter in Mayavi's protocol library that isn't a borrower. Aave V3, SparkLend, Compound V3 — all CDP-shaped protocols where a user deposits collateral, borrows against it, and faces liquidation if the health factor crashes. Curve is an AMM. The agent population on a Curve scenario isn't borrowers being liquidated — it's liquidity providers, arbitrageurs, and the swap traffic that runs through the pool when the peg breaks.

Phase 5 ships one Curve scenario today: a mainnet StableSwap depeg cascade. This post walks the StableSwap invariant, the oracle-shock injection that triggers the depeg, and the cascade dynamics that follow.

StableSwap, not CryptoSwap

Curve runs two different AMM invariants. StableSwap is tuned for pegged-asset pools (3pool USDC/USDT/DAI, FRAX/USDC, crvUSD/USDC) — the swap curve leans toward constant-sum (1:1 exchange) when the prices are near peg, and bends toward constant-product (Uniswap V2-style) as they drift apart. CryptoSwap is for volatile-asset pools (tricrypto, ETH/BTC variants) and uses a different invariant entirely. Mayavi's adapter targets StableSwap; CryptoSwap is documented but out of Phase 5 scope.

The StableSwap invariant is fragile in exactly the regime where it matters most: small price moves around the peg are absorbed by the curve's near-constant-sum region, but once one asset depegs sharply, the curve drains liquidity from one side faster than arbitrageurs can refill. By the time the pool re-prices, the depeg has already moved through every downstream protocol that took the StableSwap price as oracle truth.

That's the cascade. Modeling it requires:

  1. The StableSwap invariant — quote a swap, execute it, read the pool's balances and virtual_price.
  2. A way to trigger the depeg — an oracle shock injected into the contracts that downstream protocols read.
  3. An agent population whose behaviour depends on the oracle truth (typically: borrowers facing liquidation when their collateral repriced).

The Curve adapter

mayavi/protocols/curve.py wraps the StableSwap pool contract. The public surface is small and reads like the Uniswap V3 Quoter pattern from Post 2:

class CurvePool:
    def quote(self, i: int, j: int, dx: int) -> int:
        """get_dy(int128 i, int128 j, uint256 dx) — static call, no state."""
 
    def exchange(self, i: int, j: int, dx: int, min_dy: int) -> CallResult:
        """exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)."""
 
    def balances(self) -> list[int]:
        """[balances(uint256 i) for i in range(n_coins)]."""
 
    def virtual_price(self) -> int:
        """get_virtual_price() — invariant value, drifts up with fees."""

Two index-type subtleties carry inline comments because they're trap-prone:

  1. int128 for swap indices, uint256 for getters. exchange and get_dy use int128 i, int128 j; balances(uint256) and coins(uint256) use uint256. Some older StableSwap pools use int128 for balances/coins too — supporting those is a Phase 6 parameterisation, not a Phase 5 bug.
  2. N is pool-specific. A pool's add_liquidity takes uint256[N] where N is the coin count (3 for 3pool, 2 for FRAX/USDC). The encoders infer N from len(amounts); CurvePool records n_coins on the instance so callers and the arg-length check have it.

Validation is the Quoter-bit-exact pattern from EIGEN, repointed at Curve: static-call get_dy, execute the same exchange, assert outputs match exactly. Curve's own published Vyper reference implementation is the source of truth.

The depeg-cascade scenario

The Curve depeg cascade lives at mayavi/scenarios/depeg_cascade.{py,yaml}. The setup, on mainnet at the pinned block:

  1. Five leveraged WETH-long Aave V3 borrowers — same shape as the Aave depeg-cascade scenarios from Post 4.
  2. Initial state: each supplies 1 WETH (~$2,562 collateral at the pinned block), borrows $1,700 USDC. Starting health factor ≈ 1.28.
  3. A BorrowerAgent per position, target HF 1.5, rebalance threshold 1.2.
  4. A liquidator wallet funded with $10M USDC headroom, hosting five LiquidatorAgents.
  5. At step 3 of an 8-step horizon (1 day per step, 7,200 blocks warp), a one-shot OracleShockAgent overrides AaveOracle's reported USDC price to $1.50 (50% spike — modeling a chaotic stablecoin flight-to-quality event).

After the shock, every borrower's HF crashes well below 1.0. Borrower agents try to repay back to target but their cash on hand from the initial borrow is short. The liquidators fire liquidationCall on their assigned borrowers in the next step.

What we assert end-to-end:

  • ≥ 1 successful liquidate action recorded.
  • Exactly one oracle_shock event at the configured shock_step.
  • The cascade completes within the horizon — no agent stuck in a step-internal deadlock.

This is the Aave-side bundle:

Bundle: aave-v3-mainnet-depeg-cascade-usdc-2026-05-14Aave V3 USDC depeg cascade — five borrowers, one oracle shock, downstream liquidations

What the Curve bundle adds

The Aave depeg cascade tells one half of the story: what happens downstream of the depeg. The Curve bundle tells the upstream half — what the StableSwap pool itself does when a coin moves away from peg, and how the pool's quoted price compares to the (artificially shocked) oracle truth that the downstream protocols are reading.

Bundle: curve-mainnet-depeg-2026-05-14Curve mainnet StableSwap depeg — pool composition + virtual_price under the shock

A scenario that chains these together — one shock that fires both into Curve's pool composition and into Aave's oracle simultaneously, then watches the cascade flow through both pools in the same run — is what cross_protocol_shock does. The scenario composes three existing agents (OracleShockAgent + Aave V3 BorrowerAgent/LiquidatorAgent + a 3pool CurveDepegAgent) onto a shared mainnet fork at the same pinned block and step them under one shock event. The compose layer is in mayavi/scenarios/cross_protocol_shock.py; the YAML at mayavi/scenarios/cross_protocol_shock.yaml wires 3 Aave borrowers + 2 Curve sellers + a USDC oracle shock to $1.50 at step 3 of an 8-step horizon.

Bundle: cross-protocol-mainnet-shock-2026-05-15One USDC oracle shock, two protocols reacting on the same fork — Aave V3 borrowers + Curve 3pool sellers under shared `cross_protocol_shock` scenario

What this composed run buys over the two single-protocol bundles above:

  • Same fork, same block, same shock step. The Aave borrowers and the Curve sellers see the same chain state and the same oracle event; there is no scenario-level skew to hand-wave.
  • One scheduler, one timeline. The cascade's temporal shape (borrower repayment attempts pre-shock, oracle override at step 3, liquidator + StableSwap responses post-shock) lives in one PSUB schedule rather than two separately-rendered runs.
  • No new protocol code. The adapters in mayavi/protocols/aave_v3.py and mayavi/protocols/curve.py are unchanged — the cross-protocol composition is a scenario-layer construction. Adding Compound V3 to the same shock would be one more agent block in the YAML, not a protocol rewrite.

Where this leaves the protocol-coverage theme

Four posts, four protocols, twelve committed bundles. The shape of the work to come is now clear:

  • The credibility wall (Post 2) holds. Every assertion in this theme runs against real on-chain state at a pinned block.
  • The protocol adapters scale. Adding a fifth protocol is the same recipe: thin Python wrapper, dispatch table keyed by chain_id, real-fork validation tests, scenario YAML.
  • The dispatch architecture catches its own bugs. The BNB decimal trap, the seconds-per-block silent miscalibration, the balance-slot mis-registrations — all caught by fail-loud per-chain tests, not silent fallbacks.

The next theme — RL agent reports — is what happens when we train a policy against this surface. Post 8 — Training a PPO Borrower walks Aave V3 borrower training across two backends (local GTX 1650 vs Modal A10G) and a fresh 200K-step run done specifically for this post.