Smart Contract

UniswapV3SwapperConnectorV3

A.ca7ee55e4fc3251a.UniswapV3SwapperConnectorV3

Valid From

135,713,359

Deployed

5d ago
Feb 23, 2026, 12:47:17 AM UTC

Dependents

1 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import EVM from 0xe467b9dd11fa00df
4import Burner from 0xf233dcee88fe0abe
5import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
6import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
7import DeFiActions from 0xca7ee55e4fc3251a
8
9/// UniswapV3SwapperConnectorV3
10///
11/// DeFiActions Swapper connector for Uniswap V3 routers on Flow EVM.
12/// Based on the official FlowActions UniswapV3SwapConnectors pattern.
13///
14/// Supports single-hop and multi-hop swaps using exactInput with proper
15/// FlowEVMBridge integration for token bridging.
16///
17access(all) contract UniswapV3SwapperConnectorV3 {
18
19    /// Events
20    access(all) event SwapperCreated(
21        routerAddress: String,
22        tokenPath: [String],
23        feePath: [UInt32]
24    )
25    access(all) event SwapExecuted(
26        routerAddress: String,
27        amountIn: UFix64,
28        amountOut: UFix64,
29        tokenPath: [String]
30    )
31    access(all) event QuoteFetched(
32        quoterAddress: String,
33        amountIn: UFix64,
34        amountOut: UFix64
35    )
36
37    /// Storage paths
38    access(all) let AdminStoragePath: StoragePath
39
40    /// Default addresses for FlowSwap V3 on Flow EVM Mainnet
41    access(all) let defaultRouterAddress: EVM.EVMAddress
42    access(all) let defaultQuoterAddress: EVM.EVMAddress
43    access(all) let defaultFactoryAddress: EVM.EVMAddress
44
45    /// WFLOW address on Flow EVM Mainnet
46    access(all) let wflowAddress: EVM.EVMAddress
47
48    /// ABI Helper: Encode a UInt256 as 32 bytes (big-endian)
49    access(all) fun abiUInt256(_ value: UInt256): [UInt8] {
50        var result: [UInt8] = []
51        var remaining = value
52        var bytes: [UInt8] = []
53
54        if remaining == 0 {
55            bytes.append(0)
56        } else {
57            while remaining > 0 {
58                bytes.append(UInt8(remaining % 256))
59                remaining = remaining / 256
60            }
61        }
62
63        // Pad to 32 bytes
64        while bytes.length < 32 {
65            bytes.append(0)
66        }
67
68        // Reverse to get big-endian
69        var i = 31
70        while i >= 0 {
71            result.append(bytes[i])
72            if i == 0 { break }
73            i = i - 1
74        }
75
76        return result
77    }
78
79    /// ABI Helper: Encode an address as 32 bytes
80    access(all) fun abiAddress(_ addr: EVM.EVMAddress): [UInt8] {
81        var result: [UInt8] = []
82        // 12 bytes of zero padding
83        var i = 0
84        while i < 12 {
85            result.append(0)
86            i = i + 1
87        }
88        // 20 bytes of address
89        for byte in addr.bytes {
90            result.append(byte)
91        }
92        return result
93    }
94
95    /// ABI Helper: Encode dynamic bytes with length prefix
96    access(all) fun abiDynamicBytes(_ data: [UInt8]): [UInt8] {
97        var result: [UInt8] = []
98        // Length as uint256
99        result = result.concat(self.abiUInt256(UInt256(data.length)))
100        // Data
101        result = result.concat(data)
102        // Pad to 32-byte boundary
103        let padding = (32 - (data.length % 32)) % 32
104        var i = 0
105        while i < padding {
106            result.append(0)
107            i = i + 1
108        }
109        return result
110    }
111
112    /// ABI Helper: Encode word (32 bytes)
113    access(all) fun abiWord(_ value: UInt256): [UInt8] {
114        return self.abiUInt256(value)
115    }
116
117    /// Encode exactInput tuple: (bytes path, address recipient, uint256 amountIn, uint256 amountOutMin)
118    access(all) fun encodeExactInputTuple(
119        pathBytes: [UInt8],
120        recipient: EVM.EVMAddress,
121        amountIn: UInt256,
122        amountOutMin: UInt256
123    ): [UInt8] {
124        let tupleHeadSize = 32 * 4  // 4 fields: offset, address, uint256, uint256
125
126        var head: [[UInt8]] = []
127        var tail: [[UInt8]] = []
128
129        // 1) bytes path (dynamic) -> offset to tail (after head)
130        head.append(self.abiWord(UInt256(tupleHeadSize)))
131        tail.append(self.abiDynamicBytes(pathBytes))
132
133        // 2) address recipient
134        head.append(self.abiAddress(recipient))
135
136        // 3) uint256 amountIn
137        head.append(self.abiUInt256(amountIn))
138
139        // 4) uint256 amountOutMin
140        head.append(self.abiUInt256(amountOutMin))
141
142        // Concatenate head and tail
143        var result: [UInt8] = []
144        for part in head {
145            result = result.concat(part)
146        }
147        for part in tail {
148            result = result.concat(part)
149        }
150        return result
151    }
152
153    /// UniswapV3Swapper resource implementing DeFiActions.Swapper
154    access(all) resource UniswapV3Swapper: DeFiActions.Swapper {
155        /// Router address for V3 swaps
156        access(all) let routerAddress: EVM.EVMAddress
157        /// Quoter address for price quotes
158        access(all) let quoterAddress: EVM.EVMAddress
159        /// Factory address for pool lookups
160        access(all) let factoryAddress: EVM.EVMAddress
161
162        /// Token path for multi-hop swaps (at least 2 addresses)
163        access(all) let tokenPath: [EVM.EVMAddress]
164        /// Fee path for V3 pools (length = tokenPath.length - 1)
165        access(all) let feePath: [UInt32]
166
167        /// Input vault type (Cadence)
168        access(self) let inVaultType: Type
169        /// Output vault type (Cadence)
170        access(self) let outVaultType: Type
171
172        /// COA capability for EVM interactions
173        access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
174
175        /// Track last execution effort (gas used)
176        access(self) var lastGasUsed: UInt64
177
178        init(
179            routerAddress: EVM.EVMAddress,
180            quoterAddress: EVM.EVMAddress,
181            factoryAddress: EVM.EVMAddress,
182            tokenPath: [EVM.EVMAddress],
183            feePath: [UInt32],
184            inVaultType: Type,
185            outVaultType: Type,
186            coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
187        ) {
188            pre {
189                tokenPath.length >= 2: "tokenPath must contain at least two addresses"
190                feePath.length == tokenPath.length - 1: "feePath length must be tokenPath.length - 1"
191                coaCapability.check(): "Invalid COA Capability"
192            }
193            self.routerAddress = routerAddress
194            self.quoterAddress = quoterAddress
195            self.factoryAddress = factoryAddress
196            self.tokenPath = tokenPath
197            self.feePath = feePath
198            self.inVaultType = inVaultType
199            self.outVaultType = outVaultType
200            self.coaCapability = coaCapability
201            self.lastGasUsed = 0
202        }
203
204        /// Build V3 path bytes: token0 + fee0 + token1 + fee1 + token2 ...
205        access(self) fun buildPathBytes(reverse: Bool): [UInt8] {
206            var path: [UInt8] = []
207
208            // Build indices based on direction
209            let length = self.tokenPath.length
210
211            var i = 0
212            while i < length {
213                // Get index based on direction
214                let tokenIdx = reverse ? (length - 1 - i) : i
215                let feeIdx = reverse ? (self.feePath.length - 1 - i) : i
216
217                // Add token address (20 bytes)
218                for byte in self.tokenPath[tokenIdx].bytes {
219                    path.append(byte)
220                }
221
222                // Add fee tier (3 bytes, big-endian) if not last token
223                if i < self.feePath.length {
224                    let fee = self.feePath[feeIdx]
225                    path.append(UInt8((fee >> 16) & 0xFF))
226                    path.append(UInt8((fee >> 8) & 0xFF))
227                    path.append(UInt8(fee & 0xFF))
228                }
229                i = i + 1
230            }
231            return path
232        }
233
234        /// Execute swap on Uniswap V3 via exactInput
235        /// Uses FlowEVMBridge for token bridging - the bridge handles FLOW↔WFLOW automatically
236        access(all) fun swap(
237            inVault: @{FungibleToken.Vault},
238            quote: DeFiActions.Quote
239        ): @{FungibleToken.Vault} {
240            let originalAmount = inVault.balance
241            let minOut = quote.minAmount
242
243            let coa = self.coaCapability.borrow()
244                ?? panic("Invalid COA Capability")
245
246            // Get input/output token addresses from path
247            let inToken = self.tokenPath[0]
248            let outToken = self.tokenPath[self.tokenPath.length - 1]
249
250            // Round DOWN to nearest 10^10 wei (Cadence-compatible precision)
251            let evmWei = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
252                originalAmount,
253                erc20Address: inToken
254            )
255
256            let precision: UInt256 = 10_000_000_000 // 10^10
257            let cleanWei = (evmWei / precision) * precision
258
259            let cleanAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
260                cleanWei,
261                erc20Address: inToken
262            )
263
264            let amountIn = cleanAmount > 0.0 ? cleanAmount : originalAmount
265
266            // Withdraw only the clean amount from the vault
267            var vaultToBridge: @{FungibleToken.Vault}? <- nil
268            if amountIn < originalAmount && amountIn > 0.0 {
269                vaultToBridge <-! inVault.withdraw(amount: amountIn)
270                destroy inVault
271            } else {
272                vaultToBridge <-! inVault
273            }
274
275            // Convert amounts to EVM format
276            let evmAmountIn = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
277                amountIn,
278                erc20Address: inToken
279            )
280            let evmAmountOutMin = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
281                minOut,
282                erc20Address: outToken
283            )
284
285            // Calculate bridge fee (2x for deposit + withdraw)
286            let bridgeFee = 2.0 * FlowEVMBridgeUtils.calculateBridgeFee(bytes: 256)
287            let bridgeFeeBalance = EVM.Balance(attoflow: 0)
288            bridgeFeeBalance.setFLOW(flow: bridgeFee)
289
290            // Check COA balance for fees
291            let coaBalance = coa.balance()
292            let requiredAttoflow = bridgeFeeBalance.attoflow
293
294            if coaBalance.attoflow < requiredAttoflow {
295                if self.inVaultType == Type<@FlowToken.Vault>() {
296                    let feeDeposit = 0.02
297                    var tempVault <- vaultToBridge <- nil
298                    let unwrappedVault <- tempVault!
299                    let feeFunding <- unwrappedVault.withdraw(amount: feeDeposit)
300                    vaultToBridge <-! unwrappedVault
301                    coa.deposit(from: <-(feeFunding as! @FlowToken.Vault))
302                } else {
303                    panic("COA has insufficient FLOW for bridge fees. Please fund your COA.")
304                }
305            }
306
307            let feeVault <- coa.withdraw(balance: bridgeFeeBalance)
308            let feeVaultRef = &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
309
310            // Bridge input tokens to EVM
311            coa.depositTokens(vault: <-vaultToBridge!, feeProvider: feeVaultRef)
312
313            // Build V3 path bytes
314            let pathBytes = self.buildPathBytes(reverse: false)
315
316            // Approve router to spend input tokens
317            let approveData = EVM.encodeABIWithSignature(
318                "approve(address,uint256)",
319                [self.routerAddress, evmAmountIn]
320            )
321            let approveResult = coa.call(
322                to: inToken,
323                data: approveData,
324                gasLimit: 120_000,
325                value: EVM.Balance(attoflow: 0)
326            )
327            if approveResult.status != EVM.Status.successful {
328                panic("Failed to approve router: ".concat(approveResult.errorMessage))
329            }
330
331            // exactInput selector: 0xb858183f
332            let selector: [UInt8] = [0xb8, 0x58, 0x18, 0x3f]
333
334            let argsBlob = UniswapV3SwapperConnectorV3.encodeExactInputTuple(
335                pathBytes: pathBytes,
336                recipient: coa.address(),
337                amountIn: evmAmountIn,
338                amountOutMin: evmAmountOutMin
339            )
340
341            let head = UniswapV3SwapperConnectorV3.abiWord(UInt256(32))
342            let calldata = selector.concat(head).concat(argsBlob)
343
344            // Execute the swap
345            var swapResult = coa.call(
346                to: self.routerAddress,
347                data: calldata,
348                gasLimit: 2_000_000,
349                value: EVM.Balance(attoflow: 0)
350            )
351
352            var evmAmountOut: UInt256 = 0
353
354            if swapResult.status == EVM.Status.successful {
355                let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: swapResult.data)
356                evmAmountOut = decoded.length > 0 ? decoded[0] as! UInt256 : UInt256(0)
357                self.lastGasUsed = swapResult.gasUsed
358            } else {
359                panic("V3 swap FAILED. Error: ".concat(swapResult.errorMessage))
360            }
361
362            // Query output token decimals
363            let decimalsData = EVM.encodeABIWithSignature("decimals()", [])
364            let decimalsResult = coa.call(
365                to: outToken,
366                data: decimalsData,
367                gasLimit: 50_000,
368                value: EVM.Balance(attoflow: 0)
369            )
370
371            var outDecimals: UInt8 = 18
372            if decimalsResult.status == EVM.Status.successful && decimalsResult.data.length > 0 {
373                let decoded = EVM.decodeABI(types: [Type<UInt8>()], data: decimalsResult.data)
374                if decoded.length > 0 {
375                    outDecimals = decoded[0] as! UInt8
376                }
377            }
378
379            // Calculate precision for output
380            var outputPrecision: UInt256 = 1
381            if outDecimals > 8 {
382                let exponent = outDecimals - 8
383                var i: UInt8 = 0
384                while i < exponent {
385                    outputPrecision = outputPrecision * 10
386                    i = i + 1
387                }
388            }
389
390            let cleanAmountOut = (evmAmountOut / outputPrecision) * outputPrecision
391
392            if cleanAmountOut == 0 {
393                panic("Swap returned zero output after precision rounding")
394            }
395
396            // Withdraw output tokens back to Cadence
397            let outVault <- coa.withdrawTokens(
398                type: self.outVaultType,
399                amount: cleanAmountOut,
400                feeProvider: feeVaultRef
401            )
402
403            // Handle leftover fee vault
404            if feeVault.balance > 0.0 {
405                coa.deposit(from: <-feeVault)
406            } else {
407                Burner.burn(<-feeVault)
408            }
409
410            let cadenceAmountOut = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
411                cleanAmountOut,
412                erc20Address: outToken
413            )
414
415            emit SwapExecuted(
416                routerAddress: self.routerAddress.toString(),
417                amountIn: amountIn,
418                amountOut: cadenceAmountOut,
419                tokenPath: self.getTokenPathStrings()
420            )
421
422            return <- outVault
423        }
424
425        /// Get quote using V3 Quoter
426        access(all) fun getQuote(
427            fromTokenType: Type,
428            toTokenType: Type,
429            amount: UFix64
430        ): DeFiActions.Quote {
431            let coa = self.coaCapability.borrow()
432
433            if coa != nil {
434                let inToken = self.tokenPath[0]
435                let outToken = self.tokenPath[self.tokenPath.length - 1]
436
437                let evmAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
438                    amount,
439                    erc20Address: inToken
440                )
441
442                let pathBytes = EVM.EVMBytes(value: self.buildPathBytes(reverse: false))
443
444                let quoteData = EVM.encodeABIWithSignature(
445                    "quoteExactInput(bytes,uint256)",
446                    [pathBytes, evmAmount]
447                )
448
449                let quoteResult = coa!.dryCall(
450                    to: self.quoterAddress,
451                    data: quoteData,
452                    gasLimit: 1_000_000,
453                    value: EVM.Balance(attoflow: 0)
454                )
455
456                if quoteResult.status == EVM.Status.successful && quoteResult.data.length > 0 {
457                    let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: quoteResult.data)
458                    if decoded.length > 0 {
459                        let evmAmountOut = decoded[0] as! UInt256
460                        let expectedOut = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
461                            evmAmountOut,
462                            erc20Address: outToken
463                        )
464                        let minAmount = expectedOut * 0.90
465
466                        emit QuoteFetched(
467                            quoterAddress: self.quoterAddress.toString(),
468                            amountIn: amount,
469                            amountOut: expectedOut
470                        )
471
472                        return DeFiActions.Quote(
473                            expectedAmount: expectedOut,
474                            minAmount: minAmount,
475                            slippageTolerance: 0.10,
476                            deadline: nil,
477                            data: {
478                                "dex": "UniswapV3" as AnyStruct,
479                                "tokenPath": self.getTokenPathStrings() as AnyStruct
480                            }
481                        )
482                    }
483                }
484            }
485
486            // Fallback estimate
487            return DeFiActions.Quote(
488                expectedAmount: amount * 0.99,
489                minAmount: amount * 0.89,
490                slippageTolerance: 0.10,
491                deadline: nil,
492                data: {
493                    "dex": "UniswapV3" as AnyStruct,
494                    "estimated": true as AnyStruct
495                }
496            )
497        }
498
499        /// Get token path as strings
500        access(self) fun getTokenPathStrings(): [String] {
501            var result: [String] = []
502            for token in self.tokenPath {
503                result.append(token.toString())
504            }
505            return result
506        }
507
508        /// Get execution effort from last swap
509        access(all) fun getLastExecutionEffort(): UInt64 {
510            return self.lastGasUsed
511        }
512
513        /// Get swapper info
514        access(all) fun getInfo(): DeFiActions.ComponentInfo {
515            return DeFiActions.ComponentInfo(
516                type: "Swapper",
517                identifier: "UniswapV3",
518                version: "3.0.0"
519            )
520        }
521    }
522
523    /// Create V3 swapper with token path and fee path
524    access(all) fun createSwapper(
525        routerAddress: EVM.EVMAddress,
526        quoterAddress: EVM.EVMAddress,
527        factoryAddress: EVM.EVMAddress,
528        tokenPath: [EVM.EVMAddress],
529        feePath: [UInt32],
530        inVaultType: Type,
531        outVaultType: Type,
532        coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
533    ): @UniswapV3Swapper {
534        var pathStrings: [String] = []
535        for token in tokenPath {
536            pathStrings.append(token.toString())
537        }
538
539        emit SwapperCreated(
540            routerAddress: routerAddress.toString(),
541            tokenPath: pathStrings,
542            feePath: feePath
543        )
544
545        return <- create UniswapV3Swapper(
546            routerAddress: routerAddress,
547            quoterAddress: quoterAddress,
548            factoryAddress: factoryAddress,
549            tokenPath: tokenPath,
550            feePath: feePath,
551            inVaultType: inVaultType,
552            outVaultType: outVaultType,
553            coaCapability: coaCapability
554        )
555    }
556
557    /// Create swapper with FlowSwap V3 defaults
558    access(all) fun createSwapperWithDefaults(
559        tokenPath: [EVM.EVMAddress],
560        feePath: [UInt32],
561        inVaultType: Type,
562        outVaultType: Type,
563        coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
564    ): @UniswapV3Swapper {
565        return <- self.createSwapper(
566            routerAddress: self.defaultRouterAddress,
567            quoterAddress: self.defaultQuoterAddress,
568            factoryAddress: self.defaultFactoryAddress,
569            tokenPath: tokenPath,
570            feePath: feePath,
571            inVaultType: inVaultType,
572            outVaultType: outVaultType,
573            coaCapability: coaCapability
574        )
575    }
576
577    /// Admin resource
578    access(all) resource Admin {
579        // Admin functions for future updates
580    }
581
582    init() {
583        self.AdminStoragePath = /storage/UniswapV3SwapperConnectorV3Admin
584
585        // FlowSwap V3 Mainnet addresses
586        self.defaultRouterAddress = EVM.addressFromString("0xeEDC6Ff75e1b10B903D9013c358e446a73d35341")
587        self.defaultFactoryAddress = EVM.addressFromString("0xca6d7Bb03334bBf135902e1d919a5feccb461632")
588        self.defaultQuoterAddress = EVM.addressFromString("0x370A8DF17742867a44e56223EC20D82092242C85")
589
590        // WFLOW on Flow EVM Mainnet
591        self.wflowAddress = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e")
592    }
593}
594