Trace one AMM swap from formula to Daml settlement
This tutorial is for an AMM developer who knows x*y=k but is new to Canton
and Daml. You will trace one exact-input swap through the repository, run three
focused Daml tests, and learn what each test does—and does not—prove.
This is a code-reading tutorial, not a live-network deployment. It uses the Daml Script runner so you can focus on contract state, authority, and value movement before adding a participant, wallet, or HTTP backend.
Before you begin
Section titled “Before you begin”Read the Canton and Daml primer, then install the Daml prerequisites from Getting started.
From the repository root, build the trading DAR once:
dpm install 3.5.2bash scripts/build-trading-surface.shA successful build ends with:
canton-dex-trading built successfully.You will work with these files:
| Question | File |
|---|---|
| Where is the constant-product formula? | PoolModel.daml |
| Where are quote binding and swap settlement enforced? | PoolRules.daml |
| What is pool configuration versus mutable state? | Pool.daml, PoolState.daml |
| Where is reserve value represented? | PoolSlice.daml and Registry/V2.daml |
| Which tests should I read first? | PoolRoundingTests.daml, PoolWorkflowTests.daml, RealRegistryDvpTests.daml |
1. Start from the familiar formula
Section titled “1. Start from the familiar formula”For reserve-in x, reserve-out y, exact input dx, and fee f, the usual
constant-product output is:
dxAfterFee = dx × (1 - f)dy = y × dxAfterFee / (x + dxAfterFee)The repository implements that in
constantProductOut:
constantProductOut reserveIn reserveOut feeBps inputAmount = let amountInAfterFee = floorDiv (floorMul inputAmount (intToDecimal (10000 - feeBps))) 10000.0 in floorDiv (floorMul amountInAfterFee reserveOut) (reserveIn + amountInAfterFee)Two details matter:
- fees use basis points, so 30 means 0.30%;
- multiplication and division round down on pool payouts so fixed-scale decimal rounding cannot make the pool pay more than the exact result.
The full input—not only amountInAfterFee—is later added to the input reserve.
That is how the fee remains in the pool and accrues to LPs.
Run the arithmetic proof
Section titled “Run the arithmetic proof”From trading-tests/:
cd trading-testsdpm test -p testSwapOutputRoundsDownToKeepConstantProductExpected result:
testSwapOutputRoundsDownToKeepConstantProduct: okRead that test in
PoolRoundingTests.daml.
It creates a zero-fee 1000/1000 pool, swaps 7 units, and asserts that the
post-swap product is not lower than the pre-swap product. Zero fees remove the
usual fee cushion, exposing a one-unit-of-precision overpayment.
This test proves arithmetic plus real Daml settlement in its fixture. It does not exercise the backend or browser.
2. Replace one “pool contract” with four responsibilities
Section titled “2. Replace one “pool contract” with four responsibilities”An EVM AMM often places configuration, reserves, and swap functions on one pair contract. This reference separates them:
flowchart TD Pool[Pool immutable instruments, parties, fee] State[PoolState aggregate reserves, LP supply, status] Rules[PoolRules request, validate, settle, pause] Slices[PoolSlice set committed reserve inventory] Holding[Token Standard Holding / Allocation actual value backing] Pool --> State Pool --> Rules State -->|prices against totals| Rules Slices -->|must sum to reserves| State Slices --> Holding Rules -->|settles and rolls forward| Slices
Open the files and identify these fields:
Pool.poolId, the two instrument IDs,lpInstrumentId, andfeeBpsare stable configuration.PoolState.reserves,totalLpSupply, andstatusare the small global state every reserve-changing operation serializes through.- each
PoolSlicenames one side, amount, and committed allocation contract ID; PoolRulesis operator-signed and exposes nonconsuming choices. The rules contract stays active while a swap archives and recreates state and slices.
The accounting invariant is:
PoolState.baseAmount = sum(active base PoolSlice.amount)PoolState.quoteAmount = sum(active quote PoolSlice.amount)PoolState makes pricing efficient; slices connect those totals to reserved
Token Standard value. A reserve number without matching slices would be only
an operator assertion, not spendable inventory.
3. See why quoting is not authorization
Section titled “3. See why quoting is not authorization”The browser can compute or request a quote without moving funds. A settle needs an allocation specification that binds the trader to exact transfer-leg sides and one pool snapshot.
The operator exercises PoolRules_RequestSwap. Its result contains:
data PoolRules_RequestSwapResult = PoolRules_RequestSwapResult with settlement : V2.SettlementInfo allocationSpec : V2.AllocationSpecification quoteBinding : Optional SwapQuoteBindingThe quoteBinding records the state and slice contract IDs plus the trader’s
minimum output:
data SwapQuoteBinding = SwapQuoteBinding with expectedPoolId : PoolId poolStateCid : ContractId PoolState inputSliceCid : ContractId PoolSlice outputSliceCids : [ContractId PoolSlice] minOutputAmount : DecimalContract IDs are part of the concurrency control. If another swap archives the
bound PoolState or a bound slice first, the old quote cannot settle. The
operator must produce a fresh request; it cannot reuse the trader’s authority
against different state.
Inside PoolRules_RequestSwap, Daml builds the specification from the prepared
input and output legs:
allocationSpec = Utils.mkIteratedAllocationSpecification pool.admin swapperAccount None (prepared.preparedSwapInLeg :: prepared.preparedOutputDelivery.legs) None FalseThe operator prepares this specification, but the trader’s wallet authors the allocation against it. Preparing terms and authorizing funds are separate actions.
4. Follow authority, not HTTP calls
Section titled “4. Follow authority, not HTTP calls”The essential swap has three ledger steps:
| Step | Daml action | Required authority | Result |
|---|---|---|---|
| Prepare | exercise PoolRules_RequestSwap |
operator | exact settlement info, allocation spec, and quote binding |
| Allocate | exercise AllocationFactory_Allocate |
trader, plus any registry-required context/actors | trader’s input value locked for those terms |
| Settle | exercise PoolRules_Swap |
operator | input and output settle atomically; state/slices roll forward |
The dApp and backend orchestrate those steps, but neither changes who controls them. A frontend button cannot substitute operator authority, and an operator API token cannot substitute the trader’s wallet authority on a self-custodial allocation.
Run the choreography proof
Section titled “Run the choreography proof”cd trading-testsdpm test -p testPoolSwapViaRequestSwapExpected result:
testPoolSwapViaRequestSwap: okRead the named test in
PoolWorkflowTests.daml.
The most important three lines of the story are:
reqRes <- submit operator $ exerciseCmd rulesCid PoolRules_RequestSwap with ...bobInstr <- submit bob $ exerciseCmd factoryCid bobAllocateArgswapRes <- submit operator $ exerciseCmd rulesCid PoolRules_Swap with ...This is excellent authority and choreography documentation: operator, then
trader, then operator. Its MockRegistry fixture does not contain real
holdings, so this particular test does not prove balance conservation. The
file header says so explicitly.
5. Read the atomic settlement boundary
Section titled “5. Read the atomic settlement boundary”PoolRules_Swap recomputes the output from the bound pool snapshot and checks
that every supplied contract ID and the minimum output match the quote binding.
It then calls the registry’s batch settlement factory:
settleResult <- exercise factoryCid V2.SettlementFactory_SettleBatch with settlement transferLegs = swapInLeg :: outDel.legs allocations = swapperFinalized :: inputFinalized :: outDel.sliceFinalizeds actors = [operator] extraArgsBecause this is nested in one Daml transaction, settlement and the following state changes are atomic. After successful settlement the choice:
- rolls the input reserve allocation forward with the full input added;
- consumes enough output slices to pay the trader and recreates any leftover boundary slice;
- asserts that slice deltas equal reserve deltas;
- archives the old
PoolStateand creates the successor reserves.
If batch settlement fails, the state and slice updates do not commit. If a reserve/slice assertion fails, the value settlement does not commit either.
6. Run the real-holding proof
Section titled “6. Run the real-holding proof”Now run the test whose fixture creates actual Token Standard holdings and uses an upstream context-requiring V2 registry:
cd trading-testsdpm test -p testRealRegistryDvpSwapSettlesExpected result:
testRealRegistryDvpSwapSettles: okRead the named test in
RealRegistryDvpTests.daml.
It proves more than the choreography test:
- the request’s sender side is the exact input instrument and amount;
- the receiver side is a positive amount of the output instrument;
- changing the signed receiver amount by
0.0000000001makes settlement fail; - the trader’s input holdings back the allocation;
- reserves move in the expected directions;
- the trader receives an output
Holding.
It still runs in Daml Script. It does not prove package upload, JSON API serialization, wallet compatibility, network topology, or browser behavior.
7. Promote the proof to a real Canton process
Section titled “7. Promote the proof to a real Canton process”Return to the repository root and run the default live proof:
bash scripts/run-dpm-sandbox-proof.shThis starts the real Canton sandbox bundled with the pinned DPM SDK, uploads the Token Standard and DEX package closure, and drives add → swap → remove through the JSON Ledger API. The final checkpoint is:
==> PASS: portable live-Canton proof completed The throwaway sandbox is now stopping; no persistent ledger state remains.You have now crossed two boundaries that Daml Script did not test: a Canton process started, and the JSON Ledger API accepted the package and value-flow commands. The script uses one unrestricted authentication-disabled sandbox user, but three Canton parties: operator/admin/LP registrar share the bootstrap party, while LP/trader and swapper are distinct counterparties. It still does not start the operator HTTP server, browser, or wallet. Those omissions are deliberate; see Local Canton from a clean clone for the proof matrix and the optional persistent environments.
8. Connect the code to the UI without overstating it
Section titled “8. Connect the code to the UI without overstating it”After the Daml tests pass, run the browser preview from Getting started. On the Trade page:
- change the BTC or USDC input and observe the quote;
- open browser developer tools and find the quote/request calls;
- connect Mock Wallet and inspect the wallet intent logged to the console;
- notice that its returned
#mock-…:0value is not the allocation created in the Daml test.
The UI shows how a real integration is orchestrated. The Daml tests show what the contracts enforce. Only a live participant plus compatible wallet joins the browser orchestration and on-ledger settlement boundaries in one validation.
9. Use the same reading pattern for other AMM flows
Section titled “9. Use the same reading pattern for other AMM flows”You can now trace add and remove liquidity with the same questions:
| Question | Add/remove liquidity answer |
|---|---|
| What computes the economic amounts? | pool ratio, LP supply, and conservative rounding in PoolModel.daml |
| What records intent? | LiquidityAllocationRequest |
| Who authorizes base/quote or LP value? | the liquidity provider through allocation factory choices |
| Who executes? | operator and LP registrar on the liquidity rules choice |
| What makes it atomic? | one settlement batch combines deposits/redemption with LP mint/burn |
| Which real-value test should I read? | testDvpAddLiquidity, testDvpRemoveDeliversToHolder, and their negative cases in PoolLiquidityRulesTests.daml |
Then read Liquidity and custody for the full slice design and LP tokens for issuance and redemption.
Completion checklist
Section titled “Completion checklist”You have completed this tutorial when you can point to:
- the function that computes
amountOut; - the contracts that separate pool configuration, aggregate state, and reserve backing;
- the choice that builds the trader’s exact allocation specification;
- the line where the trader—not the operator—authors the allocation;
- the nested batch-settlement choice;
- one mock-registry choreography test and one real-holding value test;
- the final checkpoint of the DPM sandbox proof;
- the reason passing the Daml and sandbox proofs is not yet a live browser and external-wallet dApp.
Next canonical step: 15-minute design tour. Use Liquidity and custody and Local Canton from a clean clone as topic references.