// 00 Priime at ETHOnline 2026ETHOnline 2026 · Continuity track

Priime at ETHOnline 2026

What is on this page

Two parts, one page. The deck at the top, twelve slides on what we shipped during the event. Below it, seven sections open the routed vault and the integrations behind it, each with its code and a question form.

// 0112 slides

The pitch deck

Click to browse the deck1 / 12
// 02  ContentsStart here

The code under the deck

Seven sections, in order

The deck above says what we built. This appendix shows it. Each section takes one part of the routed vault, or one integration behind it, says what it does and what it changes for the person who deposits, then prints the code that does it, straight from the repository. Ask a question under any section and leave an address. We reply by email.

// 03  The recursive loop strategyRust

The recursive loop strategy

What it is

The loop is the strategy the vault runs. Operators compute the whole opening sequence off the chain as one plan: swap, deposit, borrow, sized to the target leverage. The vault executes that plan and nothing else.

Why it matters

The minimum you receive on the swap is fixed in the plan, before anything is signed. Every operator computes the same plan, and the vault moves only when they agree. A builder reads the strategy as a list of calls.

components/vault-nav/src/nav.rs/lines 174-234/main (d4e9b70)
/// Compose the plan for opening a fresh recursive-loop position at the
/// composer's target leverage.
///
/// Sequence (five steps, one strike):
///   1. USDC.approve(router, usdc_amount)
///   2. router.exactInputSingle(USDC -> USDe, amountOutMinimum = min_usde_out)
///   3. USDe.approve(morpho, min_usde_out)
///   4. morpho.supplyCollateral(marketParams, min_usde_out, vault, "")
///   5. morpho.borrow(marketParams, target_borrow_usdc, 0, vault, vault)
///
/// `target_borrow_usdc` lands so post-strike measured leverage
/// = collateral / (collateral - debt) = target_leverage_bps / 10_000. Anyone
/// running the same TWAP + config produces identical bytes; Morpho's own
/// LLTV guard rejects the borrow at execution if the oracle disagrees.
#[allow(clippy::too_many_arguments)]
pub fn plan_open_position(
    vault: Address,
    usdc: Address,
    usde: Address,
    morpho: Address,
    swap_router: Address,
    pool_fee: u32,
    market_params: (Address, Address, Address, Address, U256),
    usdc_amount: U256,
    twap_price_1e24: U256,
    slippage_bps: u32,
    target_leverage_bps: u32,
    deadline_secs: u64,
) -> PlanBuild {
    let min_usde_out = swap_min_usde_out(usdc_amount, twap_price_1e24, slippage_bps);

    // Borrow amount lands us at target leverage.
    // debt = collateral * (L-1) / L; collateral_value ~= min_usde_out * 1e-12
    let target_l = U256::from(target_leverage_bps);
    let collateral_value_usdc = min_usde_out / U256::from(1_000_000_000_000u64);
    let borrow_usdc = if target_leverage_bps > 10_000 {
        collateral_value_usdc * (target_l - U256::from(10_000u16)) / target_l
    } else {
        U256::ZERO
    };

    let (loan_token, collateral_token, oracle, irm, lltv) = market_params;
    let m = MorphoMarketParams {
        loanToken: loan_token,
        collateralToken: collateral_token,
        oracle,
        irm,
        lltv,
    };

    let mut plan = PlanBuild::empty(deadline_secs);

    // 1. approve USDC to router
    plan.push(
        usdc,
        approveCall {
            spender: swap_router,
            amount: usdc_amount,
        }
        .abi_encode(),
    );

Written by Jakub and Khaled.

?Have a question about The recursive loop strategy?
Thanks, we'll get back to you by email.
↑ Back to contents
// 04  Liquidity sourceRust

Liquidity source

What it is

This module pins the one market this vault runs on, and the pick carries its venue with it. Every read the running operator makes is keyed to that market's id, its addresses and its ceiling. Nothing downstream is composed until that market is set.

Why it matters

Where you lend is the largest lever in the vault, and the product never sets it for you. It moves the carry, the room the vault has for deposits, and the distance to trouble. A depositor reads the venue and the market on the published vault.

components/vault-nav/src/lib.rs/lines 207-249/main (d4e9b70)
    fn run_cycle(trigger_time_secs: u64) -> Result<Vec<u8>, String> {
        // Belt-and-suspenders against a composer that offers dials the
        // component cannot honor. Loop-server's config validator has the
        // canonical enforcement; this catches any config that reaches wasm
        // with a knob we cannot attest against.
        refuse_unimplemented()?;

        let vault = cfg_address("vault_address")?;
        let market_id: FixedBytes<32> = cfg("market_id")?
            .parse()
            .map_err(|e| format!("bad market_id: {e}"))?;
        let lltv: U256 = cfg("lltv")?.parse().map_err(|e| format!("bad lltv: {e}"))?;
        let usdc = cfg_address("usdc_address")?;
        let usde = cfg_address("usde_address")?;
        let oracle = cfg_address("oracle_address")?;
        let irm = cfg_address("irm_address")?;

        let targets = ReadTargets {
            morpho: cfg_address("morpho_address")?,
            market_id,
            market_params: (usdc, usde, oracle, irm, lltv),
            irm,
            pool: cfg_address("pool_address")?,
            usdc,
            vault,
            twap_window_secs: u32::try_from(cfg_u64("twap_window_secs")?).map_err(|_| {
                "twap_window_secs exceeds u32 (observe secondsAgos is uint32)".to_string()
            })?,
            inputs_block_lag: cfg_u64("inputs_block_lag")?,
        };

        let s = fetch_state(rpc_url()?, &targets, trigger_time_secs)?;

        if s.market_last_update == 0 || s.market_last_update > s.block_timestamp {
            return Err(format!(
                "market lastUpdate {} incoherent with block timestamp {}",
                s.market_last_update, s.block_timestamp
            ));
        }
        let elapsed = s.block_timestamp - s.market_last_update;

        let accrued = nav::accrued_total_borrow(s.total_borrow_assets, s.borrow_rate_wad, elapsed);
        let debt = nav::borrow_assets_up(s.borrow_shares, accrued, s.total_borrow_shares);

Written by Jakub and Khaled.

?Have a question about Liquidity source?
Thanks, we'll get back to you by email.
↑ Back to contents
// 05  Dynamic leverageRust

Dynamic leverage

What it is

This module holds the loop at its target leverage. Every strike the operator compares the measured position against the configured bands, and a breach returns an empty plan rather than levering up. The bands come from the venue's own liquidation parameters, not from a house opinion.

Why it matters

Leverage is managed dynamically to cut liquidation risk. When the asset price moves, the vault unwinds or grows the position automatically, keeping the health factor out of the liquidation zone. The band is published on the vault page.

components/vault-nav/src/lib.rs/lines 327-377/main (d4e9b70)
        // Read the strategist's per-knob invariants and fail the cycle when
        // the measured observation breaches them. Each check is skipped when
        // the vault is not in a state the invariant applies to (no debt, no
        // NAV, no policy declared), so a freshly published vault attests
        // deterministically before any strategist action has landed.
        if let Some(raw) = cfg_opt("applied_leverage") {
            let configured_bps = nav::parse_decimal_bps(&raw)?;
            // 25% tolerance band: 2.5x published lets the position sit in
            // [1.875x, 3.125x] before the cycle refuses.
            nav::check_applied_leverage_drift(obs.leverage_bps, configured_bps, 2_500, debt)?;
        }
        if let Some(raw) = cfg_opt("hf_deleverage_bps") {
            let deleverage_bps: u32 = raw
                .trim()
                .parse()
                .map_err(|e| format!("bad hf_deleverage_bps in config: {e}"))?;
            if nav::check_deleverage_threshold(obs.ltv_bps, deleverage_bps, debt, lltv)? {
                breach_flags |= nav::BREACH_DELEVERAGE;
            }
        }
        if let Some(raw) = cfg_opt("reserve_fraction") {
            let floor_bps = nav::parse_decimal_bps(&raw)?;
            nav::check_reserve_floor(obs.reserve_bps, floor_bps, value)?;
        }
        if let Some(raw) = cfg_opt("compound_cadence_hours") {
            let cadence_hours: u32 = raw
                .trim()
                .parse()
                .map_err(|e| format!("bad compound_cadence_hours in config: {e}"))?;
            // Six-hour grace: a missed cron beat does not immediately kill
            // an otherwise healthy vault.
            nav::check_compound_cadence(obs.hours_since_update, cadence_hours, 6)?;
        }

        // Fold every guard's verdict into the observations bag so the vault
        // contract can decode `breachFlags != 0` and stop taking new
        // deposit / redeem requests (roadmap P00 #13). Non-fatal: the strike
        // still attests, downstream verifiers still see every reading.
        obs.breach_flags = breach_flags;

        // Compose the on-chain action plan the vault will dispatch after
        // NAV settlement. On breach we force an empty plan: the position
        // has drifted past what the strategist configured, and continuing
        // to lever up (or emit any other loop action) against a live guard
        // would be a lie of omission. Plan-deleverage lands in P1 #4 and
        // will replace the empty plan with an unwind step.
        let plan = if breach_flags != 0 {
            nav::PlanBuild::empty(s.block_timestamp)
        } else {
            build_action_plan(&s, debt, value, twap_price, price)?
        };

Written by Jakub and Khaled.

?Have a question about Dynamic leverage?
Thanks, we'll get back to you by email.
↑ Back to contents
// 06  Auto-compoundRust

Auto-compound

What it is

This module carries the compounding cadence you set, and turns it into an invariant the operator checks. Every strike it measures the hours since the market last accrued against that cadence plus a six hour grace. Past that ceiling the cycle fails with compound_overdue rather than attesting.

Why it matters

Rewards are compounded back into the strategy automatically. You do not have to come back and reinvest by hand, and the capital keeps working instead of sitting idle, so the same deposit earns more over time.

components/vault-nav/src/nav.rs/lines 929-948/main (d4e9b70)
/// Fail the cycle when the market has not accrued within the strategist's
/// compound cadence. `grace_hours` is added to the configured cadence so a
/// missed cron beat does not immediately kill the vault. A cadence of zero
/// disables the check (no policy declared).
pub fn check_compound_cadence(
    hours_since_update: u32,
    cadence_hours: u32,
    grace_hours: u32,
) -> Result<(), String> {
    if cadence_hours == 0 {
        return Ok(());
    }
    let ceiling = cadence_hours.saturating_add(grace_hours);
    if hours_since_update > ceiling {
        return Err(format!(
            "compound_overdue: {hours_since_update} hours since market update, cadence {cadence_hours} h (+ {grace_hours} h grace)"
        ));
    }
    Ok(())
}

Written by Jakub and Khaled.

?Have a question about Auto-compound?
Thanks, we'll get back to you by email.
↑ Back to contents
// 07  Capital routerTypeScript

Capital router

What it is

The router decides which of the vault's two lanes holds the money. It is not a split. The whole book sits in one lane, and the rule evacuates that lane and rebuilds the other when the published rates say it should.

Why it matters

If the recursive loop starts paying less than plain USDC lending, the router reallocates the book automatically. It weighs capacity, transfer cost and the size of the gap, so a move only happens when it is the better economic decision.

apps/replay-ui/lib/canvas/floor-pair.ts/lines 105-162/main (d4e9b70)
/**
 * What one firing carries: the WHOLE lane weight.
 *
 * Outside R15's [0.05, 0.25] move cap by construction, and cleared by name.
 * `applyMove` still clamps it to the room the source actually has, so a lane
 * already at zero moves nothing and the decision records what moved.
 */
export const FLOOR_PAIR_MOVE_WEIGHT = 1;

/**
 * The weekly turnover ceiling, in percent of the book.
 *
 * 100 admits ONE full move per week and refuses the second, which is the
 * budget doing its job rather than the budget being switched off: the
 * evaluator refuses a firing it cannot fund and never sizes it down.
 */
export const FLOOR_PAIR_TURNOVER_PCT_WEEK = 100;

/**
 * ══ A SWITCH HOLDS ONE LANE, AND THIS IS WHO HOLDS IT ════════════════════
 *
 * On this pair the allocation is NOT a dial and NOT an even split. The
 * mechanism evacuates a lane and rebuilds it, so between firings the book is
 * entirely in one lane; a 50/50 seat drawn on the plate, published onto the
 * record and printed in the instrument's Allocation row described a portfolio
 * this machine is never in. It also made every printed move size a half-move:
 * the replay's own first decision reads `50.0pp` out of a 100% rule, because
 * the lane it evacuated only held half the book to begin with.
 *
 * WHICH LANE, and it is a measurement rather than a preference. At publish the
 * loop holds the book, unless the pair's own published rates TODAY already
 * clear the bar for the floor, in which case seating the loop would publish a
 * vault whose first act is to leave the lane it was seated in. That is the
 * whole rule, and the bar it asks with is the shipped one.
 *
 * `todayPair` is the caller's, because this module is a leaf with one import
 * and `router-history` reaches `templates` and back to `graph-ops`, which
 * imports this file: resolving the capture here would close a cycle of exactly
 * the shape F6 already cost this package once. `lib/canvas/floor-pair-seat.ts`
 * is the one module that resolves it, and every surface reads that.
 */
export type FloorPairHolder = "loop" | "floor";

/**
 * The lane the rule holds, given the pair's published rates today.
 *
 * A missing or unpriced side seats the LOOP: the loop is the lane the vault is
 * composed around, and an absent measurement is not evidence for leaving it.
 */
export function floorPairSeat(
  todayPair: { readonly loop: number | null; readonly floor: number | null } | null,
  bar: number,
): FloorPairHolder {
  const loop = todayPair?.loop ?? null;
  const floor = todayPair?.floor ?? null;
  if (loop === null || floor === null) return "loop";
  return floor - loop >= bar ? "floor" : "loop";
}

Written by Jakub and Khaled.

?Have a question about Capital router?
Thanks, we'll get back to you by email.
↑ Back to contents
// 08  The GraphYAML

The Graph

What it is

Every vault published on Priime is indexed by The Graph from the block it goes live. Our subgraph picks up each new vault on Base mainnet the moment the factory deploys it, with nothing to redeploy.

Why it matters

A vault's track record becomes public and verifiable. Every NAV the operators attested, every plan the vault executed or refused, every deposit and redemption is queryable by anyone, without asking Priime.

Better for users

Before depositing, anyone can read a vault's full history: its NAV over time, its daily low, high and close, and how deposits and redemptions actually settled. Trust comes from the record, not the pitch.

packages/subgraph/subgraph.yaml/lines 7-76/main (d4e9b70)
dataSources:
  - kind: ethereum
    name: PriimeVaultFactory
    network: base
    source:
      abi: PriimeVaultFactory
      address: "0xa3cbA56ECC2F6684abf3D5a0Fd2C93D030849aBF"
      startBlock: 51230370
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - Vault
      abis:
        - name: PriimeVaultFactory
          file: ./abis/PriimeVaultFactory.json
      eventHandlers:
        - event: VaultCreated(indexed address,indexed address,indexed
            address,address,address,uint256)
          handler: handleVaultCreated
      file: ./src/priime-vault-factory.ts
templates:
  - kind: ethereum
    name: PriimeVaultInstance
    network: base
    source:
      abi: PriimeVault
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - Vault
        - Strike
        - DepositRequest
        - RedeemRequest
        - DepositFulfilled
        - RedeemFulfilled
        - DepositRefunded
        - PlanExecution
        - PlanRejection
        - Deleverage
        - ExecuteCall
        - VaultDailyMetric
      abis:
        - name: PriimeVault
          file: ./abis/PriimeVault.json
      eventHandlers:
        - event: NavUpdated(indexed
            bytes20,uint256,uint256,uint256,bytes32,uint32,uint32,uint32,uint32,uint32,uint16)
          handler: handleNavUpdated
        - event: DepositRequest(indexed address,indexed address,indexed
            uint256,address,uint256)
          handler: handleDepositRequest
        - event: RedeemRequest(indexed address,indexed address,indexed
            uint256,address,uint256)
          handler: handleRedeemRequest
        - event: DepositRequestFulfilled(indexed address,uint256,uint256)
          handler: handleDepositRequestFulfilled
        - event: RedeemRequestFulfilled(indexed address,uint256,uint256)
          handler: handleRedeemRequestFulfilled
        - event: DepositRequestRefunded(indexed address,uint256)
          handler: handleDepositRequestRefunded
        - event: Executed(indexed address,bytes)
          handler: handleExecuted
        - event: PlanExecuted(indexed bytes32,uint256)
          handler: handlePlanExecuted
        - event: PlanRejected(indexed bytes32,bytes)
          handler: handlePlanRejected

Written by Jakub and Khaled.

?Have a question about The Graph?
Thanks, we'll get back to you by email.
↑ Back to contents
// 09  UniswapSolidity

Uniswap

What it is

Every vault the Priime factory deploys on Base swaps through a pinned Uniswap V3 pool. Opening the loop swaps USDC into USDe collateral there, and a deleverage swaps it back in the same transaction as the flash loan.

Why it matters

The vault polices every swap in its signed plan. A quorum-signed plan can only call Uniswap's single swap, can only pay the vault, and reverts below the minimum the operators fixed before signing.

Better for users

Depositors get Uniswap liquidity with a floor they can verify, written into the signed plan before any trade happens. Already live on Base mainnet: real swaps have settled to open a loop and cut its leverage.

contracts/src/PriimeVault.sol/lines 548-576/main (d4e9b70)
        // 3. Swap USDe -> USDC through the pinned Uniswap V3 pool.
        //    `minUsdcOut` is the operator quorum's slippage floor,
        //    computed off the same TWAP the strike attests; the swap
        //    reverts if the pool has moved outside that window between
        //    the operator's read and this call. SwapRouter02 drops the
        //    per-swap `deadline`; the whole plan step is atomic in one
        //    tx so the pool state at call time is the state the quorum
        //    priced against.
        IERC20(collateralToken).forceApprove(swapRouter, collateralOut);
        uint256 usdcOut = IUniswapV3SwapRouter02(swapRouter)
            .exactInputSingle(
                IUniswapV3SwapRouter02.ExactInputSingleParams({
                    tokenIn: collateralToken,
                    tokenOut: asset(),
                    fee: poolFee,
                    recipient: address(this),
                    amountIn: collateralOut,
                    amountOutMinimum: minUsdcOut,
                    sqrtPriceLimitX96: 0
                })
            );

        if (usdcOut < minUsdcOut) revert DeleverageSlippage(usdcOut, minUsdcOut);

        // 4. Grant Morpho the allowance it needs to pull the flashloan back.
        //    `safeTransferFrom` inside `flashLoan` consumes it exactly.
        IERC20(asset()).forceApprove(address(morpho), assets);

        emit Deleveraged(assets, collateralOut, usdcOut);

Written by Jakub and Khaled.

?Have a question about Uniswap?
Thanks, we'll get back to you by email.
↑ Back to contents