xls: XX title: Repo description: Bilateral repurchase agreements with on-ledger collateral and time-proportional interest. author: Denis Angell (@dangell7) category: Amendment status: Draft proposal-from: TBD requires: XLS-85 created: 2026-09-22 updated: 2026-09-22
This amendment adds a repurchase agreement (repo) primitive to the XRP Ledger. A repurchase agreement is a secured loan written as two sales: the borrower sells an asset for cash and commits to buy it back later at a higher price, and the difference between the two prices is the interest. The opening sale is leg 1 and the repurchase is leg 2.
A seller locks collateral and receives cash from a buyer (leg 1). The seller has a committed, enforceable right to buy the collateral back by paying principal plus interest proportional to the elapsed time, before maturity (leg 2). If the seller does not pay by maturity plus a grace period, anyone can trigger default and the buyer keeps the collateral.
It is one ledger entry and five transactions, using the ledger's native
offer/accept idiom. The locking comes from Token Escrow (XLS-85) and the rate
math, rounding and grace/default semantics come from Lending (XLS-66).
RepoClose performs the payment of RepurchaseAmount and the unlock of
CollateralAmount in one transaction, and a failure of the payment fails the
transaction, so the collateral is never released without the payment. No
existing transaction releases an asset conditional on a payment landing.
The US market alone runs about $12.6T in daily exposures, and roughly 80% of tri-party volume is overnight. Tri-party repo is repo where a third-party agent holds the collateral and handles valuation and substitution for both sides. Broadridge clears about $9T a month on DLR, and JPMorgan's Kinexys has settled over $3T. Kinexys' signature product is intraday repo, borrowing against collateral for a few hours and paying interest only for the minutes outstanding.
Every live platform automates the same slice: atomic delivery versus payment
(DvP) at creation, meaning the collateral and the cash move together or neither
moves; collateral immobilized, meaning held where neither party can spend or
move it for the term; and a deterministic unwind. None of them put margin
calls, substitution or netting on-chain. A margin call is a demand for more
collateral when the existing collateral falls in value. Substitution is the
seller swapping one piece of collateral for another during the term. Netting is
settling many trades between the same two parties as one net amount. All three
fall back to the Global Master Repurchase Agreement (GMRA), the standard
bilateral contract the repo market trades under, which sets what happens when a
party fails to perform. Adoption happened anyway. This spec follows the same
line: automate the performing trade, leave the rest to the contract referenced
in the Data field.
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT" and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Key Space: 0xXXXX (TBD, next free value)
The ID is the tagged hash of the key space prefix, the seller's AccountID,
and the transaction Sequence (or ticket) of the creating RepoCreate.
| Field Name | Constant | Required | Internal Type | Default | Description |
|---|---|---|---|---|---|
| LedgerEntryType | Yes | Yes | UINT16 | TBD | Identifies this as a Repo entry |
| Account | No | Yes | ACCOUNT | N/A | Seller (cash borrower). Owns the entry |
| Counterparty | No | Yes | ACCOUNT | N/A | Buyer (cash lender) |
| CollateralAmount | No | Yes | AMOUNT | N/A | XRP, IOU or MPT locked for the term |
| PurchasePrice | No | Yes | AMOUNT | N/A | Cash delivered to the seller at accept. MUST be a different asset than the collateral |
| InterestRate | No | Yes | UINT32 | N/A | Annualized rate in 1/10 basis points, the same scale sfInterestRate carries on ltLOAN, so 100,000 is 100 percent per year. 0 is valid, and the ceiling is kMaxInterestRate |
| Expiration | No | Yes | UINT32 | N/A | Pending offer expiry. After this, anyone MAY cancel |
| StartDate | No | Conditional | UINT32 | N/A | Set by RepoAccept to that ledger's close time. Absent while pending |
| MaturityDate | No | Yes | UINT32 | N/A | Latest permissible repurchase. MUST be greater than StartDate |
| GracePeriod | No | Yes | UINT32 | N/A | Seconds after MaturityDate before default becomes possible |
| TransferRate | No | Optional | UINT32 | N/A | Collateral issuer's transfer rate captured at create (XLS-85 pattern) |
| Data | No | Optional | BLOB | N/A | Up to 256 bytes. Pointer to the governing legal annex or trade ID. Institutions need the GMRA reference |
| OwnerNode | No | Yes | UINT64 | N/A | Owner directory hint (seller) |
| DestinationNode | No | Yes | UINT64 | N/A | Owner directory hint (buyer) |
| IssuerNode | No | Conditional | UINT64 | N/A | Issuer directory hint for issued-asset collateral, per XLS-85 |
| PreviousTxnID | No | Yes | HASH256 | N/A | Standard |
| PreviousTxnLgrSeq | No | Yes | UINT32 | N/A | Standard |
The buyer normally lends less cash than the collateral is worth, and that
shortfall is the haircut, the buyer's protection against the collateral falling
in value. There is no haircut field. It is implicit in PurchasePrice relative
to the collateral's value, and in cleared dealer flow it is often 0% anyway.
The repurchase price is computed at close, not stored:
RepurchaseAmount = PurchasePrice * (1 + InterestRate * elapsed / (100,000 * 31,536,000))
elapsed = min(closeTime, MaturityDate) - StartDate [seconds]
Computed in Number, rounded up to the cash asset's precision per the XLS-66
rounding rules, so the buyer is never short by a rounding step.
No flags are defined. Pending and active are distinguished by whether
StartDate is present.
The entry is owned by Account (the seller) and appears in both parties'
owner directories, plus the issuer directory for issued-asset collateral
(XLS-85 pattern).
One owner reserve, charged to Account.
Deleted by exactly three paths: RepoCancel (pending), RepoClose (active,
paid) and RepoDefault (active, expired). An account with an open Repo
cannot be deleted: AccountDelete returns tecHAS_OBLIGATIONS, and because
the entry sits in both owner directories that holds for the buyer as well as
the seller, and for an IOU issuer whose asset is the collateral.
A transaction of any other type that removes a Repo fails the ValidRepo
invariant.
Applying an action to a repo in the wrong state returns tecREPO_ACTIVE when
the action requires a pending repo and tecREPO_PENDING when it requires an
active one.
Collateral locking follows XLS-85 exactly: issuer opt-in via
lsfAllowTrustLineLocking (IOU) and lsfMPTCanEscrow (MPT),
sfLockedAmount accounting, and the XLS-85 freeze tables at create, close
and default time.
- A
Repois created (pending) if and only if exactlyCollateralAmountbecomes newly locked fromAccountand zero cash moves. - Pending to active via
RepoAcceptrequires exactlyPurchasePricedelivered fromCounterpartytoAccountandStartDateset to that ledger's close time. - Deletion via
RepoCancelrequires full unlock toAccountand zero cash moved. If the submitter is not the seller, the close time MUST be pastExpiration. - Deletion via
RepoCloserequires the buyer credited at least the computedRepurchaseAmount(net of the cash asset's transfer rate) and the full collateral unlocked to the seller.RepurchaseAmount >= PurchasePricealways, since the rate is non-negative and rounding goes up. - Deletion via
RepoDefaultrequires close time pastMaturityDate + GracePeriod, full collateral delivered toCounterparty, and zero cash moved. - While any
Repoexists, aggregate locked balances are at least the sum of open Repo and Escrow collateral. This extends the XLS-85 locked-balance invariant. CollateralAmount,PurchasePrice,InterestRateandMaturityDateare immutable after create. Changing terms means close and recreate.
Invariants 1, 2, 4 and 5 are statements about how much value moved. An
invariant checker is handed the entries a transaction touched and not the
amounts it paid, so those four are enforced by balance assertions in the test
suite rather than by a checker. The ValidRepo checker enforces 3 and 7, the
state machine (no active repo reverting to pending, no repo deleted by any
transaction but RepoCancel, RepoClose and RepoDefault), and the field
bounds: positive collateral and price, MaturityDate after Expiration, and
the seller never equal to the counterparty. Invariant 6 is XLS-85's
locked-balance invariant, and the XRP half is enforced by XRPNotCreated,
which ltREPO was added to.
repo, in account_objects as a type filter and in ledger_entry either as
an object index or as an object of account (the seller) and seq (the
sequence of the creating RepoCreate).
The seller's offer. Locks the collateral immediately and creates the entry in a pending state. Locking at create means the buyer accepts against collateral already immobilized.
| Field Name | Required | Internal Type | Description |
|---|---|---|---|
| Account | Yes | ACCOUNT | Seller |
| Counterparty | Yes | ACCOUNT | Buyer. Only this account may submit RepoAccept. An offer open to any acceptor is not defined here |
| CollateralAmount | Yes | AMOUNT | Asset to lock |
| PurchasePrice | Yes | AMOUNT | Cash the buyer must deliver at accept |
| InterestRate | Yes | UINT32 | Annualized, 1/10 basis points. Above kMaxInterestRate returns temBAD_AMOUNT |
| Expiration | Yes | UINT32 | Offer expiry |
| MaturityDate | Yes | UINT32 | Repurchase deadline |
| GracePeriod | Yes | UINT32 | Seconds of grace after maturity |
| Data | Optional | BLOB | Legal annex or trade ID reference |
Follows the XLS-85 create table: frozen, unauthorized or non-lockable assets
fail. Additionally: the issuer of the collateral MUST be neither party,
collateral and cash MUST be different assets, Counterparty MUST NOT be
Account, and MaturityDate MUST be sane relative to Expiration.
Collateral locked from Account (XLS-85 machinery), Repo entry created in
both owner directories (plus issuer directory for issued assets), one owner
reserve charged.
The buyer's activation. Valid only from the account named in Counterparty,
while pending, before Expiration.
| Field Name | Required | Internal Type | Description |
|---|---|---|---|
| Account | Yes | ACCOUNT | Buyer (must equal the entry's Counterparty) |
| RepoID | Yes | HASH256 | The pending Repo entry |
Not the account named in Counterparty, already active, past Expiration, or
the payment of PurchasePrice fails (insufficient funds, frozen cash asset).
PurchasePrice delivered from buyer to seller through the payment engine,
StartDate set to this ledger's close time. The repo is active. Interest runs
from when the cash actually moved, not from when the offer was posted.
Pre-accept exit. The seller at any time while pending, or any account after
Expiration, the same permissioning as EscrowCancel.
Entry is active, or the submitter is neither the seller nor past Expiration.
Collateral unlocked back to the seller, entry deleted, reserve released. No cash moves.
Leg 2. Submitted by the seller any time up to MaturityDate + GracePeriod.
Not the seller, entry still pending, past MaturityDate + GracePeriod, or the
payment of RepurchaseAmount fails. A failed payment fails the whole
transaction and the collateral stays locked.
In one transaction: RepurchaseAmount computed and paid seller to buyer
through the payment engine (buyer-side trustline/MPToken auto-creation per the
XLS-85 finish rules), collateral unlocked back to the seller, entry deleted.
Submittable by any account once close time passes MaturityDate + GracePeriod,
the same permissioning as EscrowFinish and EscrowCancel.
Entry pending, or close time not yet past MaturityDate + GracePeriod.
Collateral delivered to Counterparty at the stored TransferRate, entry
deleted. There is no partial settlement. The buyer keeps all the collateral,
which is the close-out outcome the GMRA gives for a fully collateralized single
trade. Close-out is the termination of the trade on a failure to perform, with
the non-defaulting party keeping or liquidating what it holds.
RepoCreate (seller) RepoAccept (buyer, before Expiration)
collateral locked --> [ Repo: PENDING ] -- cash to seller --> [ Repo: ACTIVE ]
| |
RepoCancel (seller anytime, +-----------------+------------------+
anyone after Expiration) | |
unlock to seller, delete RepoClose (seller, RepoDefault (anyone,
t <= Maturity+Grace) t > Maturity+Grace)
pays P*(1+r*elapsed/yr) collateral to buyer,
unlock to seller, delete delete
Pending has one entrance and two exits. Active has two exits. There are no
other states. Batch(RepoCreate, RepoAccept) (XLS-56) settles creation and
acceptance atomically. An open (rolling) repo is a daily
Batch(RepoClose, RepoCreate, RepoAccept).
Offer/accept, not one jointly signed transaction. Requiring both parties to sign a single transaction has no wallet support, forces both signing ceremonies into the same window, and offers no asynchronous path. Offers, Checks, NFTokenOffers and PayChan are all offer/accept, each party signing its own transaction on its own schedule. A desk that can coordinate signatures wraps Create and Accept in a Batch.
Computed repurchase price. A stored fixed repurchase price would only fit term repo. Computing it from elapsed time covers overnight, term and intraday repo with one object.
No collateral pricing. The protocol does not value collateral. Parties negotiate the price, the ledger enforces the mechanics.
All-collateral default with no oracle. An oracle-priced surplus refund adds an oracle to the trust model of a settlement primitive. Surplus refund can come later as an opt-in.
What v1 leaves out. Collateral substitution, variation margin (topping up collateral as prices move), repricing, tri-party agent roles, general collateral baskets (any bond from an agreed list rather than one named bond), netting, negative rates, coupon pass-through, rehypothecation (the buyer lending out or reusing the locked collateral during the term), and on-ledger matching. Matching and negotiation stay off-ledger, as on every live platform.
Privacy and market transparency. Everything in a v1 Repo is public:
sizes, rates, maturities, counterparties. That is a deliberate trade. A public
ledger of real repo trades produces a verifiable overnight rate curve. SOFR,
the Secured Overnight Financing Rate published by the New York Fed, is computed
from data nobody outside can audit. Broadridge and Kinexys get their
confidentiality from running permissioned chains. Desks that need privacy today
use omnibus accounts, where many clients' positions sit under one account name,
and agent accounts, where a third party holds the position on the client's
behalf. Once Confidential MPT (XLS-96) lands, v2 can make the amounts
commitments while the structure stays publicly enforceable. What we will not do
is move the terms off-chain into Data: if the rate and price are not
on-chain, RepoClose cannot compute or enforce anything.
None. This is a new amendment gated behind featureRepo, purely additive.
No existing entry, transaction or RPC changes behavior.
Field and type codes are allocated clear of XRPLF develop at commit
9403736199.
| Code | |
|---|---|
featureRepo |
Supported::No, VoteBehavior::DefaultNo |
ltREPO |
0x0095 |
| Ledger name space | 'Z' |
ttREPO_CREATE |
114 |
ttREPO_ACCEPT |
115 |
ttREPO_CANCEL |
116 |
ttREPO_CLOSE |
117 |
ttREPO_DEFAULT |
118 |
sfCollateralAmount |
AMOUNT 39 |
sfPurchasePrice |
AMOUNT 40 |
sfMaturityDate |
UINT32 95 |
sfRepoID |
UINT256 47 |
tecREPO_ACTIVE |
211 |
tecREPO_PENDING |
212 |
Eleven of the entry's fields are develop's own and are reused rather than
allocated: sfAccount, sfCounterparty, sfInterestRate, sfStartDate,
sfExpiration, sfGracePeriod, sfTransferRate, sfData, sfOwnerNode,
sfDestinationNode and sfIssuerNode.
src/test/app/Repo_test.cpp (xrpl.app.Repo) covers the amendment gate, the
malformed creates, accept-and-close, cancel before and after Expiration,
default after the grace period, interest accrual asserted to the exact
repurchase amount, the kMaxInterestRate ceiling, IOU collateral with and
without the issuer's opt-in, IOU collateral frozen on either side, MPT
collateral including the unlock to the buyer, account deletion blocked for both
parties while a repo is open, and the account_objects and ledger_entry
lookups.
src/test/app/invariants/InvariantsRepo_test.cpp
(xrpl.app.InvariantsRepo) drives ValidRepo with deliberate violations,
including a deletion by a transaction that is not one of the three.
A reference implementation exists and will be published with the amendment
proposal. It is registered Supported::No, VoteBehavior::DefaultNo while it is
unaudited.
Partial repurchase and rehypothecation are not implemented and are not in this
spec. There is no RPC beyond the account_objects filter and ledger_entry.
RepoCloseis the novel money-movement path: a payment and an unlock on two different assets in one transaction, each with its own transfer rate. This is where the audit should spend its time.- Accrual rounding reuses the XLS-66 round-up rules verbatim.
- Freeze, lock and authorization edge cases at create, close and default time follow the XLS-85 tables.
- Default is deliberately non-blockable. Neither party can prevent
RepoDefaultafter the grace period, since any account can submit it. A frozen cash asset can blockRepoClose; the seller's remedy is the grace period and, failing that, default, which moves no cash. - The pending state is bounded by
Expirationand cancellable by any account after it, so a pending entry cannot hold the seller's reserve indefinitely.
No. Escrow debits the funding account at create, and the repo seller by definition does not hold the repurchase cash during the term. That is why the trade exists, and it is why the leg-2 cash cannot be pre-funded.
Repo is an inverted escrow. Return to the owner is the common outcome and requires a payment, and delivery to the destination is the outcome after the deadline. Building that into Escrow means a mode flag that flips the meaning of Finish and Cancel and puts a payment inside EscrowCancel. Every existing integrator assumes cancel refunds, finish delivers, and no other money moves. Changing that adds a second meaning to every branch in transactors that are already audited, and puts repo-only fields in Escrow permanently. This spec reuses Escrow's machinery (lock helpers, locked-balance accounting, freeze tables) as shared code, not its object or its transactors.
No. Lending is vault-intermediated, under-collateralized and amortizing, with principal repaid in instalments across the term. Repo is bilateral, fully collateralized and bullet-maturity, with the whole principal repaid in one payment at the end. Different products for different counterparties.
The buyer takes the loss, as with under-margined traditional repo.
No. The collateral sits in the ledger lock, not in the buyer's account, so the buyer cannot fail to return it. GMRA mini close-out, the clause that terminates the trade when one side fails to deliver the securities back, exists to handle that failure.
- XLS-85 Token Escrow: locking model, issuer flags, freeze tables
- XLS-66 Lending Protocol: rate math, GracePeriod and default handling
- XLS-56 Batch: atomic create+accept, rollover composition
- GMRA 2011: mini close-out, close-out netting. OFR and NY Fed US repo statistics
- Broadridge DLR, JPMorgan Kinexys intraday repo, Canton/Circle tokenized-UST repo: the adoption precedents this design follows