Smart Contract

UniswapV2SwapperConnectorV2

A.17ae3b1b0b0d50db.UniswapV2SwapperConnectorV2

Valid From

135,478,155

Deployed

6d ago
Feb 22, 2026, 02:18:41 AM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import EVM from 0xe467b9dd11fa00df
4import Burner from 0xf233dcee88fe0abe
5
6import DeFiActions from 0x17ae3b1b0b0d50db
7
8/// UniswapV2SwapperConnectorV2
9///
10/// DeFiActions Swapper connector implementation for Uniswap V2 style DEXes on Flow EVM.
11/// This V2 version includes full COA-based EVM swap execution.
12///
13/// Supports PunchSwap V2 and other Uniswap V2 forks deployed on Flow EVM (Chain ID: 747).
14///
15access(all) contract UniswapV2SwapperConnectorV2 {
16
17    /// Events
18    access(all) event SwapperCreated(
19        routerAddress: String,
20        factoryAddress: String,
21        tokenPath: [String]
22    )
23    access(all) event SwapExecuted(
24        routerAddress: String,
25        amountIn: UFix64,
26        amountOut: UFix64,
27        path: [String]
28    )
29    access(all) event SwapFailed(
30        routerAddress: String,
31        amountIn: UFix64,
32        reason: String
33    )
34    access(all) event QuoteFetched(
35        routerAddress: String,
36        amountIn: UFix64,
37        amountOut: UFix64
38    )
39
40    /// Storage paths
41    access(all) let AdminStoragePath: StoragePath
42
43    /// Default router and factory addresses for PunchSwap V2 on Flow EVM Mainnet
44    access(all) let defaultRouterAddress: EVM.EVMAddress
45    access(all) let defaultFactoryAddress: EVM.EVMAddress
46    
47    /// WFLOW address on Flow EVM
48    access(all) let wflowAddress: EVM.EVMAddress
49
50    /// UniswapV2Swapper resource implementing DeFiActions.Swapper interface
51    access(all) resource UniswapV2Swapper: DeFiActions.Swapper {
52        /// EVM Router address for Uniswap V2 swaps
53        access(all) let routerAddress: EVM.EVMAddress
54        /// EVM factory address for pair lookups
55        access(all) let factoryAddress: EVM.EVMAddress
56        /// Pool address for this swap pair
57        access(all) let poolAddress: EVM.EVMAddress
58        /// Token path for the swap (EVM addresses)
59        access(all) let tokenPath: [EVM.EVMAddress]
60        /// Cadence token type identifiers for mapping
61        access(all) let cadenceTokenPath: [String]
62        /// Capability to the COA for EVM interactions
63        access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?
64        /// Input vault type
65        access(self) let inVaultType: Type
66        /// Output vault type
67        access(self) let outVaultType: Type
68
69        init(
70            routerAddress: EVM.EVMAddress,
71            factoryAddress: EVM.EVMAddress,
72            poolAddress: EVM.EVMAddress,
73            tokenPath: [EVM.EVMAddress],
74            cadenceTokenPath: [String],
75            coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?,
76            inVaultType: Type,
77            outVaultType: Type
78        ) {
79            pre {
80                tokenPath.length >= 2: "Token path must have at least 2 tokens"
81                cadenceTokenPath.length >= 2: "Cadence token path must have at least 2 tokens"
82            }
83            self.routerAddress = routerAddress
84            self.factoryAddress = factoryAddress
85            self.poolAddress = poolAddress
86            self.tokenPath = tokenPath
87            self.cadenceTokenPath = cadenceTokenPath
88            self.coaCapability = coaCapability
89            self.inVaultType = inVaultType
90            self.outVaultType = outVaultType
91        }
92
93        /// Execute swap on Uniswap V2 via EVM
94        access(all) fun swap(
95            inVault: @{FungibleToken.Vault},
96            quote: DeFiActions.Quote
97        ): @{FungibleToken.Vault} {
98            let amountIn = inVault.balance
99            
100            // If no COA capability, cannot execute - return input vault unchanged
101            if self.coaCapability == nil {
102                emit SwapFailed(
103                    routerAddress: self.routerAddress.toString(),
104                    amountIn: amountIn,
105                    reason: "No COA capability - swap cannot be executed"
106                )
107                return <- inVault
108            }
109
110            if let coa = self.coaCapability!.borrow() {
111                // Execute the actual EVM swap
112                return <- self.executeEVMSwap(
113                    coa: coa,
114                    inVault: <- inVault,
115                    amountOutMin: quote.minAmount
116                )
117            }
118
119            emit SwapFailed(
120                routerAddress: self.routerAddress.toString(),
121                amountIn: amountIn,
122                reason: "COA capability is invalid"
123            )
124            return <- inVault
125        }
126
127        /// Execute the actual EVM swap via COA
128        access(self) fun executeEVMSwap(
129            coa: auth(EVM.Owner) &EVM.CadenceOwnedAccount,
130            inVault: @{FungibleToken.Vault},
131            amountOutMin: UFix64
132        ): @{FungibleToken.Vault} {
133            let amountIn = inVault.balance
134            let inTokenAddress = self.tokenPath[0]
135            let outTokenAddress = self.tokenPath[self.tokenPath.length - 1]
136
137            // Convert Cadence amount to EVM amount (18 decimals)
138            let evmAmountIn = self.toEVMAmount(amountIn, decimals: 18)
139            let evmAmountOutMin = self.toEVMAmount(amountOutMin, decimals: 18)
140
141            // Step 1: Deposit tokens to COA (bridge Cadence -> EVM)
142            if inVault.getType() == Type<@FlowToken.Vault>() {
143                // Deposit FLOW directly - it becomes native balance in COA
144                coa.deposit(from: <- (inVault as! @FlowToken.Vault))
145                
146                // Wrap FLOW to WFLOW by calling WFLOW contract deposit()
147                // evmAmountIn is already in 18 decimals (attoflow), no multiplication needed
148                let wrapResult = coa.call(
149                    to: UniswapV2SwapperConnectorV2.wflowAddress,
150                    data: EVM.encodeABIWithSignature("deposit()", []),
151                    gasLimit: 100_000,
152                    value: EVM.Balance(attoflow: UInt(evmAmountIn))
153                )
154                
155                if wrapResult.status != EVM.Status.successful {
156                    panic("Failed to wrap FLOW to WFLOW: ".concat(wrapResult.errorMessage))
157                }
158            } else {
159                // For non-FLOW tokens, destroy and panic (bridging not yet supported)
160                destroy inVault
161                panic("Non-FLOW token bridging not yet implemented")
162            }
163
164            // Step 2: Approve router to spend tokens
165            let approveData = EVM.encodeABIWithSignature(
166                "approve(address,uint256)",
167                [self.routerAddress, evmAmountIn]
168            )
169            let approveResult = coa.call(
170                to: inTokenAddress,
171                data: approveData,
172                gasLimit: 100_000,
173                value: EVM.Balance(attoflow: 0)
174            )
175            if approveResult.status != EVM.Status.successful {
176                panic("Failed to approve router: ".concat(approveResult.errorMessage))
177            }
178
179            // Step 3: Execute swap on Uniswap V2 router
180            let deadline = UInt256(UInt64(getCurrentBlock().timestamp) + 300) // 5 minute deadline
181            let swapData = EVM.encodeABIWithSignature(
182                "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
183                [evmAmountIn, evmAmountOutMin, self.tokenPath, coa.address(), deadline]
184            )
185            let swapResult = coa.call(
186                to: self.routerAddress,
187                data: swapData,
188                gasLimit: 500_000,
189                value: EVM.Balance(attoflow: 0)
190            )
191            
192            if swapResult.status != EVM.Status.successful {
193                panic("Swap failed: ".concat(swapResult.errorMessage))
194            }
195
196            // Decode the output amounts
197            let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: swapResult.data)
198            let amountsOut = decoded[0] as! [UInt256]
199            let evmAmountOut = amountsOut[amountsOut.length - 1]
200
201            // Step 4: Withdraw output tokens from COA (bridge EVM -> Cadence)
202            // Check if output is WFLOW - need to unwrap first
203            if outTokenAddress.toString().toLower() == UniswapV2SwapperConnectorV2.wflowAddress.toString().toLower() {
204                // Unwrap WFLOW to FLOW
205                let unwrapData = EVM.encodeABIWithSignature(
206                    "withdraw(uint256)",
207                    [evmAmountOut]
208                )
209                let unwrapResult = coa.call(
210                    to: UniswapV2SwapperConnectorV2.wflowAddress,
211                    data: unwrapData,
212                    gasLimit: 100_000,
213                    value: EVM.Balance(attoflow: 0)
214                )
215                if unwrapResult.status != EVM.Status.successful {
216                    panic("Failed to unwrap WFLOW: ".concat(unwrapResult.errorMessage))
217                }
218            }
219
220            // Withdraw FLOW from COA (evmAmountOut is already in 18 decimals)
221            let withdrawBalance = EVM.Balance(attoflow: UInt(evmAmountOut))
222            let outputVault <- coa.withdraw(balance: withdrawBalance) as! @FlowToken.Vault
223
224            let cadenceAmountOut = self.fromEVMAmount(evmAmountOut, decimals: 18)
225            
226            emit SwapExecuted(
227                routerAddress: self.routerAddress.toString(),
228                amountIn: amountIn,
229                amountOut: cadenceAmountOut,
230                path: self.cadenceTokenPath
231            )
232
233            return <- outputVault
234        }
235
236        /// Convert Cadence UFix64 to EVM UInt256 amount
237        access(self) fun toEVMAmount(_ amount: UFix64, decimals: UInt8): UInt256 {
238            // Cadence UFix64 has 8 decimal places
239            // EVM typically uses 18 decimals
240            let factor = self.pow10(UInt8(decimals) - 8)
241            return UInt256(amount * 100_000_000.0) * factor
242        }
243
244        /// Convert EVM UInt256 to Cadence UFix64 amount
245        access(self) fun fromEVMAmount(_ amount: UInt256, decimals: UInt8): UFix64 {
246            let factor = self.pow10(UInt8(decimals) - 8)
247            let cadenceAmount = amount / factor
248            return UFix64(cadenceAmount) / 100_000_000.0
249        }
250
251        /// Helper to calculate 10^n
252        access(self) fun pow10(_ n: UInt8): UInt256 {
253            var result: UInt256 = 1
254            var i: UInt8 = 0
255            while i < n {
256                result = result * 10
257                i = i + 1
258            }
259            return result
260        }
261
262        /// Get quote for a swap
263        access(all) fun getQuote(
264            fromTokenType: Type,
265            toTokenType: Type,
266            amount: UFix64
267        ): DeFiActions.Quote {
268            // Try to get a real quote from the router if COA is available
269            if self.coaCapability != nil {
270                if let coa = self.coaCapability!.borrow() {
271                    let evmAmount = self.toEVMAmount(amount, decimals: 18)
272                    
273                    // Call getAmountsOut on the router
274                    let quoteData = EVM.encodeABIWithSignature(
275                        "getAmountsOut(uint256,address[])",
276                        [evmAmount, self.tokenPath]
277                    )
278                    let quoteResult = coa.call(
279                        to: self.routerAddress,
280                        data: quoteData,
281                        gasLimit: 100_000,
282                        value: EVM.Balance(attoflow: 0)
283                    )
284                    
285                    if quoteResult.status == EVM.Status.successful {
286                        let decoded = EVM.decodeABI(types: [Type<[UInt256]>()], data: quoteResult.data)
287                        let amounts = decoded[0] as! [UInt256]
288                        let expectedOut = self.fromEVMAmount(amounts[amounts.length - 1], decimals: 18)
289                        let minAmount = expectedOut * 0.995 // 0.5% slippage
290                        
291                        emit QuoteFetched(
292                            routerAddress: self.routerAddress.toString(),
293                            amountIn: amount,
294                            amountOut: expectedOut
295                        )
296                        
297                        return DeFiActions.Quote(
298                            expectedAmount: expectedOut,
299                            minAmount: minAmount,
300                            slippageTolerance: 0.005,
301                            deadline: nil,
302                            data: {"dex": "UniswapV2" as AnyStruct, "router": self.routerAddress.toString() as AnyStruct}
303                        )
304                    }
305                }
306            }
307            
308            // Fallback to estimated quote
309            let estimatedOutput = amount * 0.997 // 0.3% fee estimate
310            return DeFiActions.Quote(
311                expectedAmount: estimatedOutput,
312                minAmount: estimatedOutput * 0.95, // 5% slippage for safety
313                slippageTolerance: 0.05,
314                deadline: nil,
315                data: {"dex": "UniswapV2" as AnyStruct, "estimated": true as AnyStruct}
316            )
317        }
318
319        /// Get execution effort (not tracked for V2)
320        access(all) fun getLastExecutionEffort(): UInt64 {
321            return 0
322        }
323
324        /// Get swapper info
325        access(all) fun getInfo(): DeFiActions.ComponentInfo {
326            return DeFiActions.ComponentInfo(
327                type: "Swapper",
328                identifier: "UniswapV2",
329                version: "2.0.0"
330            )
331        }
332    }
333
334    /// Create UniswapV2 swapper with COA capability
335    access(all) fun createSwapper(
336        routerAddress: EVM.EVMAddress,
337        factoryAddress: EVM.EVMAddress,
338        poolAddress: EVM.EVMAddress,
339        tokenPath: [EVM.EVMAddress],
340        cadenceTokenPath: [String],
341        coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?,
342        inVaultType: Type,
343        outVaultType: Type
344    ): @UniswapV2Swapper {
345        emit SwapperCreated(
346            routerAddress: routerAddress.toString(),
347            factoryAddress: factoryAddress.toString(),
348            tokenPath: cadenceTokenPath
349        )
350
351        return <- create UniswapV2Swapper(
352            routerAddress: routerAddress,
353            factoryAddress: factoryAddress,
354            poolAddress: poolAddress,
355            tokenPath: tokenPath,
356            cadenceTokenPath: cadenceTokenPath,
357            coaCapability: coaCapability,
358            inVaultType: inVaultType,
359            outVaultType: outVaultType
360        )
361    }
362
363    /// Create swapper with default addresses (PunchSwap V2)
364    access(all) fun createSwapperWithDefaults(
365        tokenPath: [EVM.EVMAddress],
366        cadenceTokenPath: [String],
367        coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>?,
368        inVaultType: Type,
369        outVaultType: Type
370    ): @UniswapV2Swapper {
371        return <- self.createSwapper(
372            routerAddress: self.defaultRouterAddress,
373            factoryAddress: self.defaultFactoryAddress,
374            poolAddress: self.defaultFactoryAddress, // Will be resolved at swap time
375            tokenPath: tokenPath,
376            cadenceTokenPath: cadenceTokenPath,
377            coaCapability: coaCapability,
378            inVaultType: inVaultType,
379            outVaultType: outVaultType
380        )
381    }
382
383    /// Admin resource for contract updates
384    access(all) resource Admin {
385        // Admin functions can be added here for future updates
386    }
387
388    init() {
389        self.AdminStoragePath = /storage/UniswapV2SwapperConnectorV2Admin
390
391        // PunchSwap V2 Mainnet addresses (Flow EVM Chain ID: 747)
392        self.defaultRouterAddress = EVM.addressFromString("0xf45AFe28fd5519d5f8C1d4787a4D5f724C0eFa4d")
393        self.defaultFactoryAddress = EVM.addressFromString("0x29372c22459a4e373851798bFd6808e71EA34A71")
394        
395        // WFLOW on Flow EVM Mainnet
396        self.wflowAddress = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e")
397    }
398}
399
400