Smart Contract
UniswapV2SwapperConnectorV2
A.17ae3b1b0b0d50db.UniswapV2SwapperConnectorV2
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