@@ -45,6 +45,49 @@ access(all) contract UniswapV3SwapConnectors {
4545 return EVMAbiHelpers.concat (head).concat (EVMAbiHelpers.concat (tail))
4646 }
4747
48+ /// Convert an ERC20 `UInt256` amount into a Cadence `UFix64` **by rounding down** to the
49+ /// maximum `UFix64` precision (8 decimal places).
50+ ///
51+ /// - For `decimals <= 8`, the value is exactly representable, so this is a direct conversion.
52+ /// - For `decimals > 8`, this floors the ERC20 amount to the nearest multiple of
53+ /// `quantum = 10^(decimals - 8)` so the result round-trips safely:
54+ /// `ufix64ToUInt256(result) <= amt`.
55+ access (all) fun toCadenceOutWithDecimals (_ amt : UInt256 , decimals : UInt8 ): UFix64 {
56+ if decimals < = 8 {
57+ return FlowEVMBridgeUtils.uint256ToUFix64 (value : amt, decimals : decimals)
58+ }
59+
60+ let quantumExp : UInt8 = decimals - 8
61+ let quantum : UInt256 = FlowEVMBridgeUtils.pow (base : 10 , exponent : quantumExp)
62+ let remainder : UInt256 = amt % quantum
63+ let floored : UInt256 = amt - remainder
64+
65+ return FlowEVMBridgeUtils.uint256ToUFix64 (value : floored, decimals : decimals)
66+ }
67+
68+ /// Convert an ERC20 `UInt256` amount into a Cadence `UFix64` **by rounding up** to the
69+ /// smallest representable value at `UFix64` precision (8 decimal places).
70+ ///
71+ /// - For `decimals <= 8`, the value is exactly representable, so this is a direct conversion.
72+ /// - For `decimals > 8`, this ceils the ERC20 amount to the next multiple of
73+ /// `quantum = 10^(decimals - 8)` (unless already exact), ensuring:
74+ /// `ufix64ToUInt256(result) >= amt`, and the increase is `< quantum`.
75+ access (all) fun toCadenceInWithDecimals (_ amt : UInt256 , decimals : UInt8 ): UFix64 {
76+ if decimals < = 8 {
77+ return FlowEVMBridgeUtils.uint256ToUFix64 (value : amt, decimals : decimals)
78+ }
79+
80+ let quantumExp : UInt8 = decimals - 8
81+ let quantum : UInt256 = FlowEVMBridgeUtils.pow (base : 10 , exponent : quantumExp)
82+
83+ let remainder : UInt256 = amt % quantum
84+ var padded : UInt256 = amt
85+ if remainder ! = 0 {
86+ padded = amt + (quantum - remainder)
87+ }
88+
89+ return FlowEVMBridgeUtils.uint256ToUFix64 (value : padded, decimals : decimals)
90+ }
4891
4992 /// Swapper
5093 access (all) struct Swapper : DeFiActions .Swapper {
@@ -118,19 +161,20 @@ access(all) contract UniswapV3SwapConnectors {
118161 erc20Address : tokenEVMAddress
119162 )
120163
121- let maxAmount = self .getMaxAmount ( zeroForOne : reverse)
164+ let maxAmountOut = self .maxOutAmount ( reverse : reverse)
122165
123- var safeAmount = desired
124- if safeAmount > maxAmount {
125- safeAmount = maxAmount
166+ var safeAmountOut = desired
167+ if safeAmountOut > maxAmountOut {
168+ safeAmountOut = maxAmountOut
126169 }
127170
128- let safeAmountDesired = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount (
129- safeAmount,
171+ // Desired OUT amount => floor
172+ let safeAmountDesired = self ._toCadenceOut (
173+ safeAmountOut,
130174 erc20Address : tokenEVMAddress
131175 )
132- // panic("desired: \(desired), maxAmount: \(maxAmount), safeAmount: \(safeAmount)")
133- let amountIn = self .getV3Quote (out : false , amount : safeAmount , reverse : reverse)
176+
177+ let amountIn = self .getV3Quote (out : false , amount : safeAmountOut , reverse : reverse)
134178 return SwapConnectors.BasicQuote (
135179 inType : reverse ? self .outType () : self .inType (),
136180 outType : reverse ? self .inType () : self .outType (),
@@ -147,14 +191,15 @@ access(all) contract UniswapV3SwapConnectors {
147191 erc20Address : tokenEVMAddress
148192 )
149193
150- let maxAmount = self .getMaxAmount ( zeroForOne : reverse)
194+ let maxAmount = self .maxInAmount ( reverse : reverse)
151195
152196 var safeAmount = provided
153197 if safeAmount > maxAmount {
154198 safeAmount = maxAmount
155199 }
156200
157- let safeAmountProvided = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount (
201+ // Provided IN amount => ceil
202+ let safeAmountProvided = self ._toCadenceIn (
158203 safeAmount,
159204 erc20Address : tokenEVMAddress
160205 )
@@ -251,6 +296,62 @@ access(all) contract UniswapV3SwapConnectors {
251296
252297 return EVM.EVMAddress (bytes : addrBytes)
253298 }
299+ access (self ) fun getPoolTokens (_ pool : EVM .EVMAddress ): [EVM .EVMAddress ] {
300+ let SEL_TOKEN0 : [UInt8 ] = [0x0d , 0xfe , 0x16 , 0x81 ] // token0()
301+ let SEL_TOKEN1 : [UInt8 ] = [0xd2 , 0x12 , 0x20 , 0xa7 ] // token1()
302+
303+ let t0Res = self ._callRaw (
304+ to : pool,
305+ calldata : EVMAbiHelpers.buildCalldata (selector : SEL_TOKEN0, args : []),
306+ gasLimit : 200_000 ,
307+ value : 0
308+ )!
309+ let t1Res = self ._callRaw (
310+ to : pool,
311+ calldata : EVMAbiHelpers.buildCalldata (selector : SEL_TOKEN1, args : []),
312+ gasLimit : 200_000 ,
313+ value : 0
314+ )!
315+
316+ let t0Bytes = (t0Res.data.slice (from : 12 , upTo : 32 ))
317+ let t1Bytes = (t1Res.data.slice (from : 12 , upTo : 32 ))
318+
319+ return [
320+ EVM.EVMAddress (bytes : self .to20 (t0Bytes)),
321+ EVM.EVMAddress (bytes : self .to20 (t1Bytes))
322+ ]
323+ }
324+
325+ access (self ) fun maxInAmount (reverse : Bool ): UInt256 {
326+ let pool = self .getPoolAddress ()
327+ let tokens = self .getPoolTokens (pool)
328+ let token0 = tokens[0 ]
329+ let token1 = tokens[1 ]
330+
331+ let input = reverse ? self .tokenPath[self .tokenPath.length - 1 ]
332+ : self .tokenPath[0 ]
333+
334+ let zeroForOne = (input.toString () == token0.toString ()) // input == token0 ? 0→1 : 1→0
335+ return self .getMaxAmount (zeroForOne : zeroForOne)
336+ }
337+
338+ access (self ) fun maxOutAmount (reverse : Bool ): UInt256 {
339+ let maxIn = self .maxInAmount (reverse : reverse)
340+
341+ // Max out at that max-in, using quoteExactInput
342+ let maxOutUFix : UFix64 = self .getV3Quote (out : true , amount : maxIn, reverse : reverse)
343+ ?? 0 .0
344+
345+ // OUT token address
346+ let outToken = reverse
347+ ? self .tokenPath[0 ] // reverse: path[last] -> ... -> path[0], so out is path[0]
348+ : self .tokenPath[self .tokenPath.length - 1 ]
349+
350+ return FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount (
351+ maxOutUFix,
352+ erc20Address : outToken
353+ )
354+ }
254355
255356 /// Simplified getMaxAmount using default 6% price impact
256357 /// Uses current liquidity as proxy for max swappable amount
@@ -304,7 +405,7 @@ access(all) contract UniswapV3SwapConnectors {
304405 )
305406 let L = wordToUIntN (words (liqRes! .data)[0 ], 128 )
306407
307- // Calculate price multiplier based on 6 % price impact (600 bps)
408+ // Calculate price multiplier based on 4 % price impact (600 bps)
308409 // Use UInt256 throughout to prevent overflow in multiplication operations
309410 let bps : UInt256 = 600
310411 let Q96 : UInt256 = 0x1000000000000000000000000
@@ -325,7 +426,10 @@ access(all) contract UniswapV3SwapConnectors {
325426 // Since sqrt prices are in Q96 format: (L * ΔsqrtP * Q96) / (sqrtP * sqrtP')
326427 // This gives us native token0 units after the two Q96 divisions cancel with one Q96 multiplication
327428 let numerator : UInt256 = L_256 * deltaSqrt
328- maxAmount = (numerator * Q96) / sqrtPriceX96_256 / sqrtPriceNew
429+ let num1 : UInt256 = L_256 * bps
430+ let num2 : UInt256 = num1 * Q96
431+ let den : UInt256 = UInt256 (20000 ) * sqrtPriceNew
432+ maxAmount = den == 0 ? UInt256 (0 ) : num2 / den
329433 } else {
330434 // Swapping token1 -> token0 (price increases by maxPriceImpactBps)
331435 // Formula: Δy = L * (√P' - √P)
@@ -366,7 +470,13 @@ access(all) contract UniswapV3SwapConnectors {
366470 ? (out ? self .tokenPath[0 ] : self .tokenPath[self .tokenPath.length - 1 ])
367471 : (out ? self .tokenPath[self .tokenPath.length - 1 ] : self .tokenPath[0 ])
368472
369- return FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount (uintAmt, erc20Address : ercAddr)
473+ // out == true => quoteExactInput => result is an OUT amount => floor
474+ // out == false => quoteExactOutput => result is an IN amount => ceil
475+ if out {
476+ return self ._toCadenceOut (uintAmt, erc20Address : ercAddr)
477+ } else {
478+ return self ._toCadenceIn (uintAmt, erc20Address : ercAddr)
479+ }
370480 }
371481
372482 /// Executes exact input swap via router
@@ -456,8 +566,21 @@ access(all) contract UniswapV3SwapConnectors {
456566 let decoded = EVM.decodeABI (types : [Type< UInt256> ()], data: swapRes.data)
457567 let amountOut : UInt256 = decoded.length > 0 ? decoded[0 ] as! UInt256 : UInt256 (0 )
458568
569+ let outTokenEVMAddress =
570+ FlowEVMBridgeConfig.getEVMAddressAssociated (with : self .outType ())
571+ ?? panic (" out token \(self.outType().identifier) is not bridged" )
572+
573+ let outUFix = self ._toCadenceOut (
574+ amountOut,
575+ erc20Address : outTokenEVMAddress
576+ )
577+
578+ let safeAmountOut = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount (
579+ outUFix,
580+ erc20Address : outTokenEVMAddress
581+ )
459582 // Withdraw output back to Flow
460- let outVault <- coa.withdrawTokens (type : self .outType (), amount : amountOut , feeProvider : feeVaultRef)
583+ let outVault <- coa.withdrawTokens (type : self .outType (), amount : safeAmountOut , feeProvider : feeVaultRef)
461584
462585 // Handle leftover fee vault
463586 self ._handleRemainingFeeVault (<- feeVault)
@@ -501,6 +624,19 @@ access(all) contract UniswapV3SwapConnectors {
501624 Burner.burn (<- vault)
502625 }
503626 }
627+
628+ /// OUT amounts: round down to UFix64 precision
629+ access (self ) fun _toCadenceOut (_ amt : UInt256 , erc20Address : EVM .EVMAddress ): UFix64 {
630+ let decimals = FlowEVMBridgeUtils.getTokenDecimals (evmContractAddress : erc20Address)
631+ return UniswapV3SwapConnectors.toCadenceOutWithDecimals (amt, decimals : decimals)
632+ }
633+
634+ /// IN amounts: round up to the next UFix64 such that the ERC20 conversion
635+ /// (via ufix64ToUInt256) is >= the original UInt256 amount.
636+ access (self ) fun _toCadenceIn (_ amt : UInt256 , erc20Address : EVM .EVMAddress ): UFix64 {
637+ let decimals = FlowEVMBridgeUtils.getTokenDecimals (evmContractAddress : erc20Address)
638+ return UniswapV3SwapConnectors.toCadenceInWithDecimals (amt, decimals : decimals)
639+ }
504640 }
505641
506642 /// Revert helper
0 commit comments