@@ -3,6 +3,7 @@ import "FungibleToken"
33import " EVM"
44import " FlowEVMBridgeConfig"
55import " FlowEVMBridgeUtils"
6+ import " FlowToken"
67import " DeFiActions"
78import " DeFiActionsUtils"
89import " ERC4626SinkConnectors"
@@ -28,9 +29,10 @@ access(all) contract ERC4626SwapConnectors {
2829 /// for liquidity to flow between Cadnece & EVM. These "swaps" are performed by depositing the input asset into the
2930 /// ERC4626 vault and withdrawing the resulting shares from the ERC4626 vault.
3031 ///
31- /// NOTE: Since ERC4626 vaults typically do not support synchronous withdrawals, this Swapper only supports the
32- /// default inType -> outType path via swap() and reverts on swapBack() since the withdrawal cannot be returned
33- /// synchronously.
32+ /// NOTE: Since ERC4626 vaults typically do not support synchronous withdrawals, this is a one-way swapper that
33+ /// only supports the default inType -> outType path via swap() and reverts on swapBack(). When used with
34+ /// SwapConnectors.SwapSink, ensure the inner sink always consumes all swapped shares (has sufficient capacity).
35+ /// If residuals remain, swapBack() will be called and the transaction will revert.
3436 ///
3537 access (all ) struct Swapper : DeFiActions .Swapper {
3638 /// The asset type serving as the price basis in the ERC4626 vault
@@ -60,10 +62,20 @@ access(all) contract ERC4626SwapConnectors {
6062 " Provided asset \( asset .identifier ) is not a Vault type"
6163 coa .check ():
6264 " Provided COA Capability is invalid - need Capability<&EVM.CadenceOwnedAccount>"
65+
66+ feeSource .getSourceType () == Type <@FlowToken.Vault >():
67+ " Invalid feeSource - given Source must provide FlowToken Vault, but provides \( feeSource .getSourceType ().identifier ) "
6368 }
6469 self .asset = asset
6570 self .assetEVMAddress = FlowEVMBridgeConfig .getEVMAddressAssociated (with : asset )
6671 ?? panic (" Provided asset \( asset .identifier ) is not associated with ERC20 - ensure the type & ERC20 contracts are associated via the VM bridge" )
72+
73+ let actualUnderlyingAddress = ERC4626Utils .underlyingAssetEVMAddress (vault : vault )
74+ assert (
75+ actualUnderlyingAddress ?. equals (self .assetEVMAddress ) ?? false ,
76+ message : " Provided asset \( asset .identifier ) does not underly ERC4626 vault \( vault .toString ()) - found \( actualUnderlyingAddress ?. toString () ?? " nil" ) but expected \( self .assetEVMAddress .toString ()) "
77+ )
78+
6779 self .vault = vault
6880 self .vaultType = FlowEVMBridgeConfig .getTypeAssociated (with : vault )
6981 ?? panic (" Provided ERC4626 Vault \( vault .toString ()) is not associated with a Cadence FungibleToken - ensure the type & ERC4626 contracts are associated via the VM bridge" )
@@ -104,18 +116,53 @@ access(all) contract ERC4626SwapConnectors {
104116 outAmount : 0.0
105117 )
106118 }
107- let uintForDesired = FlowEVMBridgeUtils .convertCadenceAmountToERC20Amount (forDesired , erc20Address : self .vault )
119+
120+ // Check maximum deposit capacity first
121+ let maxCapacity = self .assetSink .minimumCapacity ()
122+ if maxCapacity > 0.0 {
123+ let uintForDesired = FlowEVMBridgeUtils .convertCadenceAmountToERC20Amount (forDesired , erc20Address : self .vault )
124+ if let uintRequired = ERC4626Utils .previewMint (vault : self .vault , shares : uintForDesired ) {
125+ let uintMaxAllowed = FlowEVMBridgeUtils .convertCadenceAmountToERC20Amount (UFix64.max , erc20Address : self .assetEVMAddress )
108126
109- if let uintRequired = ERC4626Utils .previewMint (vault : self .vault , shares : uintForDesired ) {
110- let maxAvailableRequired = FlowEVMBridgeUtils .convertCadenceAmountToERC20Amount (UFix64.max , erc20Address : self .assetEVMAddress )
111- let safeUIntRequired = uintRequired < maxAvailableRequired ? uintRequired : maxAvailableRequired
112- let ufixRequired = FlowEVMBridgeUtils .convertERC20AmountToCadenceAmount (safeUIntRequired , erc20Address : self .assetEVMAddress )
113- return SwapConnectors .BasicQuote (
114- inType : self .asset ,
115- outType : self .vaultType ,
116- inAmount : ufixRequired ,
117- outAmount : forDesired
118- )
127+ if uintRequired > uintMaxAllowed {
128+ return SwapConnectors .BasicQuote (
129+ inType : self .asset ,
130+ outType : self .vaultType ,
131+ inAmount : UFix64.max ,
132+ outAmount : forDesired
133+ )
134+ }
135+
136+ let ufixRequired = FlowEVMBridgeUtils .convertERC20AmountToCadenceAmount (uintRequired , erc20Address : self .assetEVMAddress )
137+
138+ // Cap input to maxCapacity and recalculate output if needed
139+ if ufixRequired > maxCapacity {
140+ // Required assets exceed capacity - cap at maxCapacity and calculate achievable shares
141+ let uintMaxCapacity = FlowEVMBridgeUtils .convertCadenceAmountToERC20Amount (maxCapacity , erc20Address : self .assetEVMAddress )
142+ if let uintActualShares = ERC4626Utils .previewDeposit (vault : self .vault , assets : uintMaxCapacity ) {
143+ let ufixActualShares = FlowEVMBridgeUtils .convertERC20AmountToCadenceAmount (uintActualShares , erc20Address : self .vault )
144+ return SwapConnectors .BasicQuote (
145+ inType : self .asset ,
146+ outType : self .vaultType ,
147+ inAmount : maxCapacity ,
148+ outAmount : ufixActualShares
149+ )
150+ }
151+ return SwapConnectors .BasicQuote (
152+ inType : self .asset ,
153+ outType : self .vaultType ,
154+ inAmount : 0.0 ,
155+ outAmount : 0.0
156+ )
157+ }
158+
159+ return SwapConnectors .BasicQuote (
160+ inType : self .asset ,
161+ outType : self .vaultType ,
162+ inAmount : ufixRequired ,
163+ outAmount : forDesired
164+ )
165+ }
119166 }
120167 return SwapConnectors .BasicQuote (
121168 inType : self .asset ,
@@ -172,21 +219,62 @@ access(all) contract ERC4626SwapConnectors {
172219
173220 // assign or get the quote for the swap
174221 let _quote = quote ?? self .quoteOut (forProvided : inVault .balance , reverse : false )
222+ let outAmount = _quote .outAmount
223+
224+ assert (_quote .inType == self .inType (), message : " Quote inType mismatch" )
225+ assert (_quote .outType == self .outType (), message : " Quote outType mismatch" )
226+ assert (_quote .inAmount > 0.0 , message : " Invalid quote: inAmount must be > 0" )
227+ assert (outAmount > 0.0 , message : " Invalid quote: outAmount must be > 0" )
228+
229+ // --- Slippage protection: don't allow spending more than quoted ---
230+ let beforeInBalance = inVault .balance
231+ assert (
232+ beforeInBalance < = _quote .inAmount ,
233+ message : " Swap input (\( beforeInBalance ) ) exceeds quote.inAmount (\( _quote .inAmount ) ). Provide an updated quote or reduce inVault balance."
234+ )
175235
176- // get the before available shares
236+ // Track shares available before/after to determine received shares
177237 let beforeAvailable = self .shareSource .minimumAvailable ()
178238
179- // deposit the inVault into the asset sink
239+ // Deposit the inVault into the asset sink (should consume all of it)
180240 self .assetSink .depositCapacity (from : &inVault as auth (FungibleToken.Withdraw ) &{FungibleToken .Vault })
241+
242+ let remainder = inVault .balance
243+ let consumedIn = beforeInBalance - remainder
244+
245+ // We expect full consumption in this connector's semantics.
246+ // If this ever becomes "partial fill" in the future, this check + price check below
247+ // ensures it still can't be worse than quoted.
248+ assert (
249+ consumedIn > 0.0 ,
250+ message : " Asset sink did not consume any input."
251+ )
252+ assert (remainder == 0.0 , message : " Asset sink did not consume full input; remainder: \( remainder .toString ()) . Adjust inVault balance." )
253+
181254 Burner .burn (<- inVault )
182255
183256 // get the after available shares
184257 let afterAvailable = self .shareSource .minimumAvailable ()
185258 assert (afterAvailable > beforeAvailable , message : " Expected ERC4626 Vault \( self .vault .toString ()) to have more shares after depositing" )
186259
187260 // withdraw the available difference in shares
188- let availableDiff = afterAvailable - beforeAvailable
189- let sharesVault <- self .shareSource .withdrawAvailable (maxAmount : availableDiff )
261+ let receivedShares = afterAvailable - beforeAvailable
262+
263+ // --- Slippage protection: ensure minimum out ---
264+ assert (
265+ receivedShares > = outAmount ,
266+ message : " Slippage: received \( receivedShares ) < quote.outAmount (\( outAmount ) )."
267+ )
268+
269+ let sharesVault <- self .shareSource .withdrawAvailable (maxAmount : receivedShares )
270+
271+ // Extra safety: ensure the vault we’re returning matches the computed delta
272+ // (withdrawAvailable could theoretically return less if liquidity changed)
273+ assert (
274+ sharesVault .balance > = outAmount ,
275+ message : " Slippage: withdrawn shares \( sharesVault .balance ) < outAmount (\( outAmount ) )."
276+ )
277+
190278 return <- sharesVault
191279 }
192280 /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
0 commit comments