Skip to main content

How swaps resolve

A swap first looks for a user on the opposite side, then uses reserves behind any older same-direction positions. The caller chooses whether unfilled input can queue or must revert.

The examples use an illustrative hub containing USDC and USDS. Amounts in the text diagrams are human-readable tokens; function inputs and returned token amounts use each token's native decimals.

Direct swap flow​

swapOrQueue(offerAsset, wantAsset, amount, minAmountOut, deadline):

1. Check the deadline, listed assets and route.
Allowed single-leg routes: hub → hub, hub → spoke, spoke → hub.
Check protocol/spoke pauses, wind-down direction and the offer asset's inflow checks.

2. Normalize the native offer budget and pull only whole normalized units.
Sub-unit offer-token dust stays with the caller.

3. Fill from the exact-opposite queue, oldest first.
Pay offerAsset to those owners and use their escrowed wantAsset as output.
A failed payout parks that position; successful peer matches move no reserves.

4. If input remains, visit up to eight older same-direction positions.
Fill them from route reserves; park any position whose payout fails.

5. If that same-direction queue is now empty, fill from remaining route reserves.

6. Convert the fill to native wantAsset and compare it with minAmountOut.
Revert if the immediate output is below the minimum.

7. Send the immediate output to the caller.

8. Escrow remaining offerAsset at the tail, subject to queue size and minimum rules.
Return amountIn, amountOut, amountInQueued and the new positionId.

amountIn and amountInQueued are native offer-token amounts; amountOut and minAmountOut are native wanted-token amounts. positionId is 0 if nothing queued. The caller owns a queued position, receives its later payouts, and is the only address that can cancel it.

Any revert undoes all steps, including earlier fills or parking in that transaction. For a complete immediate fill, use swapExactInput, or set swapOrQueue's minimum to floor(amount / S_offer) * S_want.

What a reserve fill moves​

Reserve settlement in steps 4 and 5 uses the same route-specific accounting:

RouteEffect
hub → hubWanted hub reserve decreases; offered hub reserve increases. No DLRS is minted or burned
hub → spokeWanted spoke reserve decreases; offered hub reserve and that spoke's DLRS side increase
spoke → hubWanted hub reserve and offered spoke's DLRS side decrease; offered spoke reserve increases

A reserve-backed spoke-to-hub fill is capped by the wanted hub reserve and the spoke's DLRS side above its protected minimum, whichever is smaller. Exact-opposite peer matches use escrow instead and do not consume that allocation.

Example: full instant swap from reserves​

Before:
Hub USDC reserve: 10,000
Hub USDS reserve: 5,000
Both directed queues empty

User offers 3,000 USDC for USDS:
No opposite peer position or earlier same-direction position.
USDS reserves cover the entire output.
User receives 3,000 USDS; no queue position is created.

After:
Hub USDC reserve: 13,000
Hub USDS reserve: 2,000

With the illustrated token decimals, the native input is 3_000e6 USDC and full output is 3_000e18 USDS. swapExactInput returns these two amounts and pays the specified recipient.

Example: peer match against the opposite queue​

Before:
Hub USDS reserve: 0
USDS → USDC queue: Alice offers 3,000 USDS

User offers 3,000 USDC for USDS:
Alice receives the user's 3,000 USDC and her position closes.
The user receives Alice's escrowed 3,000 USDS.

After:
Hub reserves unchanged
Alice's position removed

If Alice cannot receive USDC, her position is parked with its USDS escrow untouched. The swap continues to other available liquidity; it does not convert her escrow into receipts.

Example: partial fill with queue​

Before:
Hub USDC reserve: 10,000
Hub USDS reserve: 1,000
Both directed queues empty

User offers 3,000 USDC through swapOrQueue with minAmountOut = 0:
Pull 3,000 USDC.
Pay 1,000 USDS from reserves.
Escrow 2,000 USDC as a new USDC → USDS position owned by the caller.

After:
Hub USDC reserve: 11,000
Hub USDS reserve: 0
USDC → USDS queue: caller offers 2,000 USDC

The returned tuple is (3_000e6, 1_000e18, 2_000e6, positionId) using the illustrated decimals. QueueJoined reports the escrow as 2_000e6 normalized units; for an 18-decimal offer token its event amount would differ from the native queued amount.

Had the user required 3_000e18 native USDS immediately, MinAmountNotMet would revert the call. swapExactInput would instead reject the incomplete fill with InsufficientLiquidity; neither would leave funds queued.

Example: the queue gets filled​

Continue from the previous example:

Someone deposits 5,000 USDS into the hub:
Hub USDS reserve becomes 5,000.
The deposit receiver gets 5,000 DLRS.
The hub deposit does not itself settle the waiting queue.

Anyone calls processQueue(USDC, USDS, maxPositions):
The queued owner receives 2,000 USDS and the position closes.
Its 2,000 USDC of escrow enters hub reserves.

After:
Hub USDC reserve: 13,000
Hub USDS reserve: 3,000
USDC → USDS queue empty

A later same-direction swap can also settle older positions before drawing reserves. A matching opposite swap can pay them peer to peer. Deposits into a spoke settle the lines enabled by the new liquidity, up to eight visits per line. See How much settles per transaction.

Aggregator flow​

swapExactInput(offerAsset, wantAsset, amount, minAmountOut, recipient, deadline) uses the same peer matching and FIFO reserve logic, with a full-fill requirement:

1. Check deadline and recipient; validate the route and offered-token inflow.
2. Pull the rounded native input.
3. Match opposite positions, settle earlier same-direction positions, then use reserves.
4. Revert InsufficientLiquidity unless the full normalized input filled.
5. Compare the native output with minAmountOut.
6. Send the output to recipient; return native (amountIn, amountOut).

It never queues. recipient can be the end user or the integration; the zero address and DollarStore itself are rejected. getSwapQuote estimates liquidity after the same bounded queue work, assuming all transfers succeed. Simulate the actual call with its caller and recipient to check execution, including issuer restrictions and payouts to queued owners.

Spoke-to-spoke flow​

swapSpokeToSpoke(offerAsset, wantAsset, via, amount, recipient, deadline) combines two single-leg routes for each hub token in via:

offered spoke → hub token → wanted spoke

The contract validates the routes, pulls the rounded offered input once, and tries the hub tokens in the caller's order. Each entry carries up to what both legs can fill; any remainder moves to the next entry. The offer token must pass its inflow check. A hub token failing its own inflow check is skipped, because the second leg would absorb it into reserves.

Both legs follow peer matching and reserve FIFO, so positions on the lines used can fill or be parked. When both legs use reserves, aggregate hub reserves end unchanged and hub-side allocation moves from the offered spoke to the wanted spoke.

The call pays native wanted tokens to recipient and returns (amountIn, amountOut). It fills the whole normalized order or reverts, never queues, and has no separate minimum-output argument. The via order can affect fillability. For a single hub token, chain the two quotes to estimate liquidity; for a list or transfer checks, simulate the full call. The ordinary swap endpoints still reject a spoke-to-spoke pair. See Spoke to spoke.