CSA Model
The Credit Support Annex (CSA) governs variation margin between a pair of counterparties. IRSForge implements the signed Credit Support Balance (CSB) convention used by Bloomberg MARS, AcadiaSoft, and real ISDA CSAs.
One CSA per pair
There is exactly one Csa contract per (partyA, partyB) pair, signed jointly by operator, partyA, partyB, with regulators and scheduler as observers. It's created at init time from irsforge.yaml (see Demo vs Production). The operator is co-signatory because dispute adjudication requires non-trader authority — see Operator Role.
Signed Credit Support Balance
The CSA tracks one signed per-currency balance, csb, representing net collateral A has pledged toward B:
csb > 0⇒ A is the pledgor, B holds the collateralcsb < 0⇒ B is the pledgor, A holds the collateralcsb == 0⇒ no pledge outstanding
Why signed (not two-map)
An earlier iteration tracked postedByA and postedByB separately. That admitted a state — both sides simultaneously posted — that real CSAs never reach, and produced phantom margin calls on the wrong side when one side over-posted. The signed model is the same one Bloomberg MARS exposes; it makes "both sides posted" structurally impossible.
The TS shim at app/src/features/csa/decode.ts derives postedByA / postedByB from the signed CSB so existing UI components keep working without each one knowing the convention.
Parameters (from csa: block)
| Field | Meaning |
|---|---|
threshold.{DirA,DirB} | Per-direction tolerance — exposure within threshold ⇒ no call |
mta | Minimum Transfer Amount — calls below this are gated to zero |
rounding | Call increment — non-zero calls are snapped to the nearest multiple |
valuationCcy | Single reporting currency for the CSA |
eligibleCollateral[] | Whitelist of { currency, haircut } — Phase 5 ships haircut == 1.0 |
Margin call computation
exposure = NPV(swaps in netting set, valued in valuationCcy)
required.fromA = max(0, exposure - threshold.DirB)
required.fromB = max(0, -exposure - threshold.DirA)
targetCsb = required.fromA - required.fromB # signed
call = gateCall(targetCsb - currentCsb, mta, rounding)
gateCall is a no-op below mta and snaps to rounding otherwise.
Lifecycle states
| State | Meaning | Recoverable by |
|---|---|---|
Active | Normal operation | — |
MarginCallOutstanding | Call published, awaiting post | Pledgor posts (PostCollateral) |
MarkDisputed | One side disputed the latest mark | Operator (AcknowledgeDispute) |
Terminated | CSA closed (all swaps matured/unwound) | — |
State machine note: only states that need human recovery gate re-entry. An earlier bug asserted state == Active in the choice that produced MarginCallOutstanding, pinning the CSA forever — fixed by gating only on MarkDisputed.
Choices
Defined on Csa.Csa.Csa (contracts/src/Csa/Csa.daml):
| Choice | Controller | Effect |
|---|---|---|
PostCollateral | poster (A or B) | Increments signed CSB in poster's direction |
WithdrawExcess | either party | Decrements CSB if over-collateralised |
PublishMark | operator | Records new mark, computes call, may flip state to MarginCallOutstanding |
PublishMarkByScheduler | scheduler | Same body as PublishMark, scheduler-driven (sister choice — Daml 2.x has no disjunctive controllers) |
SettleVm | operator | Transfers pledged collateral to satisfy the call |
SettleVmByScheduler | scheduler | Sister choice |
Dispute | either party | Flags the mark, transitions to MarkDisputed |
AcknowledgeDispute | operator | Resolves dispute, returns to Active |
Contract identity
Every choice rotates the ContractId (Daml templates are immutable). Frontend code must look up the CSA by stable pair key (partyA + partyB), not by cached cid. Mutating choices use exerciseCsaWithRetry (app/src/features/csa/ledger/csa-actions.ts) which retries on CONTRACT_NOT_FOUND to handle racing rotations.
Production hardening
For live multi-tenant onboarding, use the CsaProposal template (Csa.Proposal:CsaProposal). It mirrors the CdsProposal pattern:
- Signatories: proposer + operator (both must authorize creation)
- Observers: counterparty + regulators
- Choices:
Accept(counterparty agrees, CSA is created),Reject(counterparty declines),Withdraw(proposer retracts before acceptance)
The init-time submitMulti [partyA, partyB, operator] path remains available for sandbox and reference deployments. The Operator console exposes the proposal workflow UI — see Operator view.