# Static Analysis Report: Bitflow DLMM Swap Router v1.1

## Scope

- Bounty: `mpwizl08f7b54c2ff179`
- Contract: `SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-swap-router-v-1-1`
- Source reviewed: Hiro source endpoint for the deployed Clarity contract
- Local source SHA-256: `A051B7662C366771CCF7648AF3ECDE6C93A85702D3575C6987777C9EC102D5E4`
- Review date: `2026-06-04`

This review is a static analysis of the router contract only. The router delegates execution to the Bitflow DLMM core contract and caller-supplied pool and SIP-010 token traits, so external contract correctness is out of scope except where the router relies on their behavior.

## Executive Summary

No critical or high severity issue was identified in the router itself. The contract has no persistent storage, no administrative authority, and no direct token transfer code. Its primary safety controls are per-hop minimum output checks, aggregate minimum output checks for same-pair routes, maximum unfavorable-bin movement checks on expected-bin routes, bounded list lengths, and bounded step counts.

The main findings are informational or low risk:

1. The step-based simple swap folds over `STEP_INDEX_RANGE`, but the folded `bin-id` parameter is unused. Each iteration reads the pool's current active bin. This is likely intentional for repeated active-bin fills, but it can mislead integrators who expect the explicit step index to determine the bin being swapped.
2. The router accepts caller-supplied pool and token trait principals and does not pre-check that the token traits match the pool pair before calling the core swap. This may be enforced by the core or pool contracts, but the router itself does not bind those inputs.
3. `swap-simple-multi` enforces each item-level `min-received`, but has no aggregate final-output minimum across the whole list. Users should rely on post-conditions and route-level checks where possible.

## State Model

The router defines no persistent state.

| Storage primitive | Present | Notes |
| --- | --- | --- |
| `define-data-var` | No | No mutable contract variables. |
| `define-map` | No | No balances, approvals, allowlists, checkpoints, or per-user data. |
| NFT or FT definitions | No | No token issuance or custody in this router. |
| Admin fields | No | No owner, guardian, operator, pause, or upgrade state. |

The effective state transitions happen in external contracts:

- `pool-trait.get-active-bin-id`
- `dlmm-core-v-1-1.swap-x-for-y`
- `dlmm-core-v-1-1.swap-y-for-x`

The router is therefore a stateless coordinator. Its security properties depend on validating route parameters before and after external calls, then returning bounded result structures.

## Function Inventory

| Function | Visibility | Purpose | Key checks |
| --- | --- | --- | --- |
| `swap-multi` | Public | Multi-pool, mixed-direction route with expected bin per hop. | Non-empty route, expected bin range in fold, per-hop min output, aggregate unfavorable-bin cap, max 319 results. |
| `swap-x-for-y-same-multi` | Public | Multi-pool same-pair X to Y route. | Non-empty route, expected bin range, per-hop min output, total Y minimum, aggregate unfavorable-bin cap, max 319 results. |
| `swap-y-for-x-same-multi` | Public | Multi-pool same-pair Y to X route. | Non-empty route, expected bin range, per-hop min output, total X minimum, aggregate unfavorable-bin cap, max 319 results. |
| `swap-simple-multi` | Public | Up to five simple route segments with per-item max steps. | Non-empty list, per-item max-steps range, per-item min output, max 5 results. |
| `swap-x-for-y-simple-multi` | Public | Wrapper for simple X to Y range with max steps set to 319. | Delegates to range function. |
| `swap-y-for-x-simple-multi` | Public | Wrapper for simple Y to X range with max steps set to 319. | Delegates to range function. |
| `swap-x-for-y-simple-range-multi` | Public | Single-pool repeated X to Y swap up to caller max steps. | Max steps 1..319, slice check, total min Y output. |
| `swap-y-for-x-simple-range-multi` | Public | Single-pool repeated Y to X swap up to caller max steps. | Max steps 1..319, slice check, total min X output. |
| `fold-swap-multi` | Private | Executes one expected-bin mixed-direction hop. | Expected bin range, active bin read, core swap, result list cap, per-hop min output. |
| `fold-swap-x-for-y-same-multi` | Private | Executes one same-pair X to Y hop if input remains. | Expected bin range, active bin read, core swap, result list cap, per-hop min output. |
| `fold-swap-y-for-x-same-multi` | Private | Executes one same-pair Y to X hop if input remains. | Expected bin range, active bin read, core swap, result list cap, per-hop min output. |
| `fold-swap-simple-multi` | Private | Executes one simple route segment. | Max steps range, delegated range function, max 5 result cap. |
| `fold-swap-x-for-y-simple-multi` | Private | Repeated active-bin X to Y execution. | Stops when input remainder is zero, active bin read, core swap. |
| `fold-swap-y-for-x-simple-multi` | Private | Repeated active-bin Y to X execution. | Stops when input remainder is zero, active bin read, core swap. |
| `abs-int` | Private | Converts signed bin delta magnitude to uint. | Branches on sign. |

## Post-Condition Coverage Matrix

| Route family | User-side value bound | Price/bin movement bound | Length/overflow bound | Notes |
| --- | --- | --- | --- | --- |
| `swap-multi` | Per-hop `min-received` | Aggregate `max-unfavorable-bins`; expected bin range `-500..500` | Input and output lists capped at 319 | Does not provide a single final route output minimum because routes can mix direction and tokens. |
| `swap-x-for-y-same-multi` | Per-hop `min-received` plus `min-y-amount-total` | Aggregate `max-unfavorable-bins`; expected bin range `-500..500` | Input and output lists capped at 319 | Strongest route-level protection for same-pair X to Y routes. |
| `swap-y-for-x-same-multi` | Per-hop `min-received` plus `min-x-amount-total` | Aggregate `max-unfavorable-bins`; expected bin range `-500..500` | Input and output lists capped at 319 | Strongest route-level protection for same-pair Y to X routes. |
| `swap-simple-multi` | Per-item `min-received` via delegated range call | No expected-bin or unfavorable-bin check | Input and output lists capped at 5; per-item steps capped 1..319 | Users should use transaction post-conditions for final asset guarantees. |
| `swap-x-for-y-simple-range-multi` | Total `min-dy` | No expected-bin or unfavorable-bin check | Step range capped 1..319 | Uses active bin each iteration. |
| `swap-y-for-x-simple-range-multi` | Total `min-dx` | No expected-bin or unfavorable-bin check | Step range capped 1..319 | Uses active bin each iteration. |

Post-condition observations:

- All public list-entry routes reject empty input lists.
- All calls that append to result lists use `as-max-len?` before returning updated results.
- Expected-bin routes validate `expected-bin-id` against `MIN_BIN_ID` and `MAX_BIN_ID`.
- Simple range routes validate `max-steps` against `MIN_STEPS` and `MAX_STEPS`, then validate the sliced step range.
- The router does not define on-chain post-conditions itself. Callers should include transaction-level post-conditions for the input and output token principals they expect.

## Authority and Access-Control Matrix

| Capability | Who can call | Contract control | Risk notes |
| --- | --- | --- | --- |
| Execute swaps | Any principal | Open public router | Expected for a stateless router. Safety depends on caller parameters, post-conditions, pool/core behavior, and token traits. |
| Change configuration | Nobody | No config storage or setter | No owner/admin surface in router. |
| Pause or resume | Nobody | Not implemented | If an external pool/core issue exists, this router has no local pause mechanism. |
| Withdraw funds | Nobody in router | No custody paths | Router does not expose withdrawal and does not hold tracked balances. |
| Upgrade route logic | Nobody in router | Immutable deployed Clarity contract | Upgrades would require new deployment and integrator migration. |

The router does not contain privileged roles. I found no owner checks, allowlists, deny lists, emergency functions, protocol-fee controls, or direct custody operations.

## Clarity Best-Practice Review

| Area | Assessment |
| --- | --- |
| Response handling | Uses `try!` and `unwrap!` consistently for external calls and prior fold state. |
| Bounded iteration | Uses statically bounded lists: 319 route entries or steps, and 5 simple route segments. |
| Result length safety | Uses `as-max-len?` after appending results. |
| Input range checks | Validates expected bin IDs and max step counts in relevant paths. |
| Empty-list handling | Public multi-list entry points reject empty inputs. |
| State minimization | No persistent state, no admin state, no custody state. |
| External trait binding | Caller supplies pool and token traits. Router relies on downstream core/pool validation for pair consistency. |
| Readability | Overall clear, but the unused `bin-id` fold parameter in simple range folds is potentially misleading. |

## Findings

### LOW-01: Step-based simple folds ignore the folded `bin-id` parameter

Severity: Low / Informational

The private functions `fold-swap-x-for-y-simple-multi` and `fold-swap-y-for-x-simple-multi` receive a `bin-id` argument from folding over `STEP_INDEX_RANGE`, but never use it. Each iteration instead calls `pool-trait.get-active-bin-id` and passes the current active bin to the core swap.

Impact:

- If maintainers or integrators read `STEP_INDEX_RANGE` as an explicit list of bin IDs to traverse, the implementation can be misunderstood.
- The actual behavior is repeated active-bin execution until the input amount is exhausted or `max-steps` iterations have run.
- This is not necessarily unsafe, but it is worth documenting because the function comments say "through up to 319 bins" while the implementation does not use the supplied step value as a target bin.

Recommendation:

- Rename the unused fold parameter to `_step-index` or equivalent if Clarity style permits.
- Update comments to state that the range is an iteration limit and the active bin is fetched every iteration.
- If explicit bin traversal was intended, use the folded value as part of a bin check or pass it through to the core as appropriate.

### INFO-01: Router does not pre-bind token traits to the selected pool

Severity: Informational

All public swap functions accept caller-supplied `pool-trait`, `x-token-trait`, and `y-token-trait` values. The router passes those values to the core swap without first checking that the token traits are the exact pair associated with the pool.

Impact:

- If the core and pool contracts enforce pair matching, this is acceptable router design.
- If downstream validation were incomplete, a caller could submit mismatched traits and receive unexpected behavior from external contracts.
- Integrators should not treat the router alone as the source of truth for pool-token binding.

Recommendation:

- Document that callers should use canonical pool and token principals.
- Where feasible, expose or call pool metadata checks before the core swap, or document that the core swap is the enforcement point.
- User transactions should include post-conditions for the exact token principals and maximum spend/minimum receive expectations.

### INFO-02: `swap-simple-multi` has per-segment minimums but no aggregate final-output minimum

Severity: Informational

`swap-simple-multi` accepts up to five route segments and delegates each segment to a simple range function with its own `min-received`. Unlike the same-pair multi functions, it does not include an aggregate final-output minimum across the full list.

Impact:

- Per-segment checks are useful, but route builders that split a desired swap across multiple segments must compute conservative per-segment minimums.
- For complex routes, a caller may prefer same-pair multi functions or transaction-level post-conditions to express the final desired protection.

Recommendation:

- Document this distinction clearly for integrators.
- Consider adding a future route wrapper with a final aggregate output check where token direction and output asset are uniform enough to support it.

## Non-Findings

I specifically checked the following areas and did not identify an issue in the router:

- No mutable storage or maps that could be corrupted.
- No admin-only pathways, hidden privileged actions, or owner authorization bugs.
- No direct transfer or custody logic in the router.
- No unbounded loops; all folds are over bounded Clarity lists.
- No unchecked append growth; appended result lists are guarded.
- No empty-route acceptance in public list-based entry points.
- No unchecked `max-steps` in simple routes.

## Conclusion

The router is a compact stateless coordinator. Its controls are mostly appropriate for the role it plays: bounded routes, bounded results, checked external-call responses, per-hop minimum outputs, route-level total minimums for same-pair routes, and unfavorable-bin caps for expected-bin routes.

The most actionable improvement is documentation or cleanup around the step-based simple folds, where the folded `bin-id` is unused and the active bin is fetched dynamically on every iteration. The other observations are integration guidance: bind pool/token principals carefully and use transaction post-conditions, especially for `swap-simple-multi` routes.
