Cross-admin settlement
The DEX settles trades whose instruments are administered by two different Token Standard V2 registries — for example Canton Coin (admin = the DSO) against USDCx (admin = its registrar). All three flows — RFQ/OTC block trades, AMM swaps, and the order book — share one settlement model.
A V2.TransferLeg names its instrument by text id only; the registry admin
lives on the AllocationSpecification and the SettlementFactory, not on the
leg. Cross-admin settlement therefore:
- Groups the trade’s legs by instrument admin.
- Builds one
SettlementFactory_SettleBatchper admin, each covering exactly that admin’s legs and the allocations authorizing both sides of those legs. - Exercises every per-admin batch inside one Daml choice, so the whole trade commits atomically or not at all.
A single-admin trade is the simplest case: one admin key, one batch, the pre-cross-admin behavior unchanged.
The registry enforces the shape (splice-token-standard-utils
fetchAndValidateAllocations): every batch’s allocations must carry
admin == factory.admin, and must authorize both the sender and the receiver
side of every leg in the batch, with no missing or extra authorizations.
Instrument identity
Section titled “Instrument identity”InstrumentId = { admin : Party, id : Text } is the identity of an asset in the
settlement and matching model — Daml records, matching keys, admin-keyed
batching, HTTP settlement payloads, indexer schema, wallet balances, and history
carry the full {admin, id}. Text symbols are display labels only; two
registries may both issue an asset called USDC, so a bare id cannot safely
identify an instrument for settlement. Some display and pricing paths still match
by bare symbol: pool price lookups fall back to first match when a pair segment
is not admin-qualified, and a bare swap inputInstrumentId is resolved against
the pool and rejected as ambiguous only when both sides share that id.
InstrumentId derives Ord, so it is a
valid Map key; amount tables keyed by instrument (fees, notionals) use
Map InstrumentId Decimal, never a text-serialized key.
Shared invariants
Section titled “Shared invariants”These hold for every flow.
- Derive and verify. A settlement choice derives the expected admins and legs from on-ledger state (the trade, pool, or matched orders) and the signed quote/price binding. It rejects a supplied admin it did not expect and a missing admin it did. It never settles caller-supplied transfer legs.
- One shared settlement domain. All allocations for one trade reference the
same admin-independent
SettlementInfo, whoseexecutors = [operator]. A batch under admin A and a batch under admin B settle the same domain. - Disclosures, not
readAs. The operator does not holdreadAsrights on an external registrar (USDCx). Every per-admin batch settles on the registry-returned disclosures merged into the transaction plus the operator’s existing allocation visibility. No flow reads as an external admin. - Bind by admin across batches, by position within one. Settlement results
are mapped back to their allocations through a tagged batch plan keyed by
admin, not by raw position across the merged batches. Within a single admin’s
batch, though, the returned next-iteration cids are position-correlated with
that admin’s input allocations:
AdminBatch.nextIterationForzipsfrag.allocationswith the settle result’snextIterationAllocationCids, then matches each allocation to its next cid by cid. Recovery is the one path that leans on tree order: when a wallet returns only anupdateId,recoverCreatedAllocationsreads the createdAllocationcids from the transaction tree in node/command order and the caller maps them to admins positionally, so it relies on deterministic creation order in the tree. Within a single flow the recovered cids are then bound to their roles by index:settleAddLiquiditydestructures them as[lpBaseDepositCid, lpQuoteDepositCid, lpReceiptCid], and a swap takes the input-admin cid first. This is why the wallet must author the allocations in the canonical order for that flow. - Cleanup on abort. When a settlement aborts (a stale quote, a withdrawn
allocation), the order book cancels all of an order’s allocations in one
all-or-nothing
Order_Cancel. Swap and liquidity flows have no single atomic cleanup: their uncommitted allocations are withdrawn individually by the trader through the registry’sAllocation_Withdraw.
RFQ / OTC (implemented, 3c165e3)
Section titled “RFQ / OTC (implemented, 3c165e3)”MatchedTrade holds tradeLegs : [TradeLeg] where
TradeLeg = { admin : Party, leg : V2.TransferLeg }. MatchedTrade_Settle
derives the expected admins via splitLegsBy (.admin), requires exactly one
SettlementBatchV2 per admin, and rejects missing or unexpected admins. Rfq
carries full baseInstrumentId/quoteInstrumentId : InstrumentId; the dApp
resolves each leg’s admin from the listed venue pair.
AMM swaps and liquidity
Section titled “AMM swaps and liquidity”Pool carries baseInstrumentId, quoteInstrumentId, lpInstrumentId : InstrumentId. PoolSlice stores instrumentId : InstrumentId. A settle path
validates the slice allocation view against it; reconcile does not re-inspect the
allocation. PoolRules_ReconcileState fetches the pool’s slices, checks each
slice’s instrumentId against its pool side, and sums PoolSlice.amount against
PoolState.reserves — auditing the recorded slice amounts and instrument ids and
trusting the slice-to-allocation binding established at settle time.
Swap and liquidity settle through admin-keyed batches:
data RegistryBatchInput = RegistryBatchInput with factoryCid : ContractId V2.SettlementFactory extraArgs : ExtraArgs
choice PoolRules_Swap with batchesByAdmin : Map Party RegistryBatchInputBase, quote, and LP resolve to one, two, or three distinct admins; the choice
groups legs and allocations by admin into a Map Party and settles one batch
per key. Add/remove liquidity use the same grouping rather than fixed
base/quote/LP batch fields.
The swapper authorizes through a SwapAllocationRequest implementing
V2.AllocationRequest, one specification per (swapper, admin), accepted and
authored in one wallet command via BatchingUtilityV2 — the path liquidity
already uses. Allocation-factory context is resolved per admin (or per
allocation) with its own factory and disclosures; that is distinct from the
per-batch settlement context.
Registry admins are not made Pool observers: a settlement factory does not
need visibility of the pool contract.
Order book
Section titled “Order book”An order holds allocationCidsByAdmin : Map Party (ContractId V2.Allocation).
A single-admin pair has one entry — one allocation that locks the funding and
authorizes both future sides. A cross-admin pair has two: the lock-admin
allocation funds the locked side, and the counter-admin allocation is a
zero-funding receipt authorization (transferLegSides = [],
nextIterationFunding = Some TextMap.empty), which iterated settlement permits.
For a bid on BASE@A / QUOTE@B:
| Admin | Initial funding | Fill authorization |
|---|---|---|
| A | empty | receive base |
| B | quote budget | send quote |
For an ask the rows swap. When A == B each pair of rows collapses to one
allocation and one batch.
- Commitment parity. The receiver allocation matches the funding allocation’s commitment: an expiring order commits both with the same deadline; a GTC order leaves both uncommitted for unilateral exit. Otherwise the trader could withdraw the receiver authorization while funding stays locked.
- One request, all specs.
OrderAllocationRequestexposes one spec per distinct admin; the wallet authors all allocations atomically throughBatchingUtilityV2. - Viable-remainder roll-forward. A fill first determines whether the order
remains open — quote budget can exhaust through price and rounding even with
base quantity left. If it stays open, every admin allocation rolls (the
lock-admin allocation with the remaining funding, the others with
Some TextMap.empty); otherwise all finalize withnextIterationFunding = Noneand the order closes. - Tagged results.
OrderMatchExecution_ExecutetakesbatchesByAdmin : Map Party RegistryBatchInput(RegistryBatchInput = { factoryCid, extraArgs }), builds a tagged batch plan, and maps eachSettleBatchResultback to its admin’s allocations — position-correlated within that admin’s batch (thezipinnextIterationFor), then matched to each allocation by cid. It never reads results by raw position across the merged batches. - Atomic cancel.
Order_Cancelcancels every allocation inallocationCidsByAdmin, all-or-nothing.
Settlement domain
Section titled “Settlement domain”All of an order’s allocations — across both admins and across the fills that
roll them forward — share one iterated settlement domain identified by
(operator, base InstrumentId, quote InstrumentId, policy version), independent
of any admin. Every resting and matched order on that book settles that domain,
which is what lets the operator supply concrete leg sides at match time under
allocations funded earlier.
Operator trust boundary
Section titled “Operator trust boundary”A prefunded iterated allocation authorizes its executor (the operator) to supply
concrete legs at settlement. OrderMatchExecution constrains those legs to the
matched price, quantity, pair, and ownership, but the registry itself cannot
prove the operator entered through that choice rather than another. This is the
operator trust the order book already relies on; cross-admin support does not
widen it. AMM swaps and liquidity differ — the trader authors every leg side up
front, so settlement cannot add or alter trader authority.
Deployment cutover
Section titled “Deployment cutover”The record and choice changes are not valid smart upgrades of the deployed
canton-dex-trading lineage. They ship in the new package lineage
canton-dex-trading-v2 (see scripts/check-upgrade-compat.sh). Cutover on a
running deployment:
- Pause and drain existing pools; cancel resting orders and their allocations.
- Cancel outstanding pool slices and any live allocations.
- Deploy the
canton-dex-trading-v2DAR. - Update template package ids to
#canton-dex-trading-v2. - Recreate pools and the order book against the new package.
Registry prerequisites
Section titled “Registry prerequisites”Both registries in a cross-admin trade must:
- Implement the V2 allocation and settlement factories.
- Permit the pool’s or order’s committed iterated allocations.
- Be reachable on one synchronizer, so a single Daml transaction settles both.
- Supply operation-specific choice contexts and disclosures for allocation and settlement.
Build sequence
Section titled “Build sequence”- Package lineage and cutover — done (
canton-dex-trading-v2 1.0.0). - Full
InstrumentIdidentity across Daml, backend, indexer, and UI. - Reusable admin-grouping helpers shared by every flow.
- Split add/remove liquidity custody settlement by admin.
SwapAllocationRequestplusBatchingUtilityV2wallet batching.- Split swap settlement by admin.
- Order funding by admin, tagged roll-forward, atomic cancel, per-admin execute.
- Cleanup/recovery and cross-admin tests for every flow.
- Live-test USDCx/CC against both real registries.