Skip to content

Understand the design in 15 minutes

This page is the shortest path from the core Daml vocabulary to the Canton DEX design. It explains which contracts carry market state, which party authorizes each step, and where Token Standard V2 moves value. If terms such as template, choice, controller, party, contract id, or active contract set are new, first read the Canton and Daml primer; it assumes no Canton background. Then return here and follow deeper links only when you need the detail behind a statement.

The DEX decides whether a market action is valid. A token registry owns holdings and performs value movement.

flowchart LR
  W["Trader or LP wallet authorizes funding"] --> A["V2.Allocation locks holdings"]
  O["Operator proposes execution"] --> R["DEX rules choice validates market terms"]
  A --> R
  R --> S["SettlementFactory_SettleBatch moves every leg atomically"]

The recurring workflow is:

  1. A market contract records intent or state.
  2. The holder’s wallet creates the exact V2.Allocation needed by the flow.
  3. An operator-controlled DEX choice fetches the market state and validates the proposed action.
  4. The registry’s SettlementFactory_SettleBatch consumes all allocations and moves all transfer legs in one transaction.

The operator can decide when to propose an action. It cannot author a trader’s allocation or settle transfer legs outside the checks in the DEX choice.

Actor What it controls What it cannot do alone
Trader or LP Its wallet submission and holder-authored allocations Exercise operator-controlled settlement choices
Operator Pair and pool state, matching proposals, pool execution, submission Lock a self-custodial user’s holdings
Asset registry admin Registry implementation, factories, context, and token policy Change a trader’s signed DEX intent
LP registrar LP instrument policy and mint/burn recording Move reserve assets without the pool settlement path

The operator-mediated RFQ example is different: its backend ledger user has act-as rights for configured parties. That authority model is not the self-custodial path used by wallet-funded orders, swaps, and liquidity, and the repository does not expose it as a public relay service.

A Holding is spendable token value. An Allocation locks holdings for one settlement and describes the sides its authorizer permits. A FinalizedAllocation supplies the exact sides used in this iteration. SettlementFactory_SettleBatch checks that the batch is balanced and that each side is authorized before value moves.

Two V2 fields matter throughout this repository:

  • committed controls whether the authorizer may withdraw before the settlement deadline.
  • nextIterationFunding allows unused locked value or received value to back a successor allocation in the same settlement.

The local CantonDex.Registry.V2 is a runnable implementation of this boundary, not a privileged registry. Production assets may come from another registry that implements the same V2 APIs. Read Registry Integration for the exact assumptions.

DexPair is the operator-signed listing record. It names one registry admin, the base and quote instrument ids, enabled trading modes, and fees. The operator creates it directly and controls its update choices. There is no separate DexRules governance contract in this reference.

That is a deliberate single-operator boundary, not decentralized pair admission. A production fork that needs proposals, voting, or threshold approval should put that authority in a governance contract rather than copy the direct bootstrap path.

Read:

PoolRules_RequestSwap reads a precise pool snapshot and returns one allocation specification containing both the trader’s input side and every pool-to-trader output side. The wallet signs that complete specification.

A pool’s reserves are not held as one balance per side: each side is a set of many small PoolSlice allocations (detailed in the next section). A swap consumes only an ordered few of them — the output slice list below — and leaves the rest untouched.

PoolRules_Swap then:

  1. requires the same pool state, input slice, output slice list, and slippage minimum that the request bound;
  2. recomputes the constant-product output on-ledger;
  3. requires the trader allocation to match the exact input and output legs;
  4. settles the trader and pool allocations atomically; and
  5. replaces PoolState and only the reserve slices touched by the swap.

The operator cannot lower the signed output or swap against another snapshot.

Read:

  • Rules: PoolRules.daml
  • Math: Pricing
  • Proofs: testPoolSwapViaRequestSwap and testRealRegistryDvpSwapSettles

The pool is split so each concern remains small:

Contract or module Responsibility
Pool Immutable pool configuration and stable pool id
PoolState Aggregate reserves, LP supply, status, and fee accrual
PoolSlice One side-specific reserve amount and its backing V2 allocation
PoolRules Swap, pause/resume, and reconciliation choices
PoolLiquidityRules Add/remove request and settlement choices
PoolModel, PoolExecution Pure pricing, slice selection, and assembly helpers

