Skip to main content

How Swaps Resolve

This page explains the complete lifecycle of a swap, from input to output.

The examples swap USDC for USDS, the two hub assets. The mechanics are the same for any listed pair — where a spoke changes the accounting, it is called out.

Direct swap flow

When calling swap(offerAsset, wantAsset, amount, minAmountOut, tip, deadline):

1. Validate
→ deadline not passed, tip == 0, offerAsset != wantAsset
→ classify the route: hub->hub, hub->spoke or spoke->hub (spoke->spoke reverts)
→ offerAsset must be listed, unpaused, on peg, with a fresh price feed

2. Pull offerAsset from the user (exact amount, sub-unit dust stays in the wallet)

3. Fill from the exact-opposite (wantAsset -> offerAsset) queue
→ those owners receive offerAsset; their escrowed wantAsset goes to the swapper
→ peer-to-peer: touches no protocol reserves

4. If anything remains, settle the same-direction (offerAsset -> wantAsset) queue
from reserves, in FIFO order, up to 8 positions

5. If that queue is now empty, fill the rest from protocol reserves on the route

6. Require amountFilled >= minAmountOut, else revert

7. Send the filled wantAsset to the user

8. Escrow any remainder as a new position at the tail of the (offerAsset -> wantAsset) queue

Note: for "all or nothing" behavior, set minAmountOut to the full normalized amount, or use swapExactInput.

What a reserve fill moves

Step 5 is the only step that touches pool accounting, and what it moves depends on the route.

RouteEffect
hub → hubHub reserve of wantAsset down, hub reserve of offerAsset up. Reserve-neutral: no DLRS minted or burned
hub → spokeSpoke reserve of wantAsset down, hub reserve of offerAsset up, and the spoke's DLRS-side reserve up
spoke → hubHub reserve of wantAsset down, spoke reserve of offerAsset up, and the spoke's DLRS-side reserve down

For a spoke → hub swap, instant liquidity is therefore capped by both the hub reserve of the wanted asset and the spoke's DLRS-side reserve above its protected minimum, whichever is smaller.

Example: full instant swap from reserves

State before:
Hub USDC reserve: 10,000
Hub USDS reserve: 5,000
Both USDC->USDS and USDS->USDC queues: empty

User swaps 3,000 USDC → USDS:

1. 3,000 USDC pulled from the user
2. Opposite (USDS->USDC) queue is empty — no peer match
3. Same-direction queue is empty, so reserves are reachable
4. USDS reserve (5,000) covers 3,000 ✓
5. 3,000 USDS sent to the user

State after:
Hub USDC reserve: 13,000
Hub USDS reserve: 2,000

Example: peer match against the opposite queue

State before:
Hub USDS reserve: 0
USDS->USDC queue: [Alice: 3,000 USDS escrowed, wants USDC]

User swaps 3,000 USDC → USDS:

1. 3,000 USDC pulled from the user
2. Opposite queue match:
→ Alice receives 3,000 USDC, her position closes
→ her escrowed 3,000 USDS goes to the swapper
3. 3,000 USDS sent to the user

State after:
Hub USDS reserve: 0 (untouched — no reserves were involved)
USDS->USDC queue: empty

Two users with opposite needs settle against each other, with no reserves in the middle.

Example: partial fill with queue

State before:
Hub USDC reserve: 10,000
Hub USDS reserve: 1,000
Both queues: empty

User swaps 3,000 USDC → USDS with minAmountOut = 0:

1. 3,000 USDC pulled from the user
2. Opposite queue empty
3. Same-direction queue empty, reserves reachable
4. USDS reserve (1,000) < 3,000 → fill 1,000
5. 1,000 USDS sent to the user
6. Remaining 2,000 USDC escrowed at the tail of the USDC->USDS queue

State after:
Hub USDC reserve: 11,000
Hub USDS reserve: 0
USDC->USDS queue: [User: 2,000 USDC escrowed]

Had minAmountOut been set to 3,000, the whole call would have reverted with MinAmountNotMet instead — nothing filled, nothing queued.

Example: the queue gets filled

Continuing from above:

State:
Hub USDS reserve: 0
USDC->USDS queue: [User: 2,000 USDC escrowed]

Someone deposits 5,000 USDS into the hub:

Hub USDS reserve: 5,000
DLRS minted to the depositor: 5,000

The queued position is now settleable. It clears on the next swap that
would otherwise touch reserves on that route, or immediately if anyone calls:

processQueue(USDC, USDS, maxPositions)

→ User receives 2,000 USDS, position closes
→ their escrowed 2,000 USDC moves into hub reserves

State after:
Hub USDC reserve: 13,000
Hub USDS reserve: 3,000
USDC->USDS queue: empty

A hub deposit does not settle queues by itself — settlement is triggered by swaps on that route, by a spoke liquidity deposit, or by an explicit processQueue call. Since processQueue is permissionless, anyone can push it, and the caller chooses how many positions to drain. See how much settles per transaction.

Aggregator flow

The swapExactInput function follows the same fill logic with one difference: it never queues.

1. Check the deadline
2. Validate and classify the route; check the offer asset is unpaused and on peg
3. Pull offerAsset from the user
4. Fill from the opposite queue, then FIFO-settle, then reserves
5. If the fill is short of the full amount, REVERT (no partial fills, no queue)
6. Require filled >= minAmountOut, else revert
7. Send wantAsset to msg.sender

The call either executes in full or reverts — no intermediate states. Note that the output goes to the caller, so a router receives the tokens and forwards them itself.