Smart Contract
UniswapV3SwapConnectors
A.a7825d405ac89518.UniswapV3SwapConnectors
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import Burner from 0xf233dcee88fe0abe
4import EVM from 0xe467b9dd11fa00df
5import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
6import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
7
8import DeFiActions from 0x6d888f175c158410
9import SwapConnectors from 0xe1a479f0cb911df9
10import EVMAbiHelpers from 0xa7825d405ac89518
11import EVMAmountUtils from 0x43c9e8bfec507db4
12
13/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
15/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16///
17/// UniswapV3SwapConnectors
18///
19/// DeFiActions Swapper connector implementation for Uniswap V3 routers on Flow EVM.
20/// Supports single-hop and multi-hop swaps using exactInput / exactInputSingle and Quoter for estimates.
21///
22access(all) contract UniswapV3SwapConnectors {
23
24 /// ExactInputSingleParams facilitates the ABI encoding/decoding of the
25 /// Solidity tuple expected in `ISwapRouter.exactInput` function.
26 access(all) struct ExactInputSingleParams {
27 access(all) let path: EVM.EVMBytes
28 access(all) let recipient: EVM.EVMAddress
29 access(all) let amountIn: UInt256
30 access(all) let amountOutMinimum: UInt256
31
32 init(
33 path: EVM.EVMBytes,
34 recipient: EVM.EVMAddress,
35 amountIn: UInt256,
36 amountOutMinimum: UInt256
37 ) {
38 self.path = path
39 self.recipient = recipient
40 self.amountIn = amountIn
41 self.amountOutMinimum = amountOutMinimum
42 }
43 }
44
45 /// Swapper
46 access(all) struct Swapper: DeFiActions.Swapper {
47 access(all) let routerAddress: EVM.EVMAddress
48 access(all) let quoterAddress: EVM.EVMAddress
49 access(self) let factoryAddress: EVM.EVMAddress
50
51 access(all) let tokenPath: [EVM.EVMAddress]
52 access(all) let feePath: [UInt32]
53
54 access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
55
56 access(self) let inVault: Type
57 access(self) let outVault: Type
58
59 access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
60
61 init(
62 factoryAddress: EVM.EVMAddress,
63 routerAddress: EVM.EVMAddress,
64 quoterAddress: EVM.EVMAddress,
65 tokenPath: [EVM.EVMAddress],
66 feePath: [UInt32],
67 inVault: Type,
68 outVault: Type,
69 coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
70 uniqueID: DeFiActions.UniqueIdentifier?
71 ) {
72 pre {
73 tokenPath.length >= 2: "tokenPath must contain at least two addresses"
74 feePath.length == tokenPath.length - 1: "feePath length must be tokenPath.length - 1"
75 FlowEVMBridgeConfig.getTypeAssociated(with: tokenPath[0]) == inVault:
76 "Provided inVault \(inVault.identifier) is not associated with ERC20 at tokenPath[0]"
77 FlowEVMBridgeConfig.getTypeAssociated(with: tokenPath[tokenPath.length - 1]) == outVault:
78 "Provided outVault \(outVault.identifier) is not associated with ERC20 at tokenPath[last]"
79 coaCapability.check():
80 "Provided COA Capability is invalid - need Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>"
81 }
82 self.factoryAddress = factoryAddress
83 self.routerAddress = routerAddress
84 self.quoterAddress = quoterAddress
85 self.tokenPath = tokenPath
86 self.feePath = feePath
87 self.inVault = inVault
88 self.outVault = outVault
89 self.coaCapability = coaCapability
90 self.uniqueID = uniqueID
91 }
92
93 /* --- DeFiActions.Swapper conformance --- */
94
95 access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
96 return DeFiActions.ComponentInfo(
97 type: self.getType(),
98 id: self.uniqueID?.id,
99 innerComponents: []
100 )
101 }
102
103 access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID }
104 access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id }
105
106 access(all) view fun inType(): Type { return self.inVault }
107 access(all) view fun outType(): Type { return self.outVault }
108
109
110 access(self) view fun outToken(_ reverse: Bool): EVM.EVMAddress {
111 if reverse {
112 return self.tokenPath[0]
113 }
114 return self.tokenPath[self.tokenPath.length - 1]
115 }
116 access(self) view fun inToken(_ reverse: Bool): EVM.EVMAddress {
117 if reverse {
118 return self.tokenPath[self.tokenPath.length - 1]
119 }
120 return self.tokenPath[0]
121 }
122
123 /// Estimate required input for a desired output
124 access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {DeFiActions.Quote} {
125 // OUT token for this direction
126 let outToken = self.outToken(reverse)
127 let desiredOutEVM = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
128 forDesired,
129 erc20Address: outToken
130 )
131
132 // Derive true Uniswap direction for pool math
133 let zeroForOne = self.isZeroForOne(reverse: reverse)
134
135 // Max INPUT proxy in correct pool terms
136 // TODO: Multi-hop clamp currently uses the first pool (tokenPath[0]/[1]) even in reverse;
137 // consider clamping per-hop or disabling clamp when tokenPath.length > 2.
138 let maxInEVM = self.getMaxInAmount(zeroForOne: zeroForOne)
139
140 // If clamp proxy is 0, don't clamp — it's a truncation/edge case
141 var safeOutEVM = desiredOutEVM
142
143 if maxInEVM > 0 {
144 // Translate max input -> max output using exactInput quote
145 if let maxOutCadence = self.getV3Quote(out: true, amount: maxInEVM, reverse: reverse) {
146 let maxOutEVM = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
147 maxOutCadence,
148 erc20Address: outToken
149 )
150 if safeOutEVM > maxOutEVM {
151 safeOutEVM = maxOutEVM
152 }
153 }
154 // If maxOutCadence is nil, we also skip clamping (better than forcing 0)
155 }
156
157 let safeOutCadence = self._toCadenceOut(
158 safeOutEVM,
159 erc20Address: outToken
160 )
161
162 // ExactOutput quote: how much IN required for safeOutEVM OUT
163 let amountInCadence = self.getV3Quote(out: false, amount: safeOutEVM, reverse: reverse)
164
165 // Refine outAmount: the ceiled input may produce more output than safeOutCadence
166 // because (a) UFix64 ceiling rounds the input up and (b) the pool's exactOutput/
167 // exactInput math is not perfectly invertible. Do a follow-up exactInput quote
168 // with the ceiled input so that quoteIn.outAmount matches what a subsequent
169 // quoteOut(forProvided: ceiledInput) would return. This keeps quote-level dust
170 // bounded at ≤ 1 UFix64 quantum (0.00000001).
171 var refinedOutCadence = safeOutCadence
172 if let inCadence = amountInCadence {
173 let inToken = self.inToken(reverse)
174 let ceiledInEVM = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
175 inCadence,
176 erc20Address: inToken
177 )
178 if let forwardOut = self.getV3Quote(out: true, amount: ceiledInEVM, reverse: reverse) {
179 refinedOutCadence = forwardOut
180 }
181 }
182
183 return SwapConnectors.BasicQuote(
184 inType: reverse ? self.outType() : self.inType(),
185 outType: reverse ? self.inType() : self.outType(),
186 inAmount: amountInCadence ?? 0.0,
187 outAmount: amountInCadence != nil ? refinedOutCadence : 0.0
188 )
189 }
190
191 /// Estimate output for a provided input
192 access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {DeFiActions.Quote} {
193 // IN token for this direction
194 let inToken = self.inToken(reverse)
195 let providedInEVM = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
196 forProvided,
197 erc20Address: inToken
198 )
199
200 // Max INPUT proxy in correct pool terms
201 // TODO: Multi-hop clamp currently uses the first pool (tokenPath[0]/[1]) even in reverse;
202 // consider clamping per-hop or disabling clamp when tokenPath.length > 2.
203 let maxInEVM = self.maxInAmount(reverse: reverse)
204
205 // If clamp proxy is 0, don't clamp — it's a truncation/edge case
206 var safeInEVM = providedInEVM
207 if maxInEVM > 0 && safeInEVM > maxInEVM {
208 safeInEVM = maxInEVM
209 }
210
211 // Provided IN amount => ceil
212 let safeInCadence = self._toCadenceIn(
213 safeInEVM,
214 erc20Address: inToken
215 )
216
217 // ExactInput quote: how much OUT for safeInEVM IN
218 let amountOutCadence = self.getV3Quote(out: true, amount: safeInEVM, reverse: reverse)
219
220 return SwapConnectors.BasicQuote(
221 inType: reverse ? self.outType() : self.inType(),
222 outType: reverse ? self.inType() : self.outType(),
223 inAmount: amountOutCadence != nil ? safeInCadence : 0.0,
224 outAmount: amountOutCadence ?? 0.0
225 )
226 }
227
228 /// Swap exact input -> min output using Uniswap V3 exactInput/Single
229 access(all) fun swap(quote: {DeFiActions.Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
230 let minOut = quote?.outAmount ?? self.quoteOut(forProvided: inVault.balance, reverse: false).outAmount
231 return <- self._swapExactIn(exactVaultIn: <-inVault, amountOutMin: minOut, reverse: false)
232 }
233
234 /// Swap back (exact input of residual -> min output)
235 access(all) fun swapBack(quote: {DeFiActions.Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
236 let minOut = quote?.outAmount ?? self.quoteOut(forProvided: residual.balance, reverse: true).outAmount
237 return <- self._swapExactIn(exactVaultIn: <-residual, amountOutMin: minOut, reverse: true)
238 }
239
240 /* --- Core swap / quote internals --- */
241
242 /// Build Uniswap V3 path bytes:
243 /// token0(20) | fee0(3) | token1(20) | fee1(3) | token2(20) | ...
244 access(self) fun _buildPathBytes(reverse: Bool): EVM.EVMBytes {
245 var out: [UInt8] = []
246
247 // helper to append address bytes
248 fun appendAddr(_ a: EVM.EVMAddress) {
249 let fixed: [UInt8; 20] = a.bytes
250 var i = 0
251 while i < 20 {
252 out.append(fixed[i])
253 i = i + 1
254 }
255 }
256
257 // helper to append uint24 fee big-endian
258 fun appendFee(_ f: UInt32) {
259 // validate fee fits uint24
260 pre { f <= 0xFFFFFF: "feePath element exceeds uint24" }
261 out.append(UInt8((f >> 16) & 0xFF))
262 out.append(UInt8((f >> 8) & 0xFF))
263 out.append(UInt8(f & 0xFF))
264 }
265
266 let nHops = self.feePath.length
267 let last = self.tokenPath.length - 1
268
269 // choose first token based on direction
270 let first = reverse ? self.tokenPath[last] : self.tokenPath[0]
271 appendAddr(first)
272
273 var i = 0
274 while i < nHops {
275 let feeIdx = reverse ? (nHops - 1 - i) : i
276 let nextIdx = reverse ? (last - (i + 1)) : (i + 1)
277
278 appendFee(self.feePath[feeIdx])
279 appendAddr(self.tokenPath[nextIdx])
280
281 i = i + 1
282 }
283
284 return EVM.EVMBytes(value: out)
285 }
286
287 access(self) fun getPoolAddress(): EVM.EVMAddress {
288 let res = self._dryCall(
289 self.factoryAddress,
290 "getPool(address,address,uint24)",
291 [ self.tokenPath[0], self.tokenPath[1], UInt256(self.feePath[0]) ],
292 120_000
293 )!
294 assert(res.status == EVM.Status.successful, message: "unable to get pool: token0 \(self.tokenPath[0].toString()), token1 \(self.tokenPath[1].toString()), feePath: self.feePath[0]")
295
296 // ABI return is one 32-byte word; the last 20 bytes are the address
297 let word = res.data as! [UInt8]
298 if word.length < 32 { panic("getPool: invalid ABI word length") }
299
300 let addrSlice = word.slice(from: 12, upTo: 32) // 20 bytes
301 let addrBytes: [UInt8; 20] = addrSlice.toConstantSized<[UInt8; 20]>()!
302
303 return EVM.EVMAddress(bytes: addrBytes)
304 }
305
306 access(self) fun maxInAmount(reverse: Bool): UInt256 {
307 let zeroForOne = self.isZeroForOne(reverse: reverse)
308 return self.getMaxInAmount(zeroForOne: zeroForOne)
309 }
310
311 /// Simplified max input calculation using default 6% price impact
312 /// Uses current liquidity as proxy for max swappable input amount
313 access(self) fun getMaxInAmount(zeroForOne: Bool): UInt256 {
314 let poolEVMAddress = self.getPoolAddress()
315
316 // Helper functions
317 fun wordToUInt(_ w: [UInt8]): UInt {
318 var acc: UInt = 0
319 var i = 0
320 while i < 32 { acc = (acc << 8) | UInt(w[i]); i = i + 1 }
321 return acc
322 }
323 fun wordToUIntN(_ w: [UInt8], _ nBits: Int): UInt {
324 let full = wordToUInt(w)
325 if nBits >= 256 { return full }
326 let mask: UInt = (UInt(1) << UInt(nBits)) - UInt(1)
327 return full & mask
328 }
329 fun words(_ data: [UInt8]): [[UInt8]] {
330 let n = data.length / 32
331 var out: [[UInt8]] = []
332 var i = 0
333 while i < n {
334 out.append(data.slice(from: i*32, upTo: (i+1)*32))
335 i = i + 1
336 }
337 return out
338 }
339
340 // Selectors
341 let SEL_SLOT0: [UInt8] = [0x38, 0x50, 0xc7, 0xbd]
342 let SEL_LIQUIDITY: [UInt8] = [0x1a, 0x68, 0x65, 0x02]
343
344 // Get slot0 (sqrtPriceX96, tick, etc.)
345 let s0Res: EVM.Result? = self._dryCallRaw(
346 to: poolEVMAddress,
347 calldata: EVMAbiHelpers.buildCalldata(selector: SEL_SLOT0, args: []),
348 gasLimit: 1_000_000,
349 )
350 let s0w = words(s0Res!.data)
351 let sqrtPriceX96 = wordToUIntN(s0w[0], 160)
352
353 // Get current active liquidity
354 let liqRes: EVM.Result? = self._dryCallRaw(
355 to: poolEVMAddress,
356 calldata: EVMAbiHelpers.buildCalldata(selector: SEL_LIQUIDITY, args: []),
357 gasLimit: 300_000,
358 )
359 let L = wordToUIntN(words(liqRes!.data)[0], 128)
360
361 // Calculate price multiplier based on 6% price impact (600 bps)
362 // Use UInt256 throughout to prevent overflow in multiplication operations
363 let bps: UInt256 = 600
364 let Q96: UInt256 = 0x1000000000000000000000000
365 let sqrtPriceX96_256: UInt256 = UInt256(sqrtPriceX96)
366 let L_256: UInt256 = UInt256(L)
367
368 var maxAmount: UInt256 = 0
369 if zeroForOne {
370 // Swapping token0 -> token1 (price decreases by maxPriceImpactBps)
371 // Formula: Δx = L * (√P - √P') / (√P * √P')
372 // Approximation: √P' ≈ √P * (1 - priceImpact/2)
373 let sqrtMultiplier: UInt256 = 10000 - (bps / 2)
374 let sqrtPriceNew: UInt256 = (sqrtPriceX96_256 * sqrtMultiplier) / 10000
375
376 // Uniswap V3 spec: getAmount0Delta
377 // Δx = L * (√P - √P') / (√P * √P')
378 // Since sqrt prices are in Q96 format: (L * ΔsqrtP * Q96) / (sqrtP * sqrtP')
379 // This gives us native token0 units after the two Q96 divisions cancel with one Q96 multiplication
380 let num1: UInt256 = L_256 * bps
381 let num2: UInt256 = num1 * Q96
382 let den: UInt256 = UInt256(20000) * sqrtPriceNew
383 maxAmount = den == 0 ? UInt256(0) : num2 / den
384 } else {
385 // Swapping token1 -> token0 (price increases by maxPriceImpactBps)
386 // Formula: Δy = L * (√P' - √P)
387 // Approximation: √P' ≈ √P * (1 + priceImpact/2)
388 let sqrtMultiplier: UInt256 = 10000 + (bps / 2)
389 let sqrtPriceNew: UInt256 = (sqrtPriceX96_256 * sqrtMultiplier) / 10000
390 let deltaSqrt: UInt256 = sqrtPriceNew - sqrtPriceX96_256
391
392 // Uniswap V3 spec: getAmount1Delta
393 // Δy = L * (√P' - √P)
394 // Divide by Q96 to convert from Q96 format to native token units
395 maxAmount = (L_256 * deltaSqrt) / Q96
396 }
397
398 return maxAmount
399 }
400
401 /// Quote using the Uniswap V3 Quoter via dryCall
402 access(self) fun getV3Quote(out: Bool, amount: UInt256, reverse: Bool): UFix64? {
403 // For exactOutput, the path must be reversed (tokenOut -> ... -> tokenIn)
404 let pathReverse = out ? reverse : !reverse
405 let pathBytes = self._buildPathBytes(reverse: pathReverse)
406
407 let callSig = out
408 ? "quoteExactInput(bytes,uint256)"
409 : "quoteExactOutput(bytes,uint256)"
410
411 let args: [AnyStruct] = [pathBytes, amount]
412
413 let res = self._dryCall(self.quoterAddress, callSig, args, 10_000_000)
414 if res == nil || res!.status != EVM.Status.successful { return nil }
415
416 let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: res!.data)
417 if decoded.length == 0 { return nil }
418 let uintAmt = decoded[0] as! UInt256
419
420 let ercAddr = out
421 ? self.outToken(reverse)
422 : self.inToken(reverse)
423
424 // out == true => quoteExactInput => result is an OUT amount => floor
425 // out == false => quoteExactOutput => result is an IN amount => ceil
426 if out {
427 return self._toCadenceOut(uintAmt, erc20Address: ercAddr)
428 } else {
429 return self._toCadenceIn(uintAmt, erc20Address: ercAddr)
430 }
431 }
432
433 /// Executes exact input swap via router
434 access(self) fun _swapExactIn(exactVaultIn: @{FungibleToken.Vault}, amountOutMin: UFix64, reverse: Bool): @{FungibleToken.Vault} {
435 let id = self.uniqueID?.id?.toString() ?? "UNASSIGNED"
436 let idType = self.uniqueID?.getType()?.identifier ?? "UNASSIGNED"
437 let coa = self.borrowCOA()
438 ?? panic("Invalid COA Capability in V3 Swapper \(self.getType().identifier) ID \(idType)#\(id)")
439
440 // Bridge fee
441 let bridgeFeeBalance = EVM.Balance(attoflow: 0)
442 bridgeFeeBalance.setFLOW(flow: 2.0 * FlowEVMBridgeUtils.calculateBridgeFee(bytes: 256))
443 let feeVault <- coa.withdraw(balance: bridgeFeeBalance)
444 let feeVaultRef = &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
445
446 // I/O tokens
447 let inToken = self.inToken(reverse)
448 let outToken = self.outToken(reverse)
449
450 // Bridge input to EVM
451 let evmAmountIn = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(exactVaultIn.balance, erc20Address: inToken)
452 coa.depositTokens(vault: <-exactVaultIn, feeProvider: feeVaultRef)
453
454 // Build path
455 let pathBytes = self._buildPathBytes(reverse: reverse)
456
457 // Approve
458 var res = self._call(
459 to: inToken,
460 signature: "approve(address,uint256)",
461 args: [self.routerAddress, evmAmountIn],
462 gasLimit: 120_000,
463 value: 0
464 )!
465 if res.status != EVM.Status.successful {
466 UniswapV3SwapConnectors._callError("approve(address,uint256)", res, inToken, idType, id, self.getType())
467 }
468
469 // Min out on EVM units
470 let minOutUint = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
471 amountOutMin,
472 erc20Address: outToken
473 )
474
475 let coaRef = self.borrowCOA()!
476 let recipient: EVM.EVMAddress = coaRef.address()
477
478 // optional dev guards
479 let _chkIn = EVMAbiHelpers.abiUInt256(evmAmountIn)
480 let _chkMin = EVMAbiHelpers.abiUInt256(minOutUint)
481 //panic("path: \(EVMAbiHelpers.toHex(pathBytes.value)), amountIn: \(evmAmountIn.toString()), amountOutMin: \(minOutUint.toString())")
482 assert(_chkIn.length == 32, message: "amountIn not 32 bytes")
483 assert(_chkMin.length == 32, message: "amountOutMin not 32 bytes")
484
485 let exactInputParams = UniswapV3SwapConnectors.ExactInputSingleParams(
486 path: pathBytes,
487 recipient: recipient,
488 amountIn: evmAmountIn,
489 amountOutMinimum: minOutUint
490 )
491
492 let calldata: [UInt8] = EVM.encodeABIWithSignature(
493 "exactInput((bytes,address,uint256,uint256))",
494 [exactInputParams]
495 )
496
497 // Call the router with raw calldata
498 let swapRes = self._callRaw(
499 to: self.routerAddress,
500 calldata: calldata,
501 gasLimit: 10_000_000,
502 value: 0
503 )!
504 if swapRes.status != EVM.Status.successful {
505 UniswapV3SwapConnectors._callError(
506 EVMAbiHelpers.toHex(calldata),
507 swapRes, self.routerAddress, idType, id, self.getType()
508 )
509 }
510 let decoded = EVM.decodeABI(types: [Type<UInt256>()], data: swapRes.data)
511 let amountOut: UInt256 = decoded.length > 0 ? decoded[0] as! UInt256 : UInt256(0)
512
513 let outVaultType = reverse ? self.inType() : self.outType()
514 let outTokenEVMAddress =
515 FlowEVMBridgeConfig.getEVMAddressAssociated(with: outVaultType)
516 ?? panic("out token \(outVaultType.identifier) is not bridged")
517
518 let outUFix = self._toCadenceOut(
519 amountOut,
520 erc20Address: outTokenEVMAddress
521 )
522
523 // Defensive: ensure the router respected amountOutMinimum.
524 // Under normal operation the V3 router reverts when output < min, but guard
525 // against a buggy or malicious router contract.
526 assert(
527 amountOutMin == 0.0 || outUFix >= amountOutMin,
528 message: "UniswapV3SwapConnectors: swap output \(outUFix.toString()) < amountOutMin \(amountOutMin.toString())"
529 )
530
531 /// Quoting exact output then swapping exact input can overshoot by up to 0.00000001 (1 UFix64 quantum)
532 /// when the pool's effective exchange rate is near 1:1.
533 ///
534 /// UFix64 has 8 decimals; EVM tokens typically have 18. One UFix64 step = 10^10 wei.
535 ///
536 /// Example (pool price 1 FLOW = 2 USDC, want 10 USDC out):
537 /// 1. Quoter says need 5,000000002000000000 FLOW wei
538 /// 2. Ceil to UFix64: 5,000000010000000000 (overshoot: 8e9 wei)
539 /// 3. exactInput swaps the ceiled amount; extra 8e9 FLOW wei × 2 = 16e9 USDC wei extra
540 /// 4. Actual output: 10,000000016000000000 USDC wei
541 /// 5. Floor to UFix64: 10.00000001 USDC (quoted 10.00000000)
542 ///
543 /// The overshoot is always non-negative (ceiled input >= what pool needs).
544 /// It surfaces when the extra output crosses a 10^10 wei quantum boundary.
545 /// Cap at amountOutMin so only the expected amount is bridged; dust stays in the COA.
546 let bridgeUFix = outUFix > amountOutMin && amountOutMin > 0.0 ? amountOutMin : outUFix
547 let dust = outUFix > bridgeUFix ? outUFix - bridgeUFix : 0.0
548 let safeAmountOut = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
549 bridgeUFix,
550 erc20Address: outTokenEVMAddress
551 )
552 // Withdraw output back to Flow; sub-quantum remainder and any overshoot stay in COA
553 let outVault <- coa.withdrawTokens(type: outVaultType, amount: safeAmountOut, feeProvider: feeVaultRef)
554
555 // Handle leftover fee vault
556 self._handleRemainingFeeVault(<-feeVault)
557 return <- outVault
558 }
559
560 /* --- Helpers --- */
561
562 access(self) view fun borrowCOA(): auth(EVM.Owner) &EVM.CadenceOwnedAccount? { return self.coaCapability.borrow() }
563
564 access(self) fun _dryCall(_ to: EVM.EVMAddress, _ signature: String, _ args: [AnyStruct], _ gas: UInt64): EVM.Result? {
565 let calldata = EVM.encodeABIWithSignature(signature, args)
566 let valueBalance = EVM.Balance(attoflow: 0)
567 if let coa = self.borrowCOA() {
568 return coa.dryCall(to: to, data: calldata, gasLimit: gas, value: valueBalance)
569 }
570 return nil
571 }
572
573 access(self) fun _dryCallRaw(to: EVM.EVMAddress, calldata: [UInt8], gasLimit: UInt64): EVM.Result? {
574 let valueBalance = EVM.Balance(attoflow: 0)
575 if let coa = self.borrowCOA() {
576 return coa.dryCall(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
577 }
578 return nil
579 }
580
581 access(self) fun _call(to: EVM.EVMAddress, signature: String, args: [AnyStruct], gasLimit: UInt64, value: UInt): EVM.Result? {
582 let calldata = EVM.encodeABIWithSignature(signature, args)
583 let valueBalance = EVM.Balance(attoflow: value)
584 if let coa = self.borrowCOA() {
585 return coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
586 }
587 return nil
588 }
589
590 access(self) fun _callRaw(to: EVM.EVMAddress, calldata: [UInt8], gasLimit: UInt64, value: UInt): EVM.Result? {
591 let valueBalance = EVM.Balance(attoflow: value)
592 if let coa = self.borrowCOA() {
593 return coa.call(to: to, data: calldata, gasLimit: gasLimit, value: valueBalance)
594 }
595 return nil
596 }
597
598 access(self) fun _handleRemainingFeeVault(_ vault: @FlowToken.Vault) {
599 if vault.balance > 0.0 {
600 self.borrowCOA()!.deposit(from: <-vault)
601 } else {
602 Burner.burn(<-vault)
603 }
604 }
605
606 /// OUT amounts: round down to UFix64 precision
607 access(self) fun _toCadenceOut(_ amt: UInt256, erc20Address: EVM.EVMAddress): UFix64 {
608 return EVMAmountUtils.toCadenceOutForToken(amt, erc20Address: erc20Address)
609 }
610
611 /// IN amounts: round up to the next UFix64 such that the ERC20 conversion
612 /// (via ufix64ToUInt256) is >= the original UInt256 amount.
613 access(self) fun _toCadenceIn(_ amt: UInt256, erc20Address: EVM.EVMAddress): UFix64 {
614 return EVMAmountUtils.toCadenceInForToken(amt, erc20Address: erc20Address)
615 }
616 access(self) fun getPoolToken0(_ pool: EVM.EVMAddress): EVM.EVMAddress {
617 // token0() selector = 0x0dfe1681
618 let SEL_TOKEN0: [UInt8] = [0x0d, 0xfe, 0x16, 0x81]
619 let res = self._dryCallRaw(
620 to: pool,
621 calldata: EVMAbiHelpers.buildCalldata(selector: SEL_TOKEN0, args: []),
622 gasLimit: 150_000,
623 )!
624 assert(res.status == EVM.Status.successful, message: "token0() call failed")
625
626 let word = res.data as! [UInt8]
627 let addrSlice = word.slice(from: 12, upTo: 32)
628 let addrBytes: [UInt8; 20] = addrSlice.toConstantSized<[UInt8; 20]>()!
629 return EVM.EVMAddress(bytes: addrBytes)
630 }
631
632 access(self) fun isZeroForOne(reverse: Bool): Bool {
633 let pool = self.getPoolAddress()
634 let token0 = self.getPoolToken0(pool)
635
636 // your actual input token for this swap direction:
637 let inToken = self.inToken(reverse)
638
639 return inToken.equals(token0)
640 }
641 }
642
643 /// Revert helper
644 access(self)
645 fun _callError(
646 _ signature: String,
647 _ res: EVM.Result,
648 _ target: EVM.EVMAddress,
649 _ uniqueIDType: String,
650 _ id: String,
651 _ swapperType: Type
652 ) {
653 panic(
654 "Call to \(target.toString()).\(signature) from Swapper \(swapperType.identifier) with UniqueIdentifier \(uniqueIDType) ID \(id) failed:\n\tStatus value: \(res.status.rawValue.toString())\n\tError code: \(res.errorCode.toString())\n\tErrorMessage: \(res.errorMessage)\n"
655 )
656 }
657}
658