From 553e0cb246c1561f3c10dfb2bfee484727e6837a Mon Sep 17 00:00:00 2001 From: sato820 Date: Tue, 23 Jun 2026 09:23:56 -0400 Subject: [PATCH] Add final output guard to simple multi swaps --- clarity/contracts/dlmm-swap-router-v-1-1.clar | 16 +++-- clarity/tests/clarigen-types.ts | 16 ++++- clarity/tests/helpers/clarigen-types.ts | 16 ++++- clarity/tests/routers/swap-router.test.ts | 66 ++++++++++++++++++- docs/dlmm-swap-router-v-1-1.md | 40 ++++++++++- 5 files changed, 146 insertions(+), 8 deletions(-) diff --git a/clarity/contracts/dlmm-swap-router-v-1-1.clar b/clarity/contracts/dlmm-swap-router-v-1-1.clar index 7ebd1e7..1ce27d0 100644 --- a/clarity/contracts/dlmm-swap-router-v-1-1.clar +++ b/clarity/contracts/dlmm-swap-router-v-1-1.clar @@ -96,12 +96,17 @@ ;; Swap through up to 319 bins in up to 5 pools (define-public (swap-simple-multi (swaps (list 5 {pool-trait: , x-token-trait: , y-token-trait: , amount: uint, min-received: uint, x-for-y: bool, max-steps: uint})) + (min-final-output uint) ) (let ( - (swap-result (try! (fold fold-swap-simple-multi swaps (ok {results: (list )})))) + (swap-result (try! (fold fold-swap-simple-multi swaps (ok {results: (list ), final-output: u0})))) ) (asserts! (> (len swaps) u0) ERR_EMPTY_SWAPS_LIST) - (ok swap-result) + (asserts! (>= (get final-output swap-result) min-final-output) ERR_MINIMUM_RECEIVED) + (ok { + results: (get results swap-result), + final-output: (get final-output swap-result) + }) ) ) @@ -264,7 +269,7 @@ (define-private (fold-swap-simple-multi (swap {pool-trait: , x-token-trait: , y-token-trait: , amount: uint, min-received: uint, x-for-y: bool, max-steps: uint}) - (result (response {results: (list 5 {in: uint, out: uint})} uint)) + (result (response {results: (list 5 {in: uint, out: uint}), final-output: uint} uint)) ) (let ( (result-data (unwrap! result ERR_NO_RESULT_DATA)) @@ -281,7 +286,10 @@ (try! (swap-y-for-x-simple-range-multi pool-trait x-token-trait y-token-trait amount min-received max-steps)))) (updated-results (unwrap! (as-max-len? (append (get results result-data) swap-result) u5) ERR_RESULTS_LIST_OVERFLOW)) ) - (ok {results: updated-results}) + (ok { + results: updated-results, + final-output: (get out swap-result) + }) ) ) diff --git a/clarity/tests/clarigen-types.ts b/clarity/tests/clarigen-types.ts index 341b390..be78470 100644 --- a/clarity/tests/clarigen-types.ts +++ b/clarity/tests/clarigen-types.ts @@ -2746,6 +2746,21 @@ dlmmSwapRouterV11: { "out": bigint; }[]; "unfavorable": bigint; +}, bigint>>, + swapSimpleMulti: {"name":"swap-simple-multi","access":"public","args":[{"name":"swaps","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"max-steps","type":"uint128"},{"name":"min-received","type":"uint128"},{"name":"pool-trait","type":"trait_reference"},{"name":"x-for-y","type":"bool"},{"name":"x-token-trait","type":"trait_reference"},{"name":"y-token-trait","type":"trait_reference"}]},"length":5}}},{"name":"min-final-output","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"final-output","type":"uint128"},{"name":"results","type":{"list":{"type":{"tuple":[{"name":"in","type":"uint128"},{"name":"out","type":"uint128"}]},"length":5}}}]},"error":"uint128"}}}} as TypedAbiFunction<[swaps: TypedAbiArg<{ + "amount": number | bigint; + "maxSteps": number | bigint; + "minReceived": number | bigint; + "poolTrait": string; + "xForY": boolean; + "xTokenTrait": string; + "yTokenTrait": string; +}[], "swaps">, minFinalOutput: TypedAbiArg], Response<{ + "finalOutput": bigint; + "results": { + "in": bigint; + "out": bigint; +}[]; }, bigint>>, swapXForYSameMulti: {"name":"swap-x-for-y-same-multi","access":"public","args":[{"name":"swaps","type":{"list":{"type":{"tuple":[{"name":"expected-bin-id","type":"int128"},{"name":"min-received","type":"uint128"},{"name":"pool-trait","type":"trait_reference"}]},"length":350}}},{"name":"x-token-trait","type":"trait_reference"},{"name":"y-token-trait","type":"trait_reference"},{"name":"amount","type":"uint128"},{"name":"min-y-amount-total","type":"uint128"},{"name":"max-unfavorable-bins","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"results","type":{"list":{"type":{"tuple":[{"name":"in","type":"uint128"},{"name":"out","type":"uint128"}]},"length":350}}},{"name":"unfavorable","type":"uint128"},{"name":"y-amount","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[swaps: TypedAbiArg<{ "expectedBinId": number | bigint; @@ -4166,4 +4181,3 @@ export const project = { contracts, deployments, } as const; - \ No newline at end of file diff --git a/clarity/tests/helpers/clarigen-types.ts b/clarity/tests/helpers/clarigen-types.ts index 341b390..be78470 100644 --- a/clarity/tests/helpers/clarigen-types.ts +++ b/clarity/tests/helpers/clarigen-types.ts @@ -2746,6 +2746,21 @@ dlmmSwapRouterV11: { "out": bigint; }[]; "unfavorable": bigint; +}, bigint>>, + swapSimpleMulti: {"name":"swap-simple-multi","access":"public","args":[{"name":"swaps","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"max-steps","type":"uint128"},{"name":"min-received","type":"uint128"},{"name":"pool-trait","type":"trait_reference"},{"name":"x-for-y","type":"bool"},{"name":"x-token-trait","type":"trait_reference"},{"name":"y-token-trait","type":"trait_reference"}]},"length":5}}},{"name":"min-final-output","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"final-output","type":"uint128"},{"name":"results","type":{"list":{"type":{"tuple":[{"name":"in","type":"uint128"},{"name":"out","type":"uint128"}]},"length":5}}}]},"error":"uint128"}}}} as TypedAbiFunction<[swaps: TypedAbiArg<{ + "amount": number | bigint; + "maxSteps": number | bigint; + "minReceived": number | bigint; + "poolTrait": string; + "xForY": boolean; + "xTokenTrait": string; + "yTokenTrait": string; +}[], "swaps">, minFinalOutput: TypedAbiArg], Response<{ + "finalOutput": bigint; + "results": { + "in": bigint; + "out": bigint; +}[]; }, bigint>>, swapXForYSameMulti: {"name":"swap-x-for-y-same-multi","access":"public","args":[{"name":"swaps","type":{"list":{"type":{"tuple":[{"name":"expected-bin-id","type":"int128"},{"name":"min-received","type":"uint128"},{"name":"pool-trait","type":"trait_reference"}]},"length":350}}},{"name":"x-token-trait","type":"trait_reference"},{"name":"y-token-trait","type":"trait_reference"},{"name":"amount","type":"uint128"},{"name":"min-y-amount-total","type":"uint128"},{"name":"max-unfavorable-bins","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"results","type":{"list":{"type":{"tuple":[{"name":"in","type":"uint128"},{"name":"out","type":"uint128"}]},"length":350}}},{"name":"unfavorable","type":"uint128"},{"name":"y-amount","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[swaps: TypedAbiArg<{ "expectedBinId": number | bigint; @@ -4166,4 +4181,3 @@ export const project = { contracts, deployments, } as const; - \ No newline at end of file diff --git a/clarity/tests/routers/swap-router.test.ts b/clarity/tests/routers/swap-router.test.ts index b145767..68773ef 100644 --- a/clarity/tests/routers/swap-router.test.ts +++ b/clarity/tests/routers/swap-router.test.ts @@ -494,6 +494,70 @@ describe('DLMM Swap Helper Functions', () => { expect(received).toBeGreaterThanOrEqual(0n); }); + it('should enforce a final output minimum for simple multi-hop swaps', async () => { + const swaps = [ + { + poolTrait: sbtcUsdcPool.identifier, + xTokenTrait: mockSbtcToken.identifier, + yTokenTrait: mockUsdcToken.identifier, + amount: 500000n, + minReceived: 1n, + xForY: true, + maxSteps: 10n + }, + { + poolTrait: sbtcUsdcPool.identifier, + xTokenTrait: mockSbtcToken.identifier, + yTokenTrait: mockUsdcToken.identifier, + amount: 25000000n, + minReceived: 1n, + xForY: false, + maxSteps: 10n + } + ]; + + const response = txOk(dlmmSwapRouter.swapSimpleMulti( + swaps, + 1n + ), alice); + + const result = cvToValue(response.result); + const lastResult = result.results[result.results.length - 1]; + + expect(result.finalOutput).toBe(lastResult.out); + expect(result.finalOutput).toBeGreaterThan(0n); + }); + + it('should fail simple multi-hop swaps when final output is below minimum', async () => { + const swaps = [ + { + poolTrait: sbtcUsdcPool.identifier, + xTokenTrait: mockSbtcToken.identifier, + yTokenTrait: mockUsdcToken.identifier, + amount: 500000n, + minReceived: 1n, + xForY: true, + maxSteps: 10n + }, + { + poolTrait: sbtcUsdcPool.identifier, + xTokenTrait: mockSbtcToken.identifier, + yTokenTrait: mockUsdcToken.identifier, + amount: 25000000n, + minReceived: 1n, + xForY: false, + maxSteps: 10n + } + ]; + + const response = txErr(dlmmSwapRouter.swapSimpleMulti( + swaps, + 999999999999n + ), alice); + + expect(cvToValue(response.result)).toBe(errors.dlmmSwapRouter.ERR_MINIMUM_RECEIVED); + }); + it('should fail when using random token in swap helper', async () => { // Mint random tokens for testing txOk(mockRandomToken.mint(1000000n, alice), deployer); @@ -544,4 +608,4 @@ describe('DLMM Swap Helper Functions', () => { expect(cvToValue(response.result)).toBe(errors.dlmmCore.ERR_INVALID_Y_TOKEN); }); }); -}); \ No newline at end of file +}); diff --git a/docs/dlmm-swap-router-v-1-1.md b/docs/dlmm-swap-router-v-1-1.md index a011b04..31f0fca 100644 --- a/docs/dlmm-swap-router-v-1-1.md +++ b/docs/dlmm-swap-router-v-1-1.md @@ -10,6 +10,7 @@ dlmm-swap-router-v-1-1 - [`swap-multi`](#swap-multi) - [`swap-x-for-y-same-multi`](#swap-x-for-y-same-multi) - [`swap-y-for-x-same-multi`](#swap-y-for-x-same-multi) +- [`swap-simple-multi`](#swap-simple-multi) - [`swap-x-for-y-simple-multi`](#swap-x-for-y-simple-multi) - [`swap-y-for-x-simple-multi`](#swap-y-for-x-simple-multi) @@ -173,6 +174,44 @@ Swap through multiple bins in multiple pools using the same token pair and Y for | min-x-amount-total | uint | | max-unfavorable-bins | uint | +### swap-simple-multi + +[View in file](../clarity/contracts/dlmm-swap-router-v-1-1.clar#L97) + +`(define-public (swap-simple-multi ((swaps (list 5 (tuple (amount uint) (max-steps uint) (min-received uint) (pool-trait trait_reference) (x-for-y bool) (x-token-trait trait_reference) (y-token-trait trait_reference)))) (min-final-output uint)) (response (tuple (final-output uint) (results (list 5 (tuple (in uint) (out uint))))) uint))` + +Swap through up to 319 bins in up to 5 pools and require the final leg output to meet `min-final-output`. + +
+ Source code: + +```clarity +(define-public (swap-simple-multi + (swaps (list 5 {pool-trait: , x-token-trait: , y-token-trait: , amount: uint, min-received: uint, x-for-y: bool, max-steps: uint})) + (min-final-output uint) + ) + (let ( + (swap-result (try! (fold fold-swap-simple-multi swaps (ok {results: (list ), final-output: u0})))) + ) + (asserts! (> (len swaps) u0) ERR_EMPTY_SWAPS_LIST) + (asserts! (>= (get final-output swap-result) min-final-output) ERR_MINIMUM_RECEIVED) + (ok { + results: (get results swap-result), + final-output: (get final-output swap-result) + }) + ) +) +``` +
+ + +**Parameters:** + +| Name | Type | +| --- | --- | +| swaps | (list 5 (tuple (amount uint) (max-steps uint) (min-received uint) (pool-trait trait_reference) (x-for-y bool) (x-token-trait trait_reference) (y-token-trait trait_reference))) | +| min-final-output | uint | + ### swap-x-for-y-simple-multi [View in file](../clarity/contracts/dlmm-swap-router-v-1-1.clar#L93) @@ -722,4 +761,3 @@ List used to swap through up to 350 bins via swap-x-for-y-simple-multi and swap- ``` [View in file](../clarity/contracts/dlmm-swap-router-v-1-1.clar#L23) - \ No newline at end of file