Contract: SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-swap-router-v-1-1
Audit date: 2026-06-08
Scope: Router source plus the directly relevant interfaces and swap paths in
dlmm-pool-trait-v-1-1, sip-010-trait-ft-standard-v-1-1, and
SP1PFR4V08H1RAZXREBGFFQ59WB739XM8VVGTFSEA.dlmm-core-v-1-1.
The router is stateless, permissionless, and intentionally delegates custody, pool validation, token-pair validation, and balance mutation to a fixed core contract. No high or critical issue was identified.
The main risk is version coupling: every swap path is hard-coded to one core version. Once a pool migrates to another core, this router cannot service it, and existing integrations fail atomically until they adopt another router. Two lower-severity integration risks arise from ambiguous batch semantics and from performing the empty-list check only after the fold.
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 1 |
| Low | 1 |
| Informational | 2 |
The router declares no data-var and no data-map. It cannot retain user
funds, allowances, routes, privileged principals, pause state, or configuration.
All mutations occur in the fixed core, selected pool, and selected SIP-010 token
contracts during nested calls.
| Lines | Trait | Purpose |
|---|---|---|
| 5 | dlmm-pool-trait |
Dynamic pool interface used to read the active bin and passed to the core |
| 6 | sip-010-trait |
Dynamic token interfaces passed to the core |
| Lines | Constant(s) | Meaning / mutation |
|---|---|---|
| 9-19 | ERR_* |
Router-local error responses; immutable |
| 22-23 | MIN_BIN_ID=-500, MAX_BIN_ID=500 |
Bounds only the caller's expected-bin-id; immutable |
| 26-27 | MIN_STEPS=1, MAX_STEPS=319 |
Bounds simple range traversal; immutable |
| 30-44 | STEP_INDEX_RANGE |
Static 0..319 list sliced to drive repeated swaps; immutable |
| 178-179, 208, 246, 303, 333 | Fixed core principal | All actual swaps target dlmm-core-v-1-1; immutable |
The router reads pool-trait.get-active-bin-id. The core independently reads
pool-trait.get-pool-for-swap, checks that the pool is registered and enabled,
checks that the pool is managed by the current core, and checks the supplied
token trait principals against the pool's registered tokens (core lines
1284-1287, 1347-1349, 1427-1430, 1490-1492).
The core performs all token transfers and pool mutations. Because the core uses
tx-sender as the swap caller (core lines 1283 and 1426), the original
transaction sender remains the source/recipient when the router calls the core.
All eight public functions are open to any caller. The router has no owner-only or administrator path. All failures revert the complete transaction.
| Function | Lines | Preconditions / asserts | Mutations and external calls |
|---|---|---|---|
swap-multi |
48-59 | Non-empty list; cumulative unfavorable-bin movement <= max-unfavorable-bins; each item validates expected bin and minimum output |
For each independent item, reads pool active bin and calls fixed core swap-x-for-y or swap-y-for-x |
swap-x-for-y-same-multi |
62-77 | Non-empty list; cumulative unfavorable movement bound; aggregate Y minimum; per-item expected bin and Y minimum | Splits one X input amount sequentially across listed pools/bins; calls fixed core |
swap-y-for-x-same-multi |
80-95 | Symmetric to X-for-Y path | Splits one Y input amount sequentially across listed pools/bins; calls fixed core |
swap-simple-multi |
98-107 | Non-empty list; each item validates max-steps and minimum output |
Executes up to five independent simple range swaps; calls internal public functions |
swap-x-for-y-simple-multi |
110-116 | Delegates with MAX_STEPS |
Calls swap-x-for-y-simple-range-multi |
swap-y-for-x-simple-multi |
119-125 | Delegates with MAX_STEPS |
Calls swap-y-for-x-simple-range-multi |
swap-x-for-y-simple-range-multi |
128-142 | 1 <= max-steps <= 319; aggregate output >= min-dy |
Repeatedly reads active bin and calls fixed core until input is exhausted or step limit reached |
swap-y-for-x-simple-range-multi |
145-159 | 1 <= max-steps <= 319; aggregate output >= min-dx |
Symmetric repeated core calls |
| Function | Lines | Role |
|---|---|---|
fold-swap-multi |
161-188 | Executes independent swaps and accumulates unfavorable-bin distance |
fold-swap-x-for-y-same-multi |
190-226 | Spends remaining X across pools, accumulating Y |
fold-swap-y-for-x-same-multi |
228-264 | Spends remaining Y across pools, accumulating X |
fold-swap-simple-multi |
266-287 | Executes up to five independent simple swaps |
fold-swap-x-for-y-simple-multi |
289-317 | Walks active bins for one X-for-Y pool |
fold-swap-y-for-x-simple-multi |
319-347 | Walks active bins for one Y-for-X pool |
abs-int |
350-352 | Converts signed expected/actual bin delta to a uint |
There are no read-only functions.
The router does not transfer tokens directly. Token movements happen in the
fixed core: input token moves from tx-sender to each selected pool and output
token moves from each selected pool to tx-sender.
| Public function | Possible token movements | Recommended caller post-conditions |
|---|---|---|
swap-multi |
Multiple independent X->Y or Y->X transfers, potentially across unrelated token pairs | For every item, cap the exact input token sent by caller and require the exact output token received by caller. Do not rely only on router min-received when composing the call. |
swap-x-for-y-same-multi |
Up to amount X sent across listed pools; Y received from each |
Cap total X sent by caller to amount; require at least min-y-amount-total Y received by caller. |
swap-y-for-x-same-multi |
Up to amount Y sent; X received |
Cap total Y sent; require aggregate X received. |
swap-simple-multi |
Up to five independent swaps, each with its own pair and amount | Attach per-item input caps and output floors. There is no batch-wide aggregate output floor. |
swap-x-for-y-simple-multi |
Up to x-amount X sent to one pool; Y received |
Cap X sent to x-amount; require Y received >= min-dy. |
swap-y-for-x-simple-multi |
Up to y-amount Y sent; X received |
Cap Y sent to y-amount; require X received >= min-dx. |
swap-x-for-y-simple-range-multi |
Same as simple X->Y, bounded by max-steps |
Cap X sent; require Y floor; bind the pool and token principals in the transaction payload. |
swap-y-for-x-simple-range-multi |
Same as simple Y->X, bounded by max-steps |
Cap Y sent; require X floor; bind the pool and token principals in the transaction payload. |
Post-conditions are especially useful for swap-multi and swap-simple-multi
because their item lists may contain unrelated token pairs. The contract's own
minimum-output checks provide atomic slippage protection, but transaction-level
post-conditions add an independent envelope around all nested transfers.
| Capability | Authority | Enforcement |
|---|---|---|
| Invoke any router public function | Any principal | No router access check |
| Select pools, token traits, amounts, directions, and slippage values | Transaction caller | Function arguments |
| Validate pool registration and enabled status | Fixed core | Core check-pool-validity |
| Validate token/pool coherence | Fixed core | Core compares token trait principals to pool metadata |
| Move caller tokens / send pool tokens | Fixed core and pool contracts | SIP-010 transfer and pool-transfer paths |
| Change router core target, pause router, or administer router | Nobody | No state or privileged functions exist |
| Migrate a pool to another core | Core administrators, outside router scope | Core migration mechanism |
There is no oracle dependency in the router. Prices, fees, balances, and active bin state come from the pool/core system. There is no router pause or kill switch. Disabling a pool in the core prevents swaps through this router.
| Check | Result |
|---|---|
tx-sender vs contract-caller |
Router uses neither. The fixed core intentionally uses tx-sender so nested router calls debit/credit the original user. This is powerful composability behavior and should remain documented. |
| Panic unwraps | None in router. All unwraps are error-returning unwrap!. |
| Arithmetic overflow / underflow | No practical router overflow found under verified-pool invariants. Remaining-input subtraction aborts atomically if a core ever returns in above the supplied amount. Expected bin is bounded; verified pools keep active bins within core bounds. |
as-contract / principal escalation |
None. |
| Trait conformance gaps | Dynamic traits are checked by Clarity; the core additionally verifies pool registration and token principal coherence. |
| Reentrancy / retained custody | No retained state or custody exists. Nested calls either complete or revert atomically. |
| Slippage controls | Explicit minimum outputs exist. Expected-bin/cumulative unfavorable-bin controls exist only in the non-simple paths. |
| Upgradeability | Fixed-core calls create a migration compatibility boundary; see BF-01. |
| ID | Severity | Function | Line | Finding | Recommended fix |
|---|---|---|---|---|---|
| BF-01 | Medium | All swap folds | 178-179, 208, 246, 303, 333 | Hard-coded core creates a pool-migration availability failure. Every swap is sent to dlmm-core-v-1-1. The core rejects pools whose core-address is no longer current-contract. Therefore, after a legitimate pool migration, this router's calls for that pool revert even though the pool remains valid under the successor core. Existing contracts and users integrating this router must migrate in lockstep. |
Resolve the active core from a governed registry or versioned router dispatcher, or accept a constrained core trait and validate it against the pool's registered core-address. Publish an explicit deprecation/migration signal for immutable routers. |
| BF-02 | Low | swap-simple-multi |
98-105, 266-285 | The batch has no aggregate slippage bound and its items are independent, not chained. Each item has its own amount and minimum output. A caller or integrator that interprets "multi" as a routed path or expects a batch-wide output guarantee can construct an economically unintended transaction. | Rename/document as an independent batch, or add a separate routed/aggregate variant that enforces token continuity and a final output floor. |
| BF-03 | Informational | Four fold-backed entrypoints | 53-56, 68-74, 86-92, 102-104 | Empty-list rejection occurs after executing the fold. An empty fold has no external side effects and the transaction correctly fails, but the ordering obscures the precondition and spends avoidable analysis/runtime work. | Assert list length before evaluating the fold, using a preceding begin or helper. |
| BF-04 | Informational | Simple range folds | 289-347 | The fold index is unused. bin-id drives iteration count only; each iteration re-reads the active bin. This is correct but makes the traversal invariant less obvious and can trigger repeated zero-progress core calls at a terminal/empty bin until max-steps is exhausted. |
Rename the argument to _step-index if supported by local style, document the repeated-active-bin behavior, and optionally stop when a call returns {in: u0, out: u0}. |
The router cannot redirect calls because it has no state and every call site
contains the same literal core principal. The core's swap paths require
pool-data.core-address == current-contract (core lines 1349 and 1492).
Consequently, migration is a deterministic compatibility break for this router,
not merely a documentation concern. No funds are lost: the transaction reverts.
The impact is availability and integration breakage.
swap-simple-multi calls the simple range function separately for each item.
Outputs are appended to a result list but never passed as the next item's input
(lines 280-285). This is useful for batching independent swaps, but it differs
from the common meaning of a multi-hop router. Per-item min-received protects
properly configured callers; the risk is integrator misuse.
- Stateless router: no retained balances, owner keys, mutable routes, or stale allowance state.
- All user-facing unwraps return explicit errors rather than panic.
- Core validates pool registration/enabled status and token/pool coherence.
- Every fold propagates errors with
try!, preserving atomicity. - Same-pair multi paths enforce both per-pool and aggregate output floors.
- Simple range paths enforce bounded iteration and aggregate output floors.
- No high or critical issue was found, so no private responsible-disclosure action was required before publishing this report.
- Migrate a pool to a successor core and prove this router reverts while the successor router succeeds.
- Confirm
swap-simple-multicannot accidentally chain outputs and document the expected independent-batch behavior. - Exercise empty lists for all four fold-backed public functions.
- Exercise terminal active bins with empty balances and
max-steps=319. - Property-test that every successful same-pair multi swap spends no more than its initial amount and satisfies both per-item and aggregate minimums.