Collateralisation

A specific kind of structured deal artifact — the contingent-contract model. Full repo lifecycle, rolls, and rehypothecation chains. Where a confirmed Intent becomes an on-chain collateralised position.

Guides/Structure

Where this sits in the alignment economy. Collateralisation is a specific shape of structure stage — the operational role where a confirmed joint prompt (Intent) becomes an on-chain artifact. A repo is one of the most useful artifact shapes available: an asset locked under a repurchase obligation, with deterministic forfeiture if repurchase fails. Multi-party deals often resolve into repos (collateralised lending, securitisation construction, structured escrow) because they let a party access funding without losing custody of the underlying position. Read the alignment-economy guide for context; this page is the specific structural mechanic.

YieldFabric's collateral primitive turns the bilateral atomic swap into a programmable repo — locking assets on one or both sides, with deterministic repurchase or forfeiture by deadline. Because the lender's position is itself a typed, referenceable contract (a contingent contract), the lender can use it as collateral in a further repo, building collateral chains of arbitrary depth.

A note on terminology. What this guide calls "rehypothecation" is structurally a matched-book / back-to-back repo: the lender re-pledges its own position (Cont_A1) in a new swap, while the borrower's underlying collateral stays locked beneath. Traditional rehypothecation directly re-pledges the client's collateral; the economic effect is the same, but the legal mechanism here is position-pledging via a referenceable contingent contract.

This page covers:

  • The contingent-contract data model — what gets created when a repo activates, and how the pieces fit together.
  • The full lifecycle: creation, completion, repurchase, expiry, cancellation.
  • Rolls — atomically moving a live repo to a new counterparty.
  • Rehypothecation — using a contingent contract as collateral in a new repo, and how the unwind cascades.
  • Bilateral and mixed-asset collateral structures.

Background: /docs/swaps covers the atomic-swap primitive in general. Read that first if "swap" is new — this page assumes you've seen createSwap / completeSwap once.

The contingent-contract model

A repo isn't a single record. When completeSwap settles a swap that has collateral slots populated, the consumer pipeline writes a small hierarchy of composed contracts into the off-chain projection:

        Cont_A                    ← top-level contingent contract
         │                          (has both swap_id AND repurchase_contract_id)
         ├── Col_A                ← collateral wrapper (has parent = Cont_A)
         │    │
         │    └── <underlying>    ← the original obligations / payments
         │                          locked as collateral
         │
         └── Rep_A                ← repurchase wrapper (referenced by
                                    Cont_A.repurchase_contract_id)
              │
              └── <payments>      ← what the borrower owes back
Stored column on composed_contracts
swap_id
Meaning
The repo this contract belongs to. Set only on Cont_X.
Stored column on composed_contracts
repurchase_contract_id
Meaning
Points at Rep_X — what the borrower owes to unwind. Set on Cont_X.
Stored column on composed_contracts
parent_composed_contract_id
Meaning
The nesting link. Col_A and Rep_A have parent = Cont_A. Original obligations have parent = Col_A. Rehypothecation also flows through here: when Cont_A1 is collateral in SWAP-A2, its parent_composed_contract_id is set to Col_A2.

The GraphQL ComposedContract type adds derived fields on top of those columns:

type ComposedContract {
  id: String!
  name: String!
  isContingent: Boolean!        # true iff swap_id IS NOT NULL
  swapId: String                # the repo this contract represents
  parentId: String              # nesting link (stored)

  # The following three are NOT stored columns — they're resolved at query time
  # by walking the swap store for swaps that reference this contract's
  # underlying contracts as collateral. Re-fetched on every query.
  collateralInSwapId: String    # set when THIS contingent is itself being used
                                # as collateral in another swap
  collateralInSwapStatus: String
  collateralSwapExpiry: DateTime

  category: String!             # "CONTINGENT" | "COLLATERAL" | "REPURCHASE" | "STANDARD"
  displaySection: String        # "COLLATERAL" | "CONTINGENT" | "REGULAR" — UI hint
  isUserInitiator: Boolean      # who am I in this swap?
  children: [ComposedContract!]!
  contracts: [Contract!]!       # leaf-level obligations
}

The contingent contract is the lender's handle on the repo position. It is the asset that exists once the repo is active, with the repurchase obligation baked into its repurchase_contract_id pointer. Treating it as a first-class contract (and not a hidden side-effect of the swap) is what lets it participate in another swap as collateral.

