Skip to main content

Aggregate borrowing limit

Verified against: spec docs/PRD.md §4.2 (aggregate rule, v7) · docs/work/interface-v7.md §A1, §A6 · D-018 (2026-11-10) · tests listed below (evidence/wo-21/cargo-test.log, 2026-11-10) · testnet: docs/EVIDENCE.md §G2b (2026-11-10T16:52Z, 91db9ea; re-run on 2.2.1-v7 2026-11-11, recorded in b8784c1), §G7 re-run after D-027 (2026-11-11, recorded in b8784c1)

Status: live on testnet (EVIDENCE §G2b, re-run on 2.2.1-v7 on 2026-11-11). It runs in nectar-core-v2.nectar.testnet: get_exit_capacity returns total debt, the shared capacity and the headroom. On 2026-11-11 the check refused the remote loan of position 8 (remote_loan_refused with reason aggregate_limit, reservation deposit refunded), as recorded in EVIDENCE §G7 re-run after D-027. The earlier v1 contract does not have this check.

Why it changed​

Q_c + R_c + D_c is one pool of exit capacity per asset, and every position in that asset shares it. v6 compared each new loan with the whole pool and ignored the loans already drawn against it, so two borrowers could each draw the full capacity (PRD §4.2). v7 adds a second check that covers the whole asset.

The two checks​

A borrow passes only if both hold (interface-v7 §A1):

position: position_debt ≤ max_nectar_ltv_bps · V_position / 10_000
asset: total_debt_usd_e6 + borrow ≤ (10_000 − h_c) · (Q_c + R_c + D_c) / 10_000
TermFieldNotes
V_positionunits × M × price (see Corporate-action multiplier)the position's own collateral
total_debt_usd_e6CollateralConfig.total_debt_usd_e6goes up on borrow, down on repay, and down by the debt repaid at settlement
Q_cactive_committed_q_usd_e6committed quotes (Pappus quotes)
R_cget_reserve(symbol).r_usd_e6Nectar Reserve pool: reserve USDC minus USDC in exit notice; no owner setter (D-024)
D_cverified_dex_depth_usd_e6verified DEX depth; forced to 0 when the session is closed (Weekend Oracle Guard)
h_cbase_haircut_bps in session, weekend_haircut_bps otherwiseowner-set

Integer maths, rounded against the borrower. A refusal comes back from preview_borrow with reason: "aggregate_limit".

Worked example (illustrative numbers, not chain state)​

Session closed, so D_c = 0 and h_c = 2,500 bps. Q_c = $10,000 and R_c = 0. The asset can carry (1 − 0.25) × $10,000 = $7,500 of total debt.

Steptotal debtHeadroomResult
Borrower A draws $6,000 (own LTV ceiling $7,000)$6,000$1,500passes both checks
Borrower B asks for $3,000 (own LTV ceiling $9,000)—$1,500refused, aggregate_limit
Borrower B asks for $1,500$7,500$0passes
A repays $2,000$5,500$2,000headroom is freed for anyone

Where you can read it​

get_exit_capacity(symbol) on v2 adds total_debt_usd_e6, aggregate_capacity_usd_e6 (= (1 − h)(Q + R + D)) and aggregate_headroom_usd_e6. preview_borrow adds aggregate_headroom_usd_e6 (interface-v7 §A6). The Feed MCP tool quote_capacity returns total debt and headroom when it runs against v2 (see Agent skill and x402).

Tests​

  • math::tests::aggregate_headroom_limits_b_c
  • tests_v7::two_borrowers_cannot_both_draw_full_capacity
  • tests_v7::split_capacity_between_two_borrowers_then_repay_frees_headroom
  • tests_v7::settlement_reduces_total_debt
  • tests_v7::failed_borrow_proceeds_leave_the_aggregate (a failed payout does not leave phantom debt)
  • sandbox aggregate_debt_two_borrowers_repay_settle (contracts/nectar-core/tests/sandbox_v7.rs)
  • Feed: agent skill: open_protected_stock_loan › "v2: aggregate_limit refusal" (services/feed/test/skill.test.ts)

Run them with cargo test --workspace in contracts/ and pnpm --filter feed test.