Smart Contract

UniswapV2SwapConnectors

A.f94f371678513b2b.UniswapV2SwapConnectors

Valid From

134,921,784

Deployed

6d ago
Feb 21, 2026, 03:43:26 PM UTC

Dependents

13 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 0x6d888f175c158410
10import SwapConnectors from 0xe1a479f0cb911df9
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 uintDesired = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
119                    forDesired,
120                    erc20Address: reverse ? self.addressPath[0] : self.addressPath[self.addressPath.length - 1]
121                )
122            let amountIn = self.getAmount(out: false, amount: uintDesired, path: reverse ? self.addressPath.reverse() : self.addressPath)
123            return SwapConnectors.BasicQuote(
124                inType: reverse ? self.outType() : self.inType(),
125                outType: reverse ? self.inType() : self.outType(),
126                inAmount: amountIn != nil ? amountIn! : 0.0,
127                outAmount: amountIn != nil ? forDesired : 0.0
128            )
129        }
130        /// The estimated amount delivered out for a provided input balance returned as a BasicQuote returned as a
131        /// BasicQuote struct containing the in and out Vault types and quoted in and out amounts
132        /// NOTE: Cadence only supports decimal precision of 8
133        ///
134        /// @param forProvided: The amount provided of the relevant pre-conversion currency
135        /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method estimates a swap
136        ///     in the opposite direction, outVault -> inVault
137        ///
138        /// @return a SwapConnectors.BasicQuote containing estimate data. In order to prevent upstream reversion,
139        ///     result.inAmount and result.outAmount will be 0.0 if an estimate is not available
140        ///
141        access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} {
142            let uintProvided = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
143                    forProvided,
144                    erc20Address: reverse ? self.addressPath[self.addressPath.length - 1] : self.addressPath[0]
145                )
146            let amountOut = self.getAmount(out: true, amount: uintProvided, path: reverse ? self.addressPath.reverse() : self.addressPath)
147            return SwapConnectors.BasicQuote(
148                inType: reverse ? self.outType() : self.inType(),
149                outType: reverse ? self.inType() : self.outType(),
150                inAmount: amountOut != nil ? forProvided : 0.0,
151                outAmount: amountOut != nil ? amountOut! : 0.0
152            )
153        }
154
155        /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. This implementation swaps
156        /// along a path defined on init routing the swap to the pre-defined UniswapV2Router implementation on Flow EVM.
157        /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
158        /// used.
159        /// NOTE: Cadence only supports decimal precision of 8
160        ///
161        /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
162        ///     desired otherwise a new quote is generated from current state
163        /// @param inVault: Tokens of type `inVault` to swap for a vault of type `outVault`
164        ///
165        /// @return a Vault of type `outVault` containing the swapped currency.
166        ///
167        access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
168            let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: false).outAmount
169            return <-self.swapExactTokensForTokens(exactVaultIn: <-inVault, amountOutMin: amountOutMin, reverse: false)
170        }
171
172        /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
173        /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
174        /// to use multiple Flow swap protocols.
175        /// Any Quote provided defines the amountOutMin value - if none is provided, the current quoted outAmount is
176        /// used.
177        /// NOTE: Cadence only supports decimal precision of 8
178        ///
179        /// @param quote: A `DeFiActions.Quote` data structure. If provided, quote.outAmount is used as the minimum amount out
180        ///     desired otherwise a new quote is generated from current state
181        /// @param residual: Tokens of type `outVault` to swap back to `inVault`
182        ///
183        /// @return a Vault of type `inVault` containing the swapped currency.
184        ///
185        access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
186            let amountOutMin = quote?.outAmount ?? self.quoteOut(forProvided: residual.balance, reverse: true).outAmount
187            return <-self.swapExactTokensForTokens(
188                exactVaultIn: <-residual,
189                amountOutMin: amountOutMin,
190                reverse: true
191            )
192        }
193
194        /// Port of UniswapV2Router.swapExactTokensForTokens swapping the exact amount provided along the given path,
195        /// returning the final output Vault
196        ///
197        /// @param exactVaultIn: The pre-conversion currency to swap
198        /// @param amountOutMin: The minimum amount of post-conversion tokens to swap for
199        /// @param reverse: If false, the default inVault -> outVault is used, otherwise, the method swaps in the
200        ///     opposite direction, outVault -> inVault
201        ///
202        /// @return the resulting Vault containing the swapped tokens
203        ///
204        access(self) fun swapExactTokensForTokens(
205            exactVaultIn: @{FungibleToken.Vault},
206            amountOutMin: UFix64,
207            reverse: Bool
208        ): @{FungibleToken.Vault} {
209            let id = self.uniqueID?.id?.toString() ?? "UNASSIGNED"
210            let idType = self.uniqueID?.getType()?.identifier ?? "UNASSIGNED"
211            let coa = self.borrowCOA()
212                ?? panic("The COA Capability contained by Swapper \(self.getType().identifier) with UniqueIdentifier "
213                    .concat("\(idType) ID \(id) is invalid - cannot perform an EVM swap without a valid COA Capability"))
214
215            // withdraw FLOW from the COA to cover the VM bridge fee
216            let bridgeFeeBalance = EVM.Balance(attoflow: 0)
217            bridgeFeeBalance.setFLOW(flow: 2.0 * FlowEVMBridgeUtils.calculateBridgeFee(bytes: 128)) // bridging to EVM then from EVM, hence factor of 2
218            let feeVault <- coa.withdraw(balance: bridgeFeeBalance)
219            let feeVaultRef = &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
220
221            // bridge the provided to the COA's EVM address
222            let inTokenAddress = reverse ? self.addressPath[self.addressPath.length - 1] : self.addressPath[0]
223            let evmAmountIn = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
224                exactVaultIn.balance,
225                erc20Address: inTokenAddress
226            )
227            coa.depositTokens(vault: <-exactVaultIn, feeProvider: feeVaultRef)
228
229            // approve the router to swap tokens
230            var res = self.call(to: inTokenAddress,
231                signature: "approve(address,uint256)",
232                args: [self.routerAddress, evmAmountIn],
233                gasLimit: 100_000,
234                value: 0,
235                dryCall: false
236            )!
237            if res.status != EVM.Status.successful {
238                UniswapV2SwapConnectors._callError("approve(address,uint256)",
239                    res, inTokenAddress, idType, id, self.getType())
240            }
241            // perform the swap
242            res = self.call(to: self.routerAddress,
243                signature: "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)", // amountIn, amountOutMin, path, to, deadline (timestamp)
244                args: [evmAmountIn, UInt256(0), (reverse ? self.addressPath.reverse() : self.addressPath), coa.address(), UInt256(getCurrentBlock().timestamp)],
245                gasLimit: 1_000_000,
246                value: 0,
247                dryCall: false
248            )!
249            if res.status != EVM.Status.successful {
250                // revert because the funds have already been deposited to the COA - a no-op would leave the funds in EVM
251                UniswapV2SwapConnectors._callError("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
252                    res, self.routerAddress, idType, id, self.getType())
253            }
254            let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: res.data)
255            let amountsOut = decoded[0] as! [UInt256]
256
257            // withdraw tokens from EVM
258            let outVault <- coa.withdrawTokens(type: self.outType(),
259                    amount: amountsOut[amountsOut.length - 1],
260                    feeProvider: feeVaultRef
261                )
262
263            // clean up the remaining feeVault & return the swap output Vault
264            self.handleRemainingFeeVault(<-feeVault)
265            return <- outVault
266        }
267
268        /* --- Internal --- */
269
270        /// Internal method used to retrieve router.getAmountsIn and .getAmountsOut estimates. The returned array is the
271        /// estimate returned from the router where each value is a swapped amount corresponding to the swap along the
272        /// provided path.
273        ///
274        /// @param out: If true, getAmountsOut is called, otherwise getAmountsIn is called
275        /// @param amount: The amount in or out. If out is true, the amount will be used as the amount in provided,
276        ///     otherwise amount defines the desired amount out for the estimate
277        /// @param path: The path of ERC20 token addresses defining the sequence of swaps executed to arrive at the
278        ///     desired token out
279        ///
280        /// @return An estimate of the amounts for each swap along the path. If out is true, the return value contains
281        ///     the values in, otherwise the array contains the values out for each swap along the path
282        ///
283        access(self) fun getAmount(out: Bool, amount: UInt256, path: [EVM.EVMAddress]): UFix64? {
284            let callRes = self.call(to: self.routerAddress,
285                signature: out ? "getAmountsOut(uint,address[])" : "getAmountsIn(uint,address[])",
286                args: [amount, path],
287                gasLimit: 1_000_000,
288                value: UInt(0),
289                dryCall: true
290            )
291            if callRes == nil || callRes!.status != EVM.Status.successful {
292                return nil
293            }
294            let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: callRes!.data) // can revert if the type cannot be decoded
295            let uintAmounts: [UInt256] = decoded.length > 0 ? decoded[0] as! [UInt256] : []
296            if uintAmounts.length == 0 {
297                return nil
298            } else if out {
299                return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[uintAmounts.length - 1], erc20Address: path[path.length - 1])
300            } else {
301                return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintAmounts[0], erc20Address: path[0])
302            }
303        }
304
305        /// Deposits any remainder in the provided Vault or burns if it it's empty
306        access(self) fun handleRemainingFeeVault(_ vault: @FlowToken.Vault) {
307            if vault.balance > 0.0 {
308                self.borrowCOA()!.deposit(from: <-vault)
309            } else {
310                Burner.burn(<-vault)
311            }
312        }
313
314        /// Returns a reference to the Swapper's COA or `nil` if the contained Capability is invalid
315        access(self) view fun borrowCOA(): auth(EVM.Owner) &EVM.CadenceOwnedAccount? {
316            return self.coaCapability.borrow()
317        }
318
319        /// Makes a call to the Swapper's routerEVMAddress via the contained COA Capability with the provided signature,
320        /// args, and value. If flagged as dryCall, the more efficient and non-mutating COA.dryCall is used. A result is
321        /// returned as long as the COA Capability is valid, otherwise `nil` is returned.
322        access(self) fun call(
323            to: EVM.EVMAddress,
324            signature: String,
325            args: [AnyStruct],
326            gasLimit: UInt64,
327            value: UInt,
328            dryCall: Bool
329        ): EVM.Result? {
330            let calldata = EVM.encodeABIWithSignature(signature, args)
331            let valueBalance = EVM.Balance(attoflow: value)
332            if let coa = self.borrowCOA() {
333                let res: EVM.Result = dryCall
334                    ? coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
335                    : coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
336                return res
337            }
338            return nil
339        }
340    }
341
342    /// Reverts with a message constructed from the provided args. Used in the event of a coa.call() error
343    access(self)
344    fun _callError(_ signature: String, _ res: EVM.Result,_ target: EVM.EVMAddress, _ uniqueIDType: String, _ id: String, _ swapperType: Type) {
345        panic("Call to \(target.toString()).\(signature) from Swapper \(swapperType.identifier) "
346            .concat("with UniqueIdentifier \(uniqueIDType) ID \(id) failed: \n\t"
347            .concat("Status value: \(res.status.rawValue)\n\t"))
348            .concat("Error code: \(res.errorCode)\n\t")
349            .concat("ErrorMessage: \(res.errorMessage)\n"))
350    }
351}
352