Add liquidity is DvP: base and quote enter the pool in the same transaction in which the provider receives LP tokens. Remove is the reverse. If the base/quote registry admin differs from the LP registrar, the choice performs one settlement batch per admin and passes each registry its own choice context.

Reserve slices are committed = true with settlementDeadline = None, so under the V2 withdrawal rule no one — not even the LP — can unilaterally call Allocation_Withdraw on a routine slice. The operator holds custody: it is the settlement executor and the only party that can release reserves, and routine LP redemption needs the operator and LP registrar together. If either disappears, the reference has no trustless LP exit.

This is a deliberate long-lived-custody choice. The full rationale — including why simply adding a deadline would not give holders an exit — is in Liquidity and Custody.

An order has two objects: an operator-signed Order containing market terms and a trader-authored allocation containing the reserved funds.

flowchart LR
  I["OrderFundingRequest"] -->|Bind| P["Pending Order + allocation request"]
  P -->|wallet Allocate| A["V2.Allocation"]
  P -->|Order_Fund| F["Funded Order"]
  A --> F
  F -->|OrderMatchExecution_Execute| S["atomic fill"]
  S -->|partial| N["remainder + next allocation"]

OrderMatchExecution_Execute fetches both orders and rechecks pair, side, price, quantity, expiry, and bound allocation. It settles the fill and creates any remainders in the same transaction. Expiring orders are committed through their deadline. GTC orders are uncommitted so the trader can withdraw even if the operator disappears.

The operator still chooses which eligible orders to match and when. On-ledger checks prevent invalid fills; they do not prove fair arrival ordering or prevent operator censorship and reordering. See Ordering and MEV.

Read:

An RFQ records a trader request and dealer quotes. Rfq_Accept jointly requires the trader and operator, records the ranking in a PolicyReceipt, and creates a MatchedTrade. Each counterparty then authors an allocation and MatchedTrade_Settle groups the transfer legs by registry admin before calling that admin’s settlement factory.

The accepted RFQ state does not mean value moved. Settlement occurs only after both allocations exist and the matched-trade settle succeeds.

Read:

Factory contract ids are not sufficient. Before a registry choice, the operator fetches that admin’s choice context and disclosed contracts off-ledger. Context is keyed by admin, not by list position. Each per-admin batch receives only the legs, factory, and extraArgs for that admin; all required disclosures accompany the single Canton submission.

The evidence is intentionally both positive and negative:

  • testDvpSettleThreadsBothAdminContexts succeeds with distinct contexts;
  • testDvpSettleRequiresPoolAdminContext removes only the pool-admin context and must fail;
  • testDvpSettleRequiresLpRegistrarContext removes only the LP context and must fail; and
  • testMatchedTradeSettlesPerAdminLegSubsets proves each admin receives its exact transfer-leg subset against the upstream context-requiring registry.

Read Choice Context for backend assembly and submission details.

Public source can contain declarations that are not active workflow APIs. Code comments use one of these markers:

  • [COMPAT] means a type, field, template, or fixture remains for Daml package lineage. Active code does not rely on it unless the comment says otherwise.
  • [RETIRED] means a choice remains callable in the package shape but always rejects and names the replacement.
  • [POLICY] marks active behavior that deliberately ignores a field for a stated policy reason.

The CantonDex.Instrument.* module family is a [COMPAT] standalone lifecycle sample. It is not Token Standard V2 and is not used by active DEX workflows. Active value flows use CantonDex.Registry.V2 or another V2 registry. Order_Adjust and Order_RecordPartialFill are [RETIRED]; use OrderMatchExecution_Execute.

If you want to understand Read next
Component and authority boundaries Architecture
Every workflow step Workflows
Reserve representation and LP exits Liquidity and Custody
Registry assumptions and context Registry Integration
What tests prove Testing
Deliberate limitations Non-goals

Next canonical step: Architecture. Use the other rows above as topic references when you need their detail.