State machine

                            createSwap
                                │
                                ▼
                          ┌──────────┐
                          │ PENDING  │  initiator signed,
                          └────┬─────┘  initiator collateral locked
                               │
              ┌────────────────┼────────────────┐
              │                │                │
       completeSwap       cancelSwap     deadline passes
              │                │                │
              ▼                ▼                ▼
          ┌────────┐      ┌──────────┐    (cancelable
          │ ACTIVE │      │CANCELLED │     by either side)
          └───┬────┘      └──────────┘
              │
              │   Cont_X, Col_X, Rep_X composed contracts now exist
              │
   ┌──────────┼──────────┐
   │          │          │
expiry    repurchase    no repo
passes    Swap paid    (one-shot)
   │          │          │
   ▼          ▼          ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│EXPIRED │ │REPURCHASED│ │COMPLETED │
└───┬────┘ └──────────┘ └──────────┘
    │
expireCollateral
    │
    ▼
┌──────────┐
│FORFEITED │
└──────────┘

Terminal states: COMPLETED, REPURCHASED, CANCELLED, FORFEITED. EXPIRED is a transient label — once expireCollateral lands the swap is FORFEITED.

Pattern 1 — Canonical repo (one-sided collateral)

Borrower locks collateral, receives funding upfront, repurchases by deadline OR forfeits.

Slot mapping:

Traditional repo
Borrower posts collateral
YieldFabric input field
initiatorCollateralContractReferences / initiatorCollateralPayments
Traditional repo
Lender funds borrower upfront
YieldFabric input field
counterpartyExpectedPayments (settles at completeSwap)
Traditional repo
Borrower repurchase obligation
YieldFabric input field
initiatorRepurchasePayments (the amount + denomination borrower must pay back)
Traditional repo
Repurchase deadline
YieldFabric input field
expiry (top-level, distinct from deadline which times completeSwap)
{
  "input": {
    "swapId": "REPO-2027-Q1-001",
    "name": "Q1 2027 Repo — bond AAA for AUD",
    "counterparty": "ENT-lender-456",
    "deadline": "2027-01-15T00:00:00Z",
    "expiry":   "2027-04-15T00:00:00Z",

    "initiatorCollateralContractReferences": [
      { "contractId": "CTR-bond-AAA-tranche-1" }
    ],

    "counterpartyExpectedPayments": {
      "denomination": "aud-token-asset",
      "amount": "990000",
      "payments": [ { /* … vault-payment input … */ } ]
    },

    "initiatorRepurchasePayments": {
      "denomination": "aud-token-asset",
      "amount": "1000000",
      "payments": [ { /* … vault-payment input … */ } ]
    },

    "idempotencyKey": "repo-q1-2027-001"
  }
}

The 10 000 AUD spread (≈ 1.0% over 90 days, ≈ 4.1% annualised on a 365-day simple basis) is the lender's yield and the borrower's cost — in line with typical short-term repo pricing. No external trustee, no manual unwind step — the chain itself enforces "repay or forfeit".

A note on collateral economics. Real-world repo lenders apply a haircut — they lend less than the collateral's market value to absorb price moves before expiry, and they may issue margin calls when the collateral re-prices. YieldFabric's mechanism locks collateral at inception with a static haircut (the difference between collateral value and counterpartyExpectedPayments is set by the parties at swap creation) and no dynamic margining. Operators wanting variation-margin behaviour layer it on as separate payment streams between the parties, scheduled out-of-band — the platform doesn't ship it as a primitive.

Two distinct timestamps:

  • deadline — until this, the counterparty (lender) can call completeSwap. After it, only cancelSwap works.
  • expiry — once completeSwap lands, the borrower has until expiry to call repurchaseSwap. After expiry, the forfeit side (typically the lender) calls expireCollateral to settle the transfer. The YF MQ pipeline accepts the call from any authenticated user; the on-chain contract enforces who's actually permitted to trigger the forfeit.

Settlement timeline:

 t=0     createSwap          PENDING
                             — bond locked on-chain
                             — initiator collateral payment positions written off-chain
 t≤d     completeSwap        ACTIVE
                             — lender pays 990 000 AUD upfront
                             — Cont_A, Col_A, Rep_A composed contracts created
                             — bond now visible as Col_A → underlying
 t≤e     repurchaseSwap      REPURCHASED
                             — borrower pays 1 000 000 AUD
                             — bond returns to borrower
                             — Cont_A's repurchase obligation is satisfied
   OR
 t>e     expireCollateral    FORFEITED
                             — bond transfers to lender's wallet
                             — Cont_A marks the repurchase as defaulted

