Skip to content

Commit 95005ee

Browse files
authored
Nialexsan/extra check erc4626 (#79)
* extra check * round up / round down * fix max check * extra check * refine math * add tests * add comments
1 parent 7ded36c commit 95005ee

File tree

3 files changed

+227
-14
lines changed

3 files changed

+227
-14
lines changed

cadence/contracts/connectors/evm/ERC4626SwapConnectors.cdc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ access(all) contract ERC4626SwapConnectors {
106106
}
107107
let uintForDesired = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(forDesired, erc20Address: self.vault)
108108
if let uintRequired = ERC4626Utils.previewMint(vault: self.vault, shares: uintForDesired) {
109+
let uintMaxAllowed = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(UFix64.max, erc20Address: self.vault)
110+
111+
if uintRequired > uintMaxAllowed {
112+
return SwapConnectors.BasicQuote(
113+
inType: self.asset,
114+
outType: self.vaultType,
115+
inAmount: UFix64.max,
116+
outAmount: forDesired
117+
)
118+
}
109119
let ufixRequired = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(uintRequired, erc20Address: self.assetEVMAddress)
110120
return SwapConnectors.BasicQuote(
111121
inType: self.asset,

cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc

Lines changed: 150 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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

cadence/tests/UniswapV3SwapConnectors_test.cdc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import BlockchainHelpers
33
import "test_helpers.cdc"
44

55
import "FlowToken"
6+
import "FlowEVMBridgeUtils"
7+
import "UniswapV3SwapConnectors"
68

79
access(all) let serviceAccount = Test.serviceAccount()
810

@@ -66,3 +68,68 @@ access(all)
6668
fun testSetupSucceeds() {
6769
log("UniswapV3SwapConnectors deployment success")
6870
}
71+
72+
/* Rounding tests */
73+
74+
access(all) fun roundTrip(_ x: UFix64, decimals: UInt8): UInt256 {
75+
return FlowEVMBridgeUtils.ufix64ToUInt256(value: x, decimals: decimals)
76+
}
77+
78+
access(all) fun quantum(decimals: UInt8): UInt256 {
79+
if decimals <= 8 { return UInt256(1) }
80+
return FlowEVMBridgeUtils.pow(base: 10, exponent: decimals - 8)
81+
}
82+
83+
access(all) fun test_decimals_le_8_exact_roundtrip_in_and_out() {
84+
// decimals 6: every unit is representable
85+
let decimals: UInt8 = 6
86+
let amt: UInt256 = UInt256(123_456_789) // 123.456789 with 6 decimals
87+
88+
let uIn = UniswapV3SwapConnectors.toCadenceInWithDecimals(amt, decimals: decimals)
89+
let uOut = UniswapV3SwapConnectors.toCadenceOutWithDecimals(amt, decimals: decimals)
90+
91+
assert(roundTrip(uIn, decimals: decimals) == amt, message: "in: round-trip should equal original when decimals<=8")
92+
assert(roundTrip(uOut, decimals: decimals) == amt, message: "out: round-trip should equal original when decimals<=8")
93+
}
94+
95+
access(all) fun test_decimals_gt_8_out_is_floor_to_quantum() {
96+
// decimals 18 => quantum = 10^(18-8) = 10^10
97+
let decimals: UInt8 = 18
98+
let q = quantum(decimals: decimals)
99+
100+
// choose an amt that's not divisible by q
101+
let amt: UInt256 = UInt256(1000) * q + UInt256(123) // remainder 123
102+
103+
let uOut = UniswapV3SwapConnectors.toCadenceOutWithDecimals(amt, decimals: decimals)
104+
let back = roundTrip(uOut, decimals: decimals)
105+
106+
assert(back <= amt, message: "out: round-trip must be <= original (floor)")
107+
assert(amt - back < q, message: "out: should only drop by < quantum")
108+
assert(back == amt - (amt % q), message: "out: must floor to multiple of quantum")
109+
}
110+
111+
access(all) fun test_decimals_gt_8_in_is_ceil_to_quantum_minimal() {
112+
let decimals: UInt8 = 18
113+
let q = quantum(decimals: decimals)
114+
115+
// not divisible by q
116+
let amt: UInt256 = UInt256(1000) * q + UInt256(123)
117+
118+
let uIn = UniswapV3SwapConnectors.toCadenceInWithDecimals(amt, decimals: decimals)
119+
let back = roundTrip(uIn, decimals: decimals)
120+
121+
assert(back >= amt, message: "in: round-trip must be >= original (ceil)")
122+
assert(back - amt < q, message: "in: should only increase by < quantum")
123+
assert(back == amt + (q - (amt % q)), message: "in: must ceil to next multiple of quantum")
124+
}
125+
126+
access(all) fun test_decimals_gt_8_in_exact_if_already_multiple_of_quantum() {
127+
let decimals: UInt8 = 18
128+
let q = quantum(decimals: decimals)
129+
130+
let amt: UInt256 = UInt256(1000) * q // exact multiple
131+
let uIn = UniswapV3SwapConnectors.toCadenceInWithDecimals(amt, decimals: decimals)
132+
let back = roundTrip(uIn, decimals: decimals)
133+
134+
assert(back == amt, message: "in: if already quantum-multiple, must not change")
135+
}

0 commit comments

Comments
 (0)