Smart Contract

UniswapV2SwapConnectors

A.0e5b1dececaca3a8.UniswapV2SwapConnectors

Valid From

122,643,930

Deployed

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

Dependents

3 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import Burner from 0xf233dcee88fe0abe
4import EVM from 0xe467b9dd11fa00df
5import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
6import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
7import FlowEVMBridge from 0x1e4aa0b87d10b141
8
9import DeFiActions from 0x92195d814edf9cb0
10import SwapConnectors from 0x0bce04a00aedf132
11
12/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
13/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
14/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
15///
16/// UniswapV2SwapConnectors
17///
18/// DeFiActions Swapper connector implementation fitting UniswapV2 EVM-based swap protocols for use in DeFiActions
19/// workflows.
20///
21access(all) contract UniswapV2SwapConnectors {
22
23    /// Swapper
24    ///
25    /// A DeFiActions connector that swaps between tokens using an EVM-based UniswapV2Router contract
26    ///
27    access(all) struct Swapper : DeFiActions.Swapper {
28        /// UniswapV2Router contract's EVM address
29        access(all) let routerAddress: EVM.EVMAddress
30        /// A swap path defining the route followed for facilitated swaps. Each element should be a valid token address
31        /// for which there is a pool available with the previous and subsequent token address via the defined Router
32        access(all) let addressPath: [EVM.EVMAddress]
33        /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
34        /// specific Identifier to associated connectors on construction
35        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
36        /// The pre-conversion currency accepted for a swap
37        access(self) let inVault: Type
38        /// The post-conversion currency returned by a swap
39        access(self) let outVault: Type
40        /// An authorized Capability on the CadenceOwnedAccount which this Swapper executes swaps on behalf of
41        access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
42
43        init(
44            routerAddress: EVM.EVMAddress,
45            path: [EVM.EVMAddress],
46            inVault: Type,
47            outVault: Type,
48            coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
49            uniqueID: DeFiActions.UniqueIdentifier?
50        ) {
51            pre {
52                path.length >= 2: "Provided path with length of \(path.length) - path must contain at least two EVM addresses)"
53                FlowEVMBridgeConfig.getTypeAssociated(with: path[0]) == inVault:
54                "Provided inVault \(inVault.identifier) is not associated with ERC20 at path[0] \(path[0].toString()) - "
55                    .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
56                FlowEVMBridgeConfig.getTypeAssociated(with: path[path.length - 1]) == outVault: 
57                "Provided outVault \(outVault.identifier) is not associated with ERC20 at path[\(path.length - 1)] \(path[path.length - 1].toString()) - "
58                    .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
59                coaCapability.check():
60                "Provided COA Capability is invalid - provided an active, unrevoked Capability<auth(EVM.Call) &EVM.CadenceOwnedAccount>"
61            }
62            self.routerAddress = routerAddress
63            self.addressPath = path
64            self.uniqueID = uniqueID
65            self.inVault = inVault
66            self.outVault = outVault
67            self.coaCapability = coaCapability
68        }
69
70        /// Returns a ComponentInfo struct containing information about this Swapper and its inner DFA components
71        ///
72        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
73        ///     each inner component in the stack.
74        ///
75        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
76            return DeFiActions.ComponentInfo(
77                type: self.getType(),
78                id: self.uniqueID?.id,
79                innerComponents: []
80            )
81        }
82        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
83        /// a DeFiActions stack. See DeFiActions.align() for more information.
84        ///
85        /// @return a copy of the struct's UniqueIdentifier
86        ///
87        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
88            return self.uniqueID
89        }
90        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
91        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
92        ///
93        /// @param id: the UniqueIdentifier to set for this component
94        ///
95        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
96            self.uniqueID = id
97        }
98        /// The type of Vault this Swapper accepts when performing a swap
99        access(all) view fun inType(): Type {
100            return self.inVault
101        }
102        /// The type of Vault this Swapper provides when performing a swap
103        access(all) view fun outType(): Type {
104            return self.outVault
105        }
106        /// The estimated amount required to provide a Vault with the desired output balance returned as a BasicQuote
107        /// struct containing the in and out Vault types and quoted in and out amounts
108        /// NOTE: Cadence only supports decimal precision of 8
109        ///
110        /// @param forDesired: The amount out desired of the post-conversion currency as a result of the swap
111        /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method estimates a swap
112        ///     in the opposite direction, outVault -> inVault
113        ///
114        /// @return a SwapConnectors.BasicQuote containing estimate data. In order to prevent upstream reversion,
115        ///     result.inAmount and result.outAmount will be 0.0 if an estimate is not available
116        ///
117        access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DeFiActions.Quote} {
118            let amountIn = self.getAmount(out: false, amount: forDesired, path: reverse ? self.addressPath.reverse() : self.addressPath)
119            return SwapConnectors.BasicQuote(
120                inType: reverse ? self.outType() : self.inType(),
121                outType: reverse ? self.inType() : self.outType(),
122                inAmount: amountIn != nil ? amountIn! : 0.0,
123                outAmount: amountIn != nil ? forDesired : 0.0
124            )
125        }
126        /// The estimated amount delivered out for a provided input balance returned as a BasicQuote returned as a
127        /// BasicQuote struct containing the in and out Vault types and quoted in and out amounts
128        /// NOTE: Cadence only supports decimal precision of 8
129        ///
130        /// @param forProvided: The amount provided of the relevant pre-conversion currency
131        /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method estimates a swap
132        ///     in the opposite direction, outVault -> inVault
133        ///
134        /// @return a SwapConnectors.BasicQuote containing estimate data. In order to prevent upstream reversion,
135        ///     result.inAmount and result.outAmount will be 0.0 if an estimate is not available
136        ///
137        access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} {
138            let amountOut = self.getAmount(out: true, amount: forProvided, path: reverse ? self.addressPath.reverse() : self.addressPath)
139            return SwapConnectors.BasicQuote(
140                inType: reverse ? self.outType() : self.inType(),
141                outType: reverse ? self.inType() : self.outType(),
142                inAmount: amountOut != nil ? forProvided : 0.0,
143                outAmount: amountOut != nil ? amountOut! : 0.0
144            )
145        }
146
147        /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. This implementation swaps
148        /// along a path defined on init routing the swap to the pre-defined UniswapV2Router implementation on Flow EVM.
149        /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
150        /// used.
151        /// NOTE: Cadence only supports decimal precision of 8
152        ///
153        /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
154        ///     desired otherwise a new quote is generated from current state
155        /// @param inVault: Tokens of type `inVault` to swap for a vault of type `outVault`
156        ///
157        /// @return a Vault of type `outVault` containing the swapped currency.
158        ///
159        access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
160            let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: true).outAmount
161            return <-self.swapExactTokensForTokens(exactVaultIn: <-inVault, amountOutMin: amountOutMin, reverse: false)
162        }
163
164        /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
165        /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
166        /// to use multiple Flow swap protocols.
167        /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
168        /// used.
169        /// NOTE: Cadence only supports decimal precision of 8
170        ///
171        /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
172        ///     desired otherwise a new quote is generated from current state
173        /// @param residual: Tokens of type `outVault` to swap back to `inVault`
174        ///
175        /// @return a Vault of type `inVault` containing the swapped currency.
176        ///
177        access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
178            let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: residual.balance, reverse: true).outAmount
179            return <-self.swapExactTokensForTokens(
180                exactVaultIn: <-residual,
181                amountOutMin: amountOutMin,
182                reverse: true
183            )
184        }
185
186        /// Port of UniswapV2Router.swapExactTokensForTokens swapping the exact amount provided along the given path,
187        /// returning the final output Vault
188        ///
189        /// @param exactVaultIn: The pre-conversion currency to swap
190        /// @param amountOutMin: The minimum amount of post-conversion tokens to swap for
191        /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method swaps in the
192        ///     opposite direction, outVault -> inVault
193        ///
194        /// @return the resulting Vault containing the swapped tokens
195        ///
196        access(self) fun swapExactTokensForTokens(
197            exactVaultIn: @{FungibleToken.Vault},
198            amountOutMin: UFix64,
199            reverse: Bool
200        ): @{FungibleToken.Vault} {
201            let id = self.uniqueID?.id?.toString() ?? "UNASSIGNED"
202            let idType = self.uniqueID?.getType()?.identifier ?? "UNASSIGNED"
203            let coa = self.borrowCOA()
204                ?? panic("The COA Capability contained by Swapper \(self.getType().identifier) with UniqueIdentifier "
205                    .concat("\(idType) ID \(id) is invalid - cannot perform an EVM swap without a valid COA Capability"))
206
207            // withdraw FLOW from the COA to cover the VM bridge fee
208            let bridgeFeeBalance = EVM.Balance(attoflow: 0)
209            bridgeFeeBalance.setFLOW(flow: 2.0 * FlowEVMBridgeUtils.calculateBridgeFee(bytes: 128)) // bridging to EVM then from EVM, hence factor of 2
210            let feeVault <- coa.withdraw(balance: bridgeFeeBalance)
211            let feeVaultRef = &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
212
213            // bridge the provided to the COA's EVM address
214            let inTokenAddress = reverse ? self.addressPath[self.addressPath.length - 1] : self.addressPath[0]
215            let evmAmountIn = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
216                exactVaultIn.balance,
217                erc20Address: inTokenAddress
218            )
219            coa.depositTokens(vault: <-exactVaultIn, feeProvider: feeVaultRef)
220
221            // approve the router to swap tokens
222            var res = self.call(to: inTokenAddress,
223                signature: "approve(address,uint256)",
224                args: [self.routerAddress, evmAmountIn],
225                gasLimit: 15_000_000,
226                value: 0,
227                dryCall: false
228            )!
229            if res.status != EVM.Status.successful {
230                UniswapV2SwapConnectors._callError("approve(address,uint256)",
231                    res, inTokenAddress, idType, id, self.getType())
232            }
233            // perform the swap
234            res = self.call(to: self.routerAddress,
235                signature: "swapExactTokensForTokens(uint,uint,address[],address,uint)", // amountIn, amountOutMin, path, to, deadline (timestamp)
236                args: [evmAmountIn, UInt256(0), (reverse ? self.addressPath.reverse() : self.addressPath), coa.address(), UInt256(getCurrentBlock().timestamp)],
237                gasLimit: 15_000_000,
238                value: 0,
239                dryCall: false
240            )!
241            if res.status != EVM.Status.successful {
242                // revert because the funds have already been deposited to the COA - a no-op would leave the funds in EVM
243                UniswapV2SwapConnectors._callError("swapExactTokensForTokens(uint,uint,address[],address,uint)",
244                    res, self.routerAddress, idType, id, self.getType())
245            }
246            let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: res.data)
247            let amountsOut = decoded[0] as! [UInt256]
248
249            // withdraw tokens from EVM
250            let outVault <- coa.withdrawTokens(type: self.outType(),
251                    amount: amountsOut[amountsOut.length - 1],
252                    feeProvider: feeVaultRef
253                )
254
255            // clean up the remaining feeVault & return the swap output Vault
256            self.handleRemainingFeeVault(<-feeVault)
257            return <- outVault
258        }
259
260        /* --- Internal --- */
261
262        /// Internal method used to retrieve router.getAmountsIn and .getAmountsOut estimates. The returned array is the
263        /// estimate returned from the router where each value is a swapped amount corresponding to the swap along the
264        /// provided path.
265        ///
266        /// @param out: If true, getAmountsOut is called, otherwise getAmountsIn is called
267        /// @param amount: The amount in or out. If out is true, the amount will be used as the amount in provided,
268        ///     otherwise amount defines the desired amount out for the estimate
269        /// @param path: The path of ERC20 token addresses defining the sequence of swaps executed to arrive at the
270        ///     desired token out
271        ///
272        /// @return An estimate of the amounts for each swap along the path. If out is true, the return value contains
273        ///     the values in, otherwise the array contains the values out for each swap along the path
274        ///
275        access(self) fun getAmount(out: Bool, amount: UFix64, path: [EVM.EVMAddress]): UFix64? {
276            let callRes = self.call(to: self.routerAddress,
277                signature: out ? "getAmountsOut(uint,address[])" : "getAmountsIn(uint,address[])",
278                args: [amount],
279                gasLimit: 5_000_000,
280                value: UInt(0),
281                dryCall: true
282            )
283            if callRes == nil || callRes!.status != EVM.Status.successful {
284                return nil
285            }
286            let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: callRes!.data) // can revert if the type cannot be decoded
287            let uintAmounts: [UInt256] = decoded.length > 0 ? decoded[0] as! [UInt256] : []
288            if uintAmounts.length == 0 {
289                return nil
290            } else if out {
291                return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[uintAmounts.length - 1], erc20Address: path[path.length - 1])
292            } else {
293                return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[0], erc20Address: path[0])
294            }
295        }
296
297        /// Deposits any remainder in the provided Vault or burns if it it's empty
298        access(self) fun handleRemainingFeeVault(_ vault: @FlowToken.Vault) {
299            if vault.balance > 0.0 {
300                self.borrowCOA()!.deposit(from: <-vault)
301            } else {
302                Burner.burn(<-vault)
303            }
304        }
305
306        /// Returns a reference to the Swapper's COA or `nil` if the contained Capability is invalid
307        access(self) view fun borrowCOA(): auth(EVM.Owner) &EVM.CadenceOwnedAccount? {
308            return self.coaCapability.borrow()
309        }
310
311        /// Makes a call to the Swapper's routerEVMAddress via the contained COA Capability with the provided signature,
312        /// args, and value. If flagged as dryCall, the more efficient and non-mutating COA.dryCall is used. A result is
313        /// returned as long as the COA Capability is valid, otherwise `nil` is returned.
314        access(self) fun call(
315            to: EVM.EVMAddress,
316            signature: String,
317            args: [AnyStruct],
318            gasLimit: UInt64,
319            value: UInt,
320            dryCall: Bool
321        ): EVM.Result? {
322            let calldata = EVM.encodeABIWithSignature(signature, args)
323            let valueBalance = EVM.Balance(attoflow: value)
324            if let coa = self.borrowCOA() {
325                let res: EVM.Result = dryCall
326                    ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
327                    : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
328                return res
329            }
330            return nil
331        }
332    }
333
334    /// Converts the given amounts from their ERC20 UInt256 to UFix64 amounts according to the ERC20 defined decimals.
335    /// Assumes each EVM address in path is an ERC20 contract
336    access(self)
337    fun _convertEVMAmountsToCadenceAmounts(_ amounts: [UInt256], path: [EVM.EVMAddress]): [UFix64] {
338        let convertedAmounts: [UFix64]= []
339        for i, amount in amounts {
340            convertedAmounts.append(FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(amount, erc20Address: path[i]))
341        }
342        return convertedAmounts
343    }
344
345    /// Reverts with a message constructed from the provided args. Used in the event of a coa.call() error
346    access(self)
347    fun _callError(_ signature: String, _ res: EVM.Result,_ target: EVM.EVMAddress, _ uniqueIDType: String, _ id: String, _ swapperType: Type) {
348        panic("Call to \(target.toString()).\(signature) from Swapper \(swapperType.identifier) "
349            .concat("with UniqueIdentifier \(uniqueIDType) ID \(id) failed: \n\t"
350            .concat("Status value: \(res.status.rawValue)\n\t"))
351            .concat("Error code: \(res.errorCode)\n\t")
352            .concat("ErrorMessage: \(res.errorMessage)\n"))
353    }
354}
355