Target bounty: mpwizl08f7b54c2ff179
Contract: SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-swap-router-v-1-1
Source reviewed: https://api.hiro.so/v2/contracts/source/SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD/dlmm-swap-router-v-1-1
Review type: static source review only. I did not execute mainnet swaps.
Responsible disclosure note: I found no high or critical severity issue in this pass. No private disclosure was required before this public report.
This router has no define-data-var, define-map, local balances, local escrow, or local owner-controlled state. It is a stateless orchestration layer over:
- the caller-supplied
dlmm-pool-trait - caller-supplied SIP-010 token traits
- the hard-coded DLMM core contract
SP1PFR4V08H1RAZXREBGFFQ59WB739XM8VVGTFSEA.dlmm-core-v-1-1
State can still be mutated through external calls to the DLMM core and the pool/token contracts that the core touches. Those mutations are outside this router's storage, but they are the important economic surface for callers.
Constants:
| Name | Value | Use |
|---|---|---|
ERR_NO_RESULT_DATA |
(err u2001) |
fold accumulator unwrap failure |
ERR_BIN_SLIPPAGE |
(err u2002) |
accumulated unfavorable bin movement exceeds caller cap |
ERR_MINIMUM_RECEIVED |
(err u2003) |
per-hop or range output below caller minimum |
ERR_MINIMUM_X_AMOUNT |
(err u2004) |
same-pair Y->X total output below minimum |
ERR_MINIMUM_Y_AMOUNT |
(err u2005) |
same-pair X->Y total output below minimum |
ERR_NO_ACTIVE_BIN_DATA |
(err u2006) |
get-active-bin-id returned none/error |
ERR_EMPTY_SWAPS_LIST |
(err u2007) |
empty public swap list |
ERR_RESULTS_LIST_OVERFLOW |
(err u2008) |
result list append exceeds declared max |
ERR_INVALID_BIN_ID |
(err u2009) |
expected bin is outside local min/max |
ERR_INVALID_MAX_STEPS |
(err u2010) |
range max steps outside [1,319] |
ERR_INVALID_STEP_INDEX_RANGE |
(err u2011) |
slice of STEP_INDEX_RANGE failed |
MIN_BIN_ID |
-500 |
lower bound for expected bin IDs |
MAX_BIN_ID |
500 |
upper bound for expected bin IDs |
MIN_STEPS |
u1 |
minimum range step count |
MAX_STEPS |
u319 |
maximum range step count |
STEP_INDEX_RANGE |
0..319 |
bounded loop source for range swaps |
Mutation model:
| Store / asset surface | Mutated by | Authority |
|---|---|---|
| Router storage | None | N/A |
| DLMM pool state | Calls routed through dlmm-core-v-1-1 |
Any caller can invoke router; actual pool/core rules decide |
| SIP-010 balances | Calls routed through dlmm-core-v-1-1 |
Caller should enforce postconditions |
Public functions:
| Function | Authority | Preconditions / asserts | State mutations / external calls |
|---|---|---|---|
swap-multi |
Open to any caller | non-empty swaps; each expected bin checked in fold; accumulated unfavorable bins must be <= caller cap; each hop output >= min-received |
fold calls DLMM core swap-x-for-y or swap-y-for-x once per row |
swap-x-for-y-same-multi |
Open | non-empty swaps; total Y >= min-y-amount-total; unfavorable <= cap; each hop output >= row minimum |
fold calls DLMM core swap-x-for-y while remaining X > 0 |
swap-y-for-x-same-multi |
Open | non-empty swaps; total X >= min-x-amount-total; unfavorable <= cap; each hop output >= row minimum |
fold calls DLMM core swap-y-for-x while remaining Y > 0 |
swap-simple-multi |
Open | non-empty swaps; each row validates its own max-steps and min output inside helper |
up to 5 independent simple/range swaps |
swap-x-for-y-simple-multi |
Open | delegated to range function using MAX_STEPS |
calls swap-x-for-y-simple-range-multi |
swap-y-for-x-simple-multi |
Open | delegated to range function using MAX_STEPS |
calls swap-y-for-x-simple-range-multi |
swap-x-for-y-simple-range-multi |
Open | max-steps in [1,319]; output Y >= min-dy |
folds up to max-steps, repeatedly calling DLMM core swap-x-for-y while remaining X > 0 |
swap-y-for-x-simple-range-multi |
Open | max-steps in [1,319]; output X >= min-dx |
folds up to max-steps, repeatedly calling DLMM core swap-y-for-x while remaining Y > 0 |
Private functions:
| Function | Purpose |
|---|---|
fold-swap-multi |
One explicit hop with per-row token traits, expected bin, amount, minimum, and direction |
fold-swap-x-for-y-same-multi |
Same token pair X->Y sequence with remaining input tracking |
fold-swap-y-for-x-same-multi |
Same token pair Y->X sequence with remaining input tracking |
fold-swap-simple-multi |
Up to 5 user-supplied simple/range swap entries |
fold-swap-x-for-y-simple-multi |
Repeated active-bin X->Y swaps until input consumed or steps exhausted |
fold-swap-y-for-x-simple-multi |
Repeated active-bin Y->X swaps until input consumed or steps exhausted |
abs-int |
Converts a signed bin delta to uint |
The router is open and trait-driven. Callers should rely on transaction postconditions in addition to the router's min-output checks, especially when route data comes from an aggregator or off-chain quote.
| Public function | Token movement surface | Suggested caller postconditions |
|---|---|---|
swap-multi |
Each row may spend the row's input token and receive the opposite token through the DLMM core | For each row: at-most spend of amount for the input token; at-least receive of min-received for the output token; exact contract principals for the intended tokens/pool where supported |
swap-x-for-y-same-multi |
At most amount X spent; total Y received across rows |
At-most spend amount X; at-least receive min-y-amount-total Y; constrain the token contracts to the intended X/Y pair |
swap-y-for-x-same-multi |
At most amount Y spent; total X received across rows |
At-most spend amount Y; at-least receive min-x-amount-total X; constrain the token contracts to the intended X/Y pair |
swap-simple-multi |
Up to 5 independent swap entries; not a chained route by default | For each entry: at-most input spend equal to row amount; at-least output receive equal to row min-received; do not assume row N receives row N-1 output unless amounts were explicitly set that way |
swap-x-for-y-simple-multi |
Single-pool X->Y over up to 319 active-bin steps | At-most spend x-amount X; at-least receive min-dy Y; constrain pool/token contracts |
swap-y-for-x-simple-multi |
Single-pool Y->X over up to 319 active-bin steps | At-most spend y-amount Y; at-least receive min-dx X; constrain pool/token contracts |
swap-x-for-y-simple-range-multi |
Single-pool X->Y over caller-capped steps | Same as above, plus choose max-steps low enough for expected gas/route tolerance |
swap-y-for-x-simple-range-multi |
Single-pool Y->X over caller-capped steps | Same as above, plus choose max-steps low enough for expected gas/route tolerance |
| Surface | Authority model | Notes |
|---|---|---|
| Router public functions | Fully open | No owner/admin gate, no pause, no kill switch |
| Router storage | None | No state to corrupt locally |
| Pool selection | Caller-controlled trait arguments | Safe use depends on caller/aggregator selecting the intended pool contract |
| Token selection | Caller-controlled SIP-010 trait arguments | Callers should use postconditions to bind expected token movements |
| Core swap execution | Hard-coded core contract principal | Good: users cannot redirect the core. Remaining risk is the pool/token trait values passed into the core |
| Active bin source | contract-call? pool-trait get-active-bin-id |
Pool-supplied value is trusted for core call and unfavorable-bin accounting |
| Governance / fee setters | Not present in router | Any fee/ramp/governance authority lives in pool/core contracts, not this router |
tx-sender: not used.contract-caller: not used.as-contract: not used.unwrap-panic/unwrap-err-panic: not used.unwrap!: used with explicit error constants for fold state, step slices, active bin reads, and result list append.- Arithmetic: unsigned subtraction assumes the core returns
in <= remaining-input; signed bin delta assumes active bin IDs stay within sane bounds. - Trait conformance: compile-time trait types are used for pool and SIP-010 tokens.
- Result list bounds: explicit
as-max-len?guards exist for bothu319andu5result lists.
| ID | Severity | Function | Line | Finding | Recommended fix |
|---|---|---|---|---|---|
| BFR-01 | Low | fold-swap-multi, fold-swap-x-for-y-same-multi, fold-swap-y-for-x-same-multi |
174, 205, 243 | The router bounds-checks expected-bin-id, but not the pool-supplied active-bin-id before computing bin-id-delta and abs-int. A malformed or unexpected pool implementation can make unfavorable-bin accounting revert or produce route semantics outside the documented [-500,500] window. The normal Bitflow pool may keep this sane, but the router accepts a trait argument and should defend the accounting boundary it documents. |
Add an active-bin-id bounds check before delta math, or define a separate supported active-bin window and return ERR_INVALID_BIN_ID when the pool reports outside it. |
| BFR-02 | Low | swap-simple-multi / fold-swap-simple-multi |
98, 266 | swap-simple-multi reads like a multi-hop route helper, but each row has its own independent amount. The function does not automatically feed row N's output into row N+1. This can lead integrators to overstate chained-route safety unless they manually set row amounts and postconditions. |
Document the function as "batch simple swaps" or add a distinct chained-route helper that passes output forward and checks final aggregate output. |
| BFR-03 | Informational | fold-swap-x-for-y-simple-multi, fold-swap-y-for-x-simple-multi |
289, 319 | The bin-id fold argument is unused; STEP_INDEX_RANGE is acting only as a bounded repeat counter while the active bin is fetched every iteration. This is likely intentional, but it is easy to misread as iterating explicit bin IDs. |
Rename the parameter to _step-index / _unused-step, or add a short comment explaining that each iteration re-reads the active bin after the previous core swap. |
| BFR-04 | Informational | swap-x-for-y-simple-range-multi |
127 | The comment says "Y for X direction" above the X-for-Y range helper. | Correct the comment to "X for Y direction." |
| BFR-05 | Informational | swap-multi, swap-x-for-y-same-multi, swap-y-for-x-same-multi, swap-simple-multi |
52, 68, 86, 102 | Empty-list validation happens after fold. With an empty list this does not mutate state, but validating user shape before invoking the fold is clearer and reduces audit burden. |
Move asserts! (> (len swaps) u0) before the fold in each public function. |
| BFR-06 | Low | swap-x-for-y-same-multi, swap-y-for-x-same-multi |
75, 93 | The response returns total output and results, but not the unspent input when a route consumes less than the supplied amount. Integrators can derive this by summing in values, but the API makes leftover handling less explicit. |
Include remaining-x-amount / remaining-y-amount, or include input-spent, in the top-level response. |
Overall assessment: the router is small, stateless, and mostly careful about explicit min-output checks and list bounds. The main residual risk is integration risk: callers supply pool/token traits and must attach strong postconditions; simple/batch route naming should be documented carefully so users do not assume automatic chaining.