Smart Contract

SwapConnectors

A.0bce04a00aedf132.SwapConnectors

Valid From

122,643,885

Deployed

5d ago
Feb 22, 2026, 06:34:46 AM UTC

Dependents

25 imports
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3
4import DeFiActions from 0x92195d814edf9cb0
5import DeFiActionsUtils from 0x92195d814edf9cb0
6
7/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
8/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
9/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10///
11/// SwapConnectors
12///
13/// This contract defines DeFi Actions Sink & Source connector implementations for use with DeFi protocols. These
14/// connectors can be used alone or in conjunction with other DeFi Actions connectors to create complex DeFi workflows.
15///
16access(all) contract SwapConnectors {
17
18    /// BasicQuote
19    ///
20    /// A simple implementation of DeFiActions.Quote allowing callers of Swapper.quoteIn() and .quoteOut() to cache quoted
21    /// amount in and/or out.
22    ///
23    access(all) struct BasicQuote : DeFiActions.Quote {
24        access(all) let inType: Type
25        access(all) let outType: Type
26        access(all) let inAmount: UFix64
27        access(all) let outAmount: UFix64
28
29        init(
30            inType: Type,
31            outType: Type,
32            inAmount: UFix64,
33            outAmount: UFix64
34        ) {
35            self.inType = inType
36            self.outType = outType
37            self.inAmount = inAmount
38            self.outAmount = outAmount
39        }
40    }
41
42    /// MultiSwapperQuote
43    ///
44    /// A MultiSwapper specific DeFiActions.Quote implementation allowing for callers to set the Swapper used in
45    /// MultiSwapper that should fulfill the Swap
46    ///
47    access(all) struct MultiSwapperQuote : DeFiActions.Quote {
48        access(all) let inType: Type
49        access(all) let outType: Type
50        access(all) let inAmount: UFix64
51        access(all) let outAmount: UFix64
52        access(all) let swapperIndex: Int
53
54        init(
55            inType: Type,
56            outType: Type,
57            inAmount: UFix64,
58            outAmount: UFix64,
59            swapperIndex: Int
60        ) {
61            pre {
62                swapperIndex >= 0: "Invalid swapperIndex - provided \(swapperIndex) is less than 0"
63            }
64            self.inType = inType
65            self.outType = outType
66            self.inAmount = inAmount
67            self.outAmount = outAmount
68            self.swapperIndex = swapperIndex
69        }
70    }
71
72    /// MultiSwapper
73    ///
74    /// A Swapper implementation routing swap requests to the optimal contained Swapper. Once constructed, this can
75    /// effectively be used as an aggregator across all contained Swapper implementations, though it is limited to the
76    /// routes and pools exposed by its inner Swappers as well as runtime computation limits.
77    ///
78    access(all) struct MultiSwapper : DeFiActions.Swapper {
79        access(all) let swappers: [{DeFiActions.Swapper}]
80        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
81        access(self) let inVault: Type
82        access(self) let outVault: Type
83
84        init(
85            inVault: Type,
86            outVault: Type,
87            swappers: [{DeFiActions.Swapper}],
88            uniqueID: DeFiActions.UniqueIdentifier?
89        ) {
90            pre {
91                inVault.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()):
92                "Invalid inVault type - \(inVault.identifier) is not a FungibleToken Vault implementation"
93                outVault.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()):
94                "Invalid outVault type - \(outVault.identifier) is not a FungibleToken Vault implementation"
95            }
96            for i in InclusiveRange(0, swappers.length - 1) {
97                let swapper = &swappers[i] as &{DeFiActions.Swapper}
98                assert(swapper.inType() == inVault,
99                    message: "Mismatched inVault \(inVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.inType().identifier)")
100                assert(swapper.outType() == outVault,
101                    message: "Mismatched outVault \(outVault.identifier) - Swapper \(swapper.getType().identifier) accepts \(swapper.outType().identifier)")
102            }
103            self.inVault = inVault
104            self.outVault = outVault
105            self.uniqueID = uniqueID
106            self.swappers = swappers
107        }
108
109        /// Returns a ComponentInfo struct containing information about this MultiSwapper and its inner DFA components
110        ///
111        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
112        ///     each inner component in the stack.
113        ///
114        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
115            let inner: [DeFiActions.ComponentInfo] = []
116            for swapper in self.swappers {
117                inner.append(swapper.getComponentInfo())
118            }
119            return DeFiActions.ComponentInfo(
120                type: self.getType(),
121                id: self.id(),
122                innerComponents: inner
123            )
124        }
125        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
126        /// a DeFiActions stack. See DeFiActions.align() for more information.
127        ///
128        /// @return a copy of the struct's UniqueIdentifier
129        ///
130        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
131            return self.uniqueID
132        }
133        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
134        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
135        ///
136        /// @param id: the UniqueIdentifier to set for this component
137        ///
138        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
139            self.uniqueID = id
140        }
141        /// The type of Vault this Swapper accepts when performing a swap
142        access(all) view fun inType(): Type {
143            return self.inVault
144        }
145        /// The type of Vault this Swapper provides when performing a swap
146        access(all) view fun outType(): Type  {
147            return self.outVault
148        }
149        /// The estimated amount required to provide a Vault with the desired output balance
150        access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DeFiActions.Quote} {
151            let estimate = self._estimate(amount: forDesired, out: true, reverse: reverse)
152            return MultiSwapperQuote(
153                inType: reverse ? self.outType() : self.inType(),
154                outType: reverse ? self.inType() : self.outType(),
155                inAmount: estimate[1],
156                outAmount: forDesired,
157                swapperIndex: Int(estimate[0])
158            )
159        }
160        /// The estimated amount delivered out for a provided input balance
161        access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} {
162            let estimate = self._estimate(amount: forProvided, out: true, reverse: reverse)
163            return MultiSwapperQuote(
164                inType: reverse ? self.outType() : self.inType(),
165                outType: reverse ? self.inType() : self.outType(),
166                inAmount: forProvided,
167                outAmount: estimate[1],
168                swapperIndex: Int(estimate[0])
169            )
170        }
171        /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose
172        /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
173        /// to use multiple Flow swap protocols. If the provided quote is not a MultiSwapperQuote, a new quote is
174        /// requested and the optimal Swapper used to fulfill the swap.
175        /// NOTE: providing a Quote does not guarantee the fulfilled swap will enforce the quote's defined outAmount
176        access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
177            return <-self._swap(quote: quote, from: <-inVault, reverse: false)
178        }
179        /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
180        /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
181        /// to use multiple Flow swap protocols.
182        /// NOTE: providing a Quote does not guarantee the fulfilled swap will enforce the quote's defined outAmount
183        access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
184            return <-self._swap(quote: quote, from: <-residual, reverse: true)
185        }
186        /// Returns the the index of the optimal Swapper (result[0]) and the associated amountOut or amountIn (result[0])
187        /// as a UFix64 array
188        access(self) fun _estimate(amount: UFix64, out: Bool, reverse: Bool): [UFix64; 2] {
189            var res: [UFix64; 2] = [0.0, 0.0]
190            for i in InclusiveRange(0, self.swappers.length - 1) {
191                let swapper = &self.swappers[i] as &{DeFiActions.Swapper}
192                // call the appropriate estimator
193                let estimate = out
194                    ? swapper.quoteOut(forProvided: amount, reverse: true).outAmount
195                    : swapper.quoteIn(forDesired: amount, reverse: true).inAmount
196                if (out ? res[1] < estimate : estimate < res[1]) {
197                    // take minimum for in, maximum for out
198                    res = [UFix64(i), estimate]
199                }
200            }
201            return res
202        }
203        /// Swaps the provided Vault in the defined direction. If the quote is not a MultiSwapperQuote, a new quote is
204        /// requested and the current optimal Swapper used to fulfill the swap.
205        access(self) fun _swap(quote: {DeFiActions.Quote}?, from: @{FungibleToken.Vault}, reverse: Bool): @{FungibleToken.Vault} {
206            var multiQuote = quote as? MultiSwapperQuote
207            if multiQuote != nil || multiQuote!.swapperIndex > self.swappers.length {
208                multiQuote = self.quoteOut(forProvided: from.balance, reverse: reverse) as! MultiSwapperQuote
209            }
210            let optimalSwapper = &self.swappers[multiQuote!.swapperIndex] as &{DeFiActions.Swapper}
211            if reverse {
212                return <- optimalSwapper.swapBack(quote: multiQuote, residual: <-from)
213            } else {
214                return <- optimalSwapper.swap(quote: multiQuote, inVault: <-from)
215            }
216        }
217    }
218
219    /// SwapSink
220    ///
221    /// A DeFiActions connector that deposits the resulting post-conversion currency of a token swap to an inner
222    /// DeFiActions Sink, sourcing funds from a deposited Vault of a pre-set Type.
223    ///
224    access(all) struct SwapSink : DeFiActions.Sink {
225        access(self) let swapper: {DeFiActions.Swapper}
226        access(self) let sink: {DeFiActions.Sink}
227        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
228
229        init(swapper: {DeFiActions.Swapper}, sink: {DeFiActions.Sink}, uniqueID: DeFiActions.UniqueIdentifier?) {
230            pre {
231                swapper.outType() == sink.getSinkType():
232                "Swapper outputs \(swapper.outType().identifier) but Sink takes \(sink.getSinkType().identifier) - "
233                    .concat("Ensure the provided Swapper outputs a Vault Type compatible with the provided Sink")
234            }
235            self.swapper = swapper
236            self.sink = sink
237            self.uniqueID = uniqueID
238        }
239
240        /// Returns a ComponentInfo struct containing information about this SwapSink and its inner DFA components
241        ///
242        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
243        ///     each inner component in the stack.
244        ///
245        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
246            return DeFiActions.ComponentInfo(
247                type: self.getType(),
248                id: self.id(),
249                innerComponents: [
250                    self.swapper.getComponentInfo(),
251                    self.sink.getComponentInfo()
252                ]
253            )
254        }
255        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
256        /// a DeFiActions stack. See DeFiActions.align() for more information.
257        ///
258        /// @return a copy of the struct's UniqueIdentifier
259        ///
260        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
261            return self.uniqueID
262        }
263        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
264        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
265        ///
266        /// @param id: the UniqueIdentifier to set for this component
267        ///
268        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
269            self.uniqueID = id
270        }
271        /// Returns the type of Vault this Sink accepts when performing a swap
272        ///
273        /// @return the type of Vault this Sink accepts when performing a swap
274        ///
275        access(all) view fun getSinkType(): Type {
276            return self.swapper.inType()
277        }
278        /// Returns the minimum capacity required to deposit to this Sink
279        ///
280        /// @return the minimum capacity required to deposit to this Sink
281        ///
282        access(all) fun minimumCapacity(): UFix64 {
283            return self.swapper.quoteIn(forDesired: self.sink.minimumCapacity(), reverse: false).inAmount
284        }
285        /// Deposits the provided Vault to this Sink, swapping the provided Vault to the required type if necessary
286        ///
287        /// @param from: the Vault to source deposits from
288        ///
289        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
290            let limit = self.sink.minimumCapacity()
291            if from.balance == 0.0 || limit == 0.0 || from.getType() != self.getSinkType() {
292                return // nothing to swap from, no capacity to ingest, invalid Vault type - do nothing
293            }
294
295            let quote = self.swapper.quoteIn(forDesired: limit, reverse: false)
296            let swapVault <- from.createEmptyVault()
297            if from.balance <= quote.inAmount  {
298                // sink can accept all of the available tokens, so we swap everything
299                swapVault.deposit(from: <-from.withdraw(amount: from.balance))
300            } else {
301                // sink is limited to fewer tokens than we have available - swap the amount we need to meet the limit
302                swapVault.deposit(from: <-from.withdraw(amount: quote.inAmount))
303            }
304
305            // swap then deposit to the inner sink
306            let swappedTokens <- self.swapper.swap(quote: quote, inVault: <-swapVault)
307            self.sink.depositCapacity(from: &swappedTokens as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
308
309            if swappedTokens.balance > 0.0 {
310                // swap back any residual to the originating vault
311                let residual <- self.swapper.swapBack(quote: nil, residual: <-swappedTokens)
312                from.deposit(from: <-residual)
313            } else {
314                Burner.burn(<-swappedTokens) // nothing left - burn & execute vault's burnCallback()
315            }
316        }
317    }
318
319    /// SwapSource
320    ///
321    /// A DeFiActions connector that returns post-conversion currency, sourcing pre-converted funds from an inner
322    /// DeFiActions Source
323    ///
324    access(all) struct SwapSource : DeFiActions.Source {
325        access(self) let swapper: {DeFiActions.Swapper}
326        access(self) let source: {DeFiActions.Source}
327        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
328
329        init(swapper: {DeFiActions.Swapper}, source: {DeFiActions.Source}, uniqueID: DeFiActions.UniqueIdentifier?) {
330            pre {
331                source.getSourceType() == swapper.inType():
332                "Source outputs \(source.getSourceType().identifier) but Swapper takes \(swapper.inType().identifier) - "
333                    .concat("Ensure the provided Source outputs a Vault Type compatible with the provided Swapper")
334            }
335            self.swapper = swapper
336            self.source = source
337            self.uniqueID = uniqueID
338        }
339
340        /// Returns a ComponentInfo struct containing information about this SwapSource and its inner DFA components
341        ///
342        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
343        ///     each inner component in the stack.
344        ///
345        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
346            return DeFiActions.ComponentInfo(
347                type: self.getType(),
348                id: self.id(),
349                innerComponents: [
350                    self.swapper.getComponentInfo(),
351                    self.source.getComponentInfo()
352                ]
353            )
354        }
355        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
356        /// a DeFiActions stack. See DeFiActions.align() for more information.
357        ///
358        /// @return a copy of the struct's UniqueIdentifier
359        ///
360        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
361            return self.uniqueID
362        }
363        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
364        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
365        ///
366        /// @param id: the UniqueIdentifier to set for this component
367        ///
368        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
369            self.uniqueID = id
370        }
371        /// Returns the type of Vault this Source provides when performing a swap
372        ///
373        /// @return the type of Vault this Source provides when performing a swap
374        ///
375        access(all) view fun getSourceType(): Type {
376            return self.swapper.outType()
377        }
378        /// Returns the minimum amount of currency available to withdraw from this Source
379        ///
380        /// @return the minimum amount of currency available to withdraw from this Source
381        ///
382        access(all) fun minimumAvailable(): UFix64 {
383            // estimate post-conversion currency based on the source's pre-conversion balance available
384            let availableIn = self.source.minimumAvailable()
385            return availableIn > 0.0
386                ? self.swapper.quoteOut(forProvided: availableIn, reverse: false).outAmount
387                : 0.0
388        }
389        /// Withdraws the provided amount of currency from this Source, swapping the provided amount to the required type if necessary
390        ///
391        /// @param maxAmount: the maximum amount of currency to withdraw from this Source
392        ///
393        /// @return the Vault containing the withdrawn currency
394        ///
395        access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
396            let minimumAvail = self.minimumAvailable()
397            if minimumAvail == 0.0 || maxAmount == 0.0 {
398                return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
399            }
400
401            // expect output amount as the lesser between the amount available and the maximum amount
402            var quote = minimumAvail < maxAmount
403                ? self.swapper.quoteOut(forProvided: self.source.minimumAvailable(), reverse: false)
404                : self.swapper.quoteIn(forDesired: maxAmount, reverse: false)
405
406            let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: quote.inAmount)
407            if sourceLiquidity.balance == 0.0 {
408                Burner.burn(<-sourceLiquidity)
409                return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
410            }
411            let outVault <- self.swapper.swap(quote: quote, inVault: <-sourceLiquidity)
412            if outVault.balance > quote.outAmount {
413                // TODO - what to do if excess is found?
414                //  - can swapBack() but can't deposit to the inner source and can't return an unsupported Vault type
415                //      -> could make inner {Source} an intersection {Source, Sink}
416            }
417            return <- outVault
418        }
419    }
420}