Pattern 2 — Bilateral collateral / structured escrow

Both sides post collateral, both have repurchase obligations. Use this for mutual margin, securitisation construction, or conditional service escrow where neither party trusts the other to settle.

{
  "input": {
    "swapId": "ESCROW-2027-A",
    "name": "Escrow — securitisation construction round A",
    "counterparty": "ENT-investor-789",
    "deadline": "2027-01-10T00:00:00Z",
    "expiry":   "2027-02-10T00:00:00Z",

    "initiatorCollateralContractReferences": [
      { "contractId": "CTR-tranche-A-obligations" }
    ],
    "counterpartyCollateralPayments": {
      "denomination": "usd-token-asset",
      "amount": "500000",
      "payments": [ { /* … */ } ]
    },

    "initiatorRepurchasePayments":    { /* what initiator pays to get its obligations back */ },
    "counterpartyRepurchasePayments": { /* what counterparty pays to get its cash back */ },

    "idempotencyKey": "escrow-2027-a-create"
  }
}

If both sides pay their repurchase by expiry → both collaterals return (effectively an unwind). If only one pays → that side gets its collateral back, the other side forfeits. If neither → both forfeit to the counterparty.

The contingent-contract structure mirrors the asymmetry: each side gets its own Cont_X referencing its own Rep_X and pointing at its own Col_X. Both contingent contracts share the same swap_id.

Pattern 3 — Mixed obligations + payments

Any collateral slot accepts both *ContractReferences (obligations) AND *Payments (cash). A common shape is an invoice-financing repo where the borrower posts a basket of invoice tokens plus a partial cash margin:

{
  "initiatorCollateralContractReferences": [
    { "contractId": "CTR-invoice-A" },
    { "contractId": "CTR-invoice-B" }
  ],
  "initiatorCollateralPayments": {
    "denomination": "aud-token-asset",
    "amount": "50000",
    "payments": [ /* … */ ]
  },
  "counterpartyExpectedPayments": { "amount": "900000", /* … */ },
  "initiatorRepurchasePayments":   { "amount": "950000", /* … */ }
}

The ContractReference shape is itself a union: pass contractId (a single leaf contract) OR composedContractId (a previously composed hierarchy you want to treat as one collateral unit). Mixing these is how rehypothecation (next section) works.

Rolls — atomically moving a repo to a new counterparty

What a roll is, and why you'd do one

A repo roll transfers a live position from one counterparty-and-terms set to another, without taking the collateral out of lock at any point. The borrower's underlying stays continuously encumbered; only the contract structure around it changes. Three canonical uses:

  1. Funding rollover — your repo was a 30-day term, expiry is tomorrow, and you want another 30 days of funding. Rather than repurchaseSwap (which requires you to source the principal from somewhere) followed by a brand-new createSwap (which requires fresh collateral establishment), you roll — the collateral never lifts and you avoid two round-trips of on-chain settlement.
  2. Counterparty substitution — your current lender is exiting the position, but another lender (with better terms, or simply willing to take it on) will step in. The roll pays out the old lender and brings the new one in atomically. This is the bread-and-butter activity of every dealer's repo desk.
  3. Term restructuring — same parties, new dates. The roll lets you change deadline and expiry, the upfront amount, and the repurchase amount in one event.

The state machine atom on the swap is REPURCHASED, not CANCELLED — from the audit point of view the old repo terminated through repayment, not default. The repayment just happened to be funded by the new lender.

The two-step mechanics

                                t=0                  t=1                t≤new_expiry
                            ┌──────────┐         ┌──────────┐
        OLD SWAP (R1)       │  ACTIVE  │ ──────► │REPURCHASED│
                            └──────────┘         └──────────┘
                                  │                    ▲
                              initiateRoll        completeRoll
                                  │                    │
                                  ▼                    │
                            ┌──────────┐         ┌──────────┐         ┌──────────┐
        NEW SWAP (RR1)      │  PENDING │ ──────► │  ACTIVE  │ ──────► │REPURCHASED│
                            └──────────┘         └──────────┘         └──────────┘
                                                                       (or expires
                                                                       → forfeited)

         Collateral hierarchy:    Cont_R1          Cont_RR1            Cont_RR1
                                  │                │                   │
                                  Col_R1           Col_RR1             Col_RR1
                                  │                │                   │
                                  underlying ─── REPARENTED ──────► same underlying
                                  (never lifts)                       (returns to
                                                                       borrower)

Step 1 — initiateRoll:

mutation {
  initiateRoll(input: {
    oldSwapId: "REPO-2027-Q1-001",
    newSwapId: "REPO-2027-Q2-001",
    newCounterparty: "ENT-new-lender-999",
    newDeadline: "2027-04-20T00:00:00Z",
    newExpiry:   "2027-07-20T00:00:00Z",
    # what the new lender will pay upfront when they complete:
    newCounterpartyExpectedPayments: { amount: "1000000", denomination: "aud-token-asset", payments: [ /* … */ ] },
    # what the initiator will repurchase from the new lender:
    newInitiatorRepurchasePayments:  { amount: "1010000", denomination: "aud-token-asset", payments: [ /* … */ ] },
    # what's paid back to the OLD lender — typically equals the old
    # repurchase amount, may be prorated for time-weighted repos:
    oldSwapRepurchasePayments:       { amount: "1000000", denomination: "aud-token-asset", payments: [ /* … */ ] }
  }) { success newSwapId messageId }
}

What this actually does, on-chain and in the projection:

  • New swap row written with status PENDING and source_swap_id = oldSwapId (the lineage link).
  • Borrower pre-funds the old repo's repurchase by signing payments that the on-chain contract escrows. Their id_hashes are stored in Swap.repurchase_prefund_id_hashes for retrieval at step 2.
  • Upfront payment record created for the new counterparty to see as Incoming and accept at step 2.
  • The old swap is NOT touched. It stays ACTIVE. If step 2 never happens, the old repo continues to its original expiry unaffected.

Step 2 — completeRoll:

mutation {
  completeRoll(input: {
    newSwapId: "REPO-2027-Q2-001",
    idempotencyKey: "roll-q2-complete"
  }) { success messageId }
}

This is the atomic moment. On the consumer side, all of the following happen in one transaction:

  1. New counterparty's upfront settles — they pay newCounterpartyExpectedPayments (the 1 000 000 AUD).
  2. Old swap marked REPURCHASED. The borrower's pre-funded repurchase from step 1 settles to the old lender; the on-chain contract zeros R1's repurchase obligations.
  3. R1's composed-contract hierarchy is dismantled. Old Cont_R1 / Col_R1 / Rep_R1 are removed. The leaf collateral contracts are unlinked from R1 before RR1's hierarchy is created — to prevent the new contingent from accidentally walking up into the old (deleted) tree.
  4. Collateral migrates to the new swap. The same underlying obligations/payments that were in R1's Col_R1 are now in RR1's Col_RR1. They never become "loose" between the two — the on-chain lock is continuous.
  5. New swap status flips PENDING → ACTIVE. RR1's Cont_RR1 / Col_RR1 / Rep_RR1 are created with their parent-child links established. The new repurchase obligation becomes payable by the borrower to the new lender by newExpiry.

After step 2 the borrower's position looks indistinguishable from opening a new repurchase trade with the new lender — except that Swap.sourceSwapId on RR1 still points at R1, preserving the audit trail.

Worked example (single counterparty substitution)

Borrower has a 90-day repo with Lender A: 1 000 000 AUD lent against a bond, 1 010 000 AUD due back at expiry. 80 days have elapsed. Lender A wants out. Lender B is willing to take the position to maturity (10 days remaining) at a 30 bps premium over the original terms.

Step 1 — initiateRoll (called by Borrower)
   pre-funds the 1 010 000 AUD owed to Lender A (escrowed on-chain)
   creates RR1 in PENDING:
       newCounterpartyExpectedPayments  = 1 010 000 AUD (Lender B's upfront)
       newInitiatorRepurchasePayments   = 1 010 084 AUD (premium for 10 days)
       newDeadline                      = NOW + 1 day  (Lender B must accept)
       newExpiry                        = old expiry + 10 days
   R1 unchanged, still ACTIVE.

Step 2 — completeRoll (called by Lender B)
   Lender B pays   1 010 000 AUD upfront for RR1
   Borrower's pre-fund (from step 1) pays  1 010 000 AUD to Lender A
   R1 → REPURCHASED. Lender A is out.
   Bond migrates from Cont_R1 to Cont_RR1. Borrower's lock unchanged.
   RR1 → ACTIVE.

Step 3 — 10 days later: Borrower → repurchaseSwap(RR1)
   Pays 1 010 084 AUD to Lender B → bond returns.

Borrower's net for the full 90 days: lost 84 AUD (the rolling premium) over what they'd have paid Lender A. Lender A exits early and gets paid in full. Lender B earns the 84 AUD on the 10-day exposure to a fully-collateralised position — a competitive bid even at low spreads.

Time-weighted / prorated repurchase

oldSwapRepurchasePayments.amount is carried through verbatim from the message into process_completed_roll. The pipeline explicitly stores the message-time amount in repurchase_prefund_id_hashes JSON so that recomputing at step 2 can't drift:

"Critical for time-weighted repos where the amount was prorated at initiateRoll time — re-computing at completeRoll would give a different value and cause a balance-hash mismatch."

If the parties agree the borrower has only used 80/90 of the term when rolling, the oldSwapRepurchasePayments.amount reflects the prorated principal + accrued. The new lender doesn't see the full original repurchase — they fund whatever the rolled-in position is worth at the roll moment.

Failure modes

The two-step roll is not atomic across both messages. There's real time between step 1 and step 2:

Failure
completeRoll never lands by newDeadline
Effect
RR1 stays PENDING past its deadline; R1 is still ACTIVE (untouched). Borrower's pre-funded payments are escrowed.
Mitigation
Treat the pre-funded escrow as encumbered until either (a) completeRoll lands or (b) the position is unwound by repurchaseSwap on R1 directly (which requires the pre-fund to be reclaimable — coordinate with the platform team for the recovery procedure).
Failure
completeRoll lands but on-chain executes a partial state
Effect
Should never happen — step 2's processor wraps the cascade. If you see a swap with source_swap_id set, PENDING, AND the old swap is REPURCHASED, that's a bug — file a ticket.
Mitigation
None at the client layer; the consumer pipeline owns this.
Failure
Borrower changes mind between step 1 and step 2
Effect
They can cancelSwap on RR1 while it's PENDING (cancel pre-completion is allowed). The pre-fund escrow needs unwinding — same caveat as the first row.
Mitigation
Don't initiate a roll unless you're confident step 2 will follow within newDeadline.
Failure
Same newSwapId used twice
Effect
messages.idempotency_key UNIQUE prevents the second submission from reaching execution.
Mitigation
Generate a fresh newSwapId per roll attempt.

Lineage

Every rolled swap carries sourceSwapId pointing at the predecessor. Chains of rolls form a linked list — to audit a position's full history:

query Trace($swapId: String!) {
  swaps {
    byId(id: $swapId) {
      id
      sourceSwapId        # the previous swap in the chain (null = original)
      status
      deadline
      expiry
      parties { entity { name } role }
    }
  }
}

Recurse on sourceSwapId to walk back to the original repo. Each node in the chain is its own bitemporal record — versions, status transitions, party changes are all queryable independently.

Rehypothecation

Here's the structural insight that makes the platform a real repo market and not just a one-shot loan tool:

The contingent contract Cont_A produced by a repo is itself a first-class composed contract. It can be used as collateral in a new repo. That second repo produces a Cont_A2 referencing the first one. Repeat as needed.

The data model encodes this through parent_composed_contract_id: when Cont_A1 is collateral in SWAP-A2, the consumer pipeline sets Cont_A1.parent_composed_contract_id = Col_A2 (the outer swap's collateral wrapper). Nested chains form a tree:

                      Cont_A2  ← Lender B's view
                       │         (the repo between Lender A and Lender B)
       ┌───────────────┤
       │               │
     Col_A2          Rep_A2     ← Lender A's repurchase obligation to Lender B
       │
       ▼
     Cont_A1  ← Lender A's view of the underlying repo
       │         Stored:  parent_composed_contract_id = Col_A2
       │                  swap_id                     = SWAP-A1
       │                  repurchase_contract_id      = Rep_A1
       │         Derived: collateralInSwapId          = SWAP-A2
       │                  (resolver walks the swap store)
       │
       ├── Col_A1
       │    │
       │    └── Corporate Bond ← the original asset
       │         ├── Coupon stream
       │         └── Redemption
       │
       └── Rep_A1  ← Borrower's repurchase obligation to Lender A

How you set it up

When Lender A wants to rehypothecate Cont_A1 to Lender B, Lender A calls createSwap from its own wallet, passing Cont_A1's composed contract id as a collateral reference:

{
  "input": {
    "swapId": "SWAP-A2",
    "name": "Rehypo — Cont_A1 for AUD funding",
    "counterparty": "ENT-lender-B",
    "deadline": "2027-01-20T00:00:00Z",
    "expiry":   "2027-04-10T00:00:00Z",

    "initiatorCollateralContractReferences": [
      { "composedContractId": "CONT-A1-COMPOSED-ID" }
    ],

    "counterpartyExpectedPayments": {
      "denomination": "aud-token-asset",
      "amount": "985000",
      "payments": [ /* … */ ]
    },

    "initiatorRepurchasePayments": {
      "denomination": "aud-token-asset",
      "amount": "993000",
      "payments": [ /* … */ ]
    },

    "idempotencyKey": "rehypo-A2-create"
  }
}

Operator discipline: the outer expiry should be ≤ the inner repo's expiry. Otherwise Lender B's claim can outlive the underlying collateral lock — once the borrower repurchases, Cont_A1 no longer wraps the bond, and the outer position has nothing substantive behind it. YieldFabric does not currently validate this constraint at createSwap — clients must enforce it.

Lender A's economics on this rehypothecation:

  Lender A lent Borrower            990 000 AUD  (inner upfront)
  Lender A receives back          1 000 000 AUD  (inner repurchase)
  Lender A borrowed from Lender B   985 000 AUD  (outer upfront)
  Lender A repays Lender B         -993 000 AUD  (outer repurchase)
  ────────────────────────────────────────────────
  Net to Lender A                    +7 000 AUD  on  5 000 AUD net capital
                                                 = leveraged carry

Lender A funded only the 5 000 AUD difference between the inner upfront and outer upfront, earning the 2 000 AUD differential between the inner and outer spreads. This is the canonical prime-brokerage leverage pattern.

Lifecycle — three independent unwinds, ordered by inner-most first

Once the chain is up, every level has its own state machine. They unwind independently in time but the substance of each level's asset depends on what's still inside it.

Scenario A — happy path, full unwind

When the inner repo is repurchased, the borrower's repurchase payment is substituted as the outer swap's collateral — Cont_A1 still wraps something, just not the original bond anymore.

t1: Borrower → repurchaseSwap(SWAP-A1)
    → Cont_A1's repurchase obligation satisfied
    → Bond would normally return to Borrower, BUT Cont_A1 is still
      locked inside Cont_A2 as part of Col_A2's collateral
    → repurchase_swap_processor substitutes: repo_payment_A
      becomes the new collateral content inside Cont_A1
    → Bond physically flows back to Borrower

t2: Lender A → repurchaseSwap(SWAP-A2)
    → Cont_A2's repurchase obligation satisfied
    → Cont_A1 (now wrapping repo_payment_A) returns to Lender A
    → Lender A receives the borrower's repurchase payment via Cont_A1

After both repurchases the bond is back with the Borrower; Lender A holds the inner repurchase payment net of what was paid to Lender B; Lender B has its principal plus the outer spread.

Scenarios B and C — defaults (mechanism partially verified)

The on-chain contract and consumer pipeline encode specific rules for what transfers on expireCollateral when the forfeit-recipient target is a contingent contract: when the root is a contingent contract, its internal collateral subtree is skipped. Only the repurchase subtree is transferred.

That means:

  • Scenario B — outer default, inner cured. Lender B calls expireCollateral(SWAP-A2). The forfeit target is Cont_A1. Per the rule above, Lender B receives Rep_A1 (which now contains the borrower's repurchase payment from the cured inner repo) but NOT Col_A1 (which is empty anyway — the bond has been released to the borrower). Lender B is made whole in cash equivalent, the borrower keeps the bond, Lender A loses its outer-leverage profit plus its haircut.

  • Scenario C — inner default, outer outcome. Lender A calls expireCollateral(SWAP-A1). The forfeit target is the bond (STANDARD contract, not a contingent). The standard transfer path applies — but Cont_A1 itself is still locked in Col_A2 of SWAP-A2. Whether the bond physically flows to Lender A's wallet or stays attached to Cont_A1 (and therefore inside Cont_A2's collateral) is governed by the on-chain contract; the off-chain pipeline tracks the resulting state via the contract-store-walks driving collateralInSwapId.

    Outcomes from this point:

    • If Lender A then repurchases the outer (repurchaseSwap(SWAP-A2)) — Cont_A1 (with the realised bond effectively inside) returns to Lender A. Lender A is whole.
    • If SWAP-A2 expires unredeemed — Lender B forfeit-receives Rep_A1, and the realised bond's destination depends on the on-chain rules for nested-forfeiture. Verify with a contract-level test before relying on a specific outcome here; the off-chain processors leave the precise on-chain flow to the contract.

Bottom line: Scenario A is the path the pipeline explicitly handles and is well-trodden. Defaults in nested chains follow the on-chain contract rules; the off-chain projection tracks the resulting state but the platform doesn't synthesise additional unwind logic on top of what the contract dictates. For production use of complex chains, write contract-level integration tests for the specific default sequences your book is exposed to.

Querying a rehypothecation chain

query Chain($contractId: String!) {
  composedContracts {
    byId(id: $contractId) {
      id
      name
      isContingent
      swapId
      collateralInSwapId        # set if THIS contingent is itself collateral
      collateralInSwapStatus    # status of the outer swap
      collateralSwapExpiry      # outer expiry — watch this!
      children {                # the underlying — recurse for nested contingents
        id
        name
        isContingent
        collateralInSwapId
        children { id name isContingent }
      }
    }
  }
}

The recursion bottoms out at a STANDARD contract (no swap_id).

Constraint discipline

Three constraints matter for nested chains. The current platform does not enforce them at createSwap validation time — clients must encode them, and operators should reject swaps that violate them.

  1. Outer expiry ≤ inner expiry. A rehypothecation that outlives its underlying collateral lock has no underlying recourse — once the borrower repurchases, Cont_A1 no longer wraps the bond. No platform-side check today; verify client-side before submitting createSwap.
  2. Only the contingent's holder should rehypothecate it. Possession is signalled by Cont_A1.isUserInitiator = false for the lender (i.e. the lender is the counterparty of the inner repo). The on-chain contract enforces that a participant owns what they're posting as collateral; the right test before submitting is Cont_A1.isUserInitiator === false from the rehypothecating party's perspective.
  3. collateralInSwapId is effectively single-valued. A contingent should be collateralised in at most one outer swap at a time. Since collateralInSwapId is derived by walking the swap store, a chain that tries to re-pledge an already-pledged contingent will be visible — and the on-chain contract checks ownership at execution. Don't rely on validation; structure your flow so the previous outer swap terminates before re-pledging.

Mutation reference (collateral lifecycle)

Mutation
createSwap
When
Repo proposed
Caller
Initiator (borrower)
What happens to composed_contracts
Swap row written; initiator collateral marked locked. No Cont_X yet.
Mutation
completeSwap
When
Lender accepts
Caller
Counterparty
What happens to composed_contracts
Cont_X, Col_X, Rep_X rows created; nesting links established.
Mutation
repurchaseSwap
When
Pre-expiry repay
Caller
Borrower (or both sides in bilateral)
What happens to composed_contracts
Cont_X.repurchase_contract_id's payments settle; underlying flows back.
Mutation
expireCollateral
When
Post-expiry forfeit
Caller
Any authenticated YF user (on-chain rules govern actual permission; typically the forfeit-recipient triggers it)
What happens to composed_contracts
Collateral transfers to opposite side; Cont_X updated to FORFEITED. When the forfeit target is itself a Cont_X, only its Rep_X subtree transfers — Col_X is left for the inner-repo parties.
Mutation
cancelSwap
When
Pre-completion bail-out
Caller
Either side with pre-agreed key
What happens to composed_contracts
No Cont_X created (cancel only valid pre-completion).
Mutation
initiateRoll
When
Move repo to new counterparty
Caller
Borrower / coordinator
What happens to composed_contracts
New swap in PENDING; new Cont_X not yet materialised.
Mutation
completeRoll
When
New lender funds, old lender exits
Caller
New lender
What happens to composed_contracts
Old Cont_X marked REPURCHASED; new Cont_X created; collateral re-parented.
Mutation
swapObligorPayment
When
Atomic obligor transfer
Caller
New obligor
What happens to composed_contracts
Specialised — replaces a payment's obligor without creating a full repo.

Common pitfalls

Pitfall
Confusing deadline and expiry
Why it bites
One times completeSwap, the other times repurchaseSwap.
Avoid
deadline = "accept by", expiry = "repurchase by". For non-repo swaps leave expiry null.
Pitfall
Rehypothecating with outer expiry > inner expiry
Why it bites
Outer holder loses recourse when inner cures (Cont_A1 no longer wraps the bond). Not validated by the platform — client-side discipline.
Avoid
Always set outer expiry ≤ inner expiry. The chain is only economically sound when repurchase windows nest in time.
Pitfall
Forgetting repurchaseSwap before expiry
Why it bites
Collateral forfeits permanently; no manual override.
Avoid
Schedule reminders. The MQ pipeline can submit repurchaseSwap automatically if pre-arranged.
Pitfall
Treating Cont_X like a static record
Why it bites
It updates: its collateralInSwapId flips on rehypothecation; its repurchase_contract_id payment status changes on repurchase.
Avoid
Always re-fetch via GraphQL before acting; don't cache shape.
Pitfall
Passing the wrong reference shape
Why it bites
Each ContractReference is XOR — contractId OR composedContractId, not both. Single contracts vs composed bundles.
Avoid
When rehypothecating, you always pass composedContractId (the Cont_X id). When posting a fresh asset, you pass contractId.
Pitfall
Calling cancelSwap after completeSwap
Why it bites
Once Cont_X exists, the only unwinds are repurchase / forfeit.
Avoid
Don't try; either honour the repurchase or wait for expiry.
Pitfall
Reading collateral payment slots as arrays
Why it bites
They're stored as JSON strings on Swap's output (e.g. initiatorCollateralPayments: String).
Avoid
Parse client-side; the structure matches InitialPaymentsInput.
Pitfall
Missing idempotencyKey on a roll
Why it bites
A network blip mid-roll can create a second new swap.
Avoid
Always pass a deterministic key, especially for rolls where two messages reference each other.

Reference

Status enum

enum SwapStatus {
  PENDING      // createSwap landed; collateral locked, awaiting completeSwap
  ACTIVE       // completeSwap settled; Cont_X exists, repurchase window open
  COMPLETED    // one-shot exchange finished (used when there's no repurchase obligation)
  REPURCHASED  // repurchaseSwap settled; collateral returned
  CANCELLED    // cancelSwap landed before completion
  EXPIRED      // expiry passed but expireCollateral not yet called (transient label)
  FORFEITED    // expireCollateral settled; collateral transferred to opposite side
}

ComposedContract category derivation

category is derived purely from stored columnsswap_id and repurchase_contract_id:

Has swap_id?
Has repurchase_contract_id?
category
CONTINGENT
Has swap_id?
Has repurchase_contract_id?
category
REPURCHASE (the Rep_X subtree)
Has swap_id?
Has repurchase_contract_id?
category
STANDARD or COLLATERAL *

displaySection is then derived per-user, additionally consulting the GraphQL-resolved collateralInSwapId (which walks the swap store):

category
CONTINGENT
collateralInSwapId set?
displaySection for holder
CONTINGENT
category
CONTINGENT
collateralInSwapId set?
displaySection for holder
COLLATERAL (this contingent is itself rehypothecated)
category
REPURCHASE
collateralInSwapId set?
displaySection for holder
none (always nested under its parent Cont_X)
category
STANDARD
collateralInSwapId set?
displaySection for holder
COLLATERAL (it's locked in some swap)
category
STANDARD
collateralInSwapId set?
displaySection for holder
REGULAR

(*) COLLATERAL is structurally the same as STANDARD — the category is just a hint about how the UI should group it. Borrowers see their posted collateral in the COLLATERAL section; lenders see their corresponding contingent in CONTINGENT.

Full collateral input slots in CreateSwapInput

input CreateSwapInput {
  swapId: String!
  counterparty: String!
  deadline: String!         # completeSwap deadline
  expiry: String            # repurchase deadline (omit for one-shot swaps)

  # Collateral — locked at swap creation (initiator) or completion (counterparty),
  # returned on repurchase or transferred on forfeiture.
  initiatorCollateralContractReferences:    [ContractReference!]
  initiatorCollateralPayments:              InitialPaymentsInput
  counterpartyCollateralContractReferences: [ContractReference!]
  counterpartyCollateralPayments:           InitialPaymentsInput

  # Repurchase — what each side owes to retrieve its collateral.
  initiatorRepurchaseContractReferences:    [ContractReference!]
  initiatorRepurchasePayments:              InitialPaymentsInput
  counterpartyRepurchaseContractReferences: [ContractReference!]
  counterpartyRepurchasePayments:           InitialPaymentsInput

  # (Plus expected/upfront slots, idempotencyKey, walletId, etc. —
  # see /docs/swaps for the full shape including the non-collateral fields.)
}

input ContractReference {
  contractId: String          # leaf contract — XOR with composedContractId
  composedContractId: String  # bundled / Cont_X composed contract — XOR with contractId
}

See also

YieldFabric docs(317)