Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save silentgeckoaudit3801/1d1c6b7e8b0227f6e7d78555cc345ebb to your computer and use it in GitHub Desktop.

Select an option

Save silentgeckoaudit3801/1d1c6b7e8b0227f6e7d78555cc345ebb to your computer and use it in GitHub Desktop.
Static analysis: Bitflow DLMM swap router v1.1

Static Analysis: Bitflow DLMM Swap Router v1.1

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.

Executive summary

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

1. State model

Persistent state

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.

Traits

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

Constants

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

External state relied upon

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.

2. Function inventory

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

Private helpers

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.

3. Post-condition coverage matrix

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.

4. Authority / access-control matrix

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.

5. Clarity best-practice review

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.

6. Findings

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}.

BF-01 impact detail

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.

BF-02 impact detail

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.

Positive security properties

  • 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.

Suggested tests

  1. Migrate a pool to a successor core and prove this router reverts while the successor router succeeds.
  2. Confirm swap-simple-multi cannot accidentally chain outputs and document the expected independent-batch behavior.
  3. Exercise empty lists for all four fold-backed public functions.
  4. Exercise terminal active bins with empty balances and max-steps=319.
  5. Property-test that every successful same-pair multi swap spends no more than its initial amount and satisfies both per-item and aggregate minimums.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment