@@ -42,7 +42,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
4242 access (all ) let univ3RouterEVMAddress : EVM .EVMAddress
4343 access (all ) let univ3QuoterEVMAddress : EVM .EVMAddress
4444
45- access (all ) let config : {String : AnyStruct }
45+ access (contract ) let config : {String : AnyStruct }
4646
4747 /// Canonical StoragePath where the StrategyComposerIssuer should be stored
4848 access (all ) let IssuerStoragePath : StoragePath
@@ -72,19 +72,32 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
7272 }
7373 }
7474
75- /// This strategy uses FUSDEV vault
75+ /// This strategy uses FUSDEV vault (Morpho ERC4626).
76+ /// Deposits collateral into a single FlowALP position, borrowing MOET as debt.
77+ /// MOET is swapped to PYUSD0 and deposited into the Morpho FUSDEV ERC4626 vault.
78+ /// Each strategy instance holds exactly one collateral type and one debt type (MOET).
79+ /// PYUSD0 (the FUSDEV vault's underlying asset) cannot be used as collateral.
7680 access (all ) resource FUSDEVStrategy : FlowYieldVaults .Strategy , DeFiActions .IdentifiableResource {
7781 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
7882 /// specific Identifier to associated connectors on construction
7983 access (contract ) var uniqueID : DeFiActions .UniqueIdentifier ?
8084 access (self ) let position : @FlowALPv0.Position
8185 access (self ) var sink : {DeFiActions .Sink }
8286 access (self ) var source : {DeFiActions .Source }
87+ /// Tracks whether the underlying FlowALP position has been closed. Once true,
88+ /// availableBalance() returns 0.0 to avoid panicking when the pool no longer
89+ /// holds the position (e.g. during YieldVault burnCallback after close).
90+ access (self ) var positionClosed : Bool
8391
84- init (id : DeFiActions .UniqueIdentifier , collateralType : Type , position : @FlowALPv0.Position ) {
92+ init (
93+ id : DeFiActions .UniqueIdentifier ,
94+ collateralType : Type ,
95+ position : @FlowALPv0.Position
96+ ) {
8597 self .uniqueID = id
8698 self .sink = position .createSink (type : collateralType )
8799 self .source = position .createSourceWithOptions (type : collateralType , pullFromTopUpSource : true )
100+ self .positionClosed = false
88101 self .position <- position
89102 }
90103
@@ -96,10 +109,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
96109 }
97110 /// Returns the amount available for withdrawal via the inner Source
98111 access (all ) fun availableBalance (ofToken : Type ): UFix64 {
112+ if self .positionClosed { return 0.0 }
99113 return ofToken == self .source .getSourceType () ? self .source .minimumAvailable () : 0.0
100114 }
101- /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference
115+ /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference.
116+ /// Only the single configured collateral type is accepted — one collateral type per position.
102117 access (all ) fun deposit (from : auth (FungibleToken.Withdraw ) &{FungibleToken .Vault }) {
118+ pre {
119+ from .getType () == self .sink .getSinkType ():
120+ " FUSDEVStrategy position only accepts \( self .sink .getSinkType ().identifier ) as collateral, got \( from .getType ().identifier ) "
121+ }
103122 self .sink .depositCapacity (from : from )
104123 }
105124 /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported,
@@ -110,6 +129,118 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
110129 }
111130 return <- self .source .withdrawAvailable (maxAmount : maxAmount )
112131 }
132+ /// Closes the underlying FlowALP position by preparing repayment funds and closing with them.
133+ ///
134+ /// This method:
135+ /// 1. Calculates debt amount from position
136+ /// 2. Creates external yield token source from AutoBalancer
137+ /// 3. Swaps yield tokens → MOET via stored swapper
138+ /// 4. Closes position with prepared MOET vault
139+ ///
140+ /// This approach eliminates circular dependencies by preparing all funds externally
141+ /// before calling the position's close method.
142+ ///
143+ access (FungibleToken.Withdraw ) fun closePosition (collateralType : Type ): @{FungibleToken .Vault } {
144+ pre {
145+ self .isSupportedCollateralType (collateralType ):
146+ " Unsupported collateral type \( collateralType .identifier ) "
147+ }
148+ post {
149+ result .getType () == collateralType : " Withdraw Vault (\( result .getType ().identifier ) ) is not of a requested collateral type (\( collateralType .identifier ) )"
150+ }
151+
152+ // Step 1: Get debt amounts - returns {Type: UFix64} dictionary
153+ let debtsByType = self .position .getTotalDebt ()
154+
155+ // Enforce: one debt type per position
156+ assert (
157+ debtsByType .length < = 1 ,
158+ message : " FUSDEVStrategy position must have at most one debt type, found \( debtsByType .length ) "
159+ )
160+
161+ // Step 2: Calculate total debt amount
162+ var totalDebtAmount : UFix64 = 0.0
163+ for debtAmount in debtsByType .values {
164+ totalDebtAmount = totalDebtAmount + debtAmount
165+ }
166+
167+ // Step 3: If no debt, close with empty sources array
168+ if totalDebtAmount == 0.0 {
169+ let resultVaults <- self .position .closePosition (
170+ repaymentSources : []
171+ )
172+ // With one collateral type and no debt the pool returns at most one vault.
173+ // Zero vaults is possible when the collateral balance is dust that rounds down
174+ // to zero (e.g. drawDownSink had no capacity, or token reserves were empty).
175+ assert (
176+ resultVaults .length < = 1 ,
177+ message : " Expected 0 or 1 collateral vault from closePosition, got \( resultVaults .length ) "
178+ )
179+ // Zero vaults: dust collateral rounded down to zero — return an empty vault
180+ if resultVaults .length == 0 {
181+ destroy resultVaults
182+ self .positionClosed = true
183+ return <- DeFiActionsUtils .getEmptyVault (collateralType )
184+ }
185+ let collateralVault <- resultVaults .removeFirst ()
186+ destroy resultVaults
187+ self .positionClosed = true
188+ return <- collateralVault
189+ }
190+
191+ // Step 4: Create external yield token source from AutoBalancer
192+ let yieldTokenSource = FlowYieldVaultsAutoBalancers .createExternalSource (id : self .id ()! )
193+ ?? panic (" Could not create external source from AutoBalancer" )
194+
195+ // Step 5: Retrieve yield→MOET swapper from contract config
196+ let swapperKey = FlowYieldVaultsStrategiesV2 .getYieldToMoetSwapperConfigKey (self .uniqueID )!
197+ let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2 .config [swapperKey ] as ! {DeFiActions .Swapper }?
198+ ?? panic (" No yield→MOET swapper found for strategy \( self .id ()! ) " )
199+
200+ // Step 6: Create a SwapSource that converts yield tokens to MOET when pulled by closePosition.
201+ // The pool will call source.withdrawAvailable(maxAmount: debtAmount) which internally uses
202+ // quoteIn(forDesired: debtAmount) to compute the exact yield token input needed.
203+ let moetSource = SwapConnectors .SwapSource (
204+ swapper : yieldToMoetSwapper ,
205+ source : yieldTokenSource ,
206+ uniqueID : self .copyID ()
207+ )
208+
209+ // Step 7: Close position - pool pulls exactly the debt amount from moetSource
210+ let resultVaults <- self .position .closePosition (repaymentSources : [moetSource ])
211+
212+ // With one collateral type and one debt type, the pool returns at most two vaults:
213+ // the collateral vault and optionally a MOET overpayment dust vault.
214+ assert (
215+ resultVaults .length > = 1 && resultVaults .length < = 2 ,
216+ message : " Expected 1 or 2 vaults from closePosition, got \( resultVaults .length ) "
217+ )
218+
219+ var collateralVault <- resultVaults .removeFirst ()
220+ assert (
221+ collateralVault .getType () == collateralType ,
222+ message : " First vault returned from closePosition must be collateral (\( collateralType .identifier ) ), got \( collateralVault .getType ().identifier ) "
223+ )
224+
225+ // Handle any overpayment dust (MOET) returned as the second vault
226+ while resultVaults .length > 0 {
227+ let dustVault <- resultVaults .removeFirst ()
228+ if dustVault .balance > 0.0 {
229+ if dustVault .getType () == collateralType {
230+ collateralVault .deposit (from : <- dustVault )
231+ } else {
232+ // @TODO implement swapping moet to collateral
233+ destroy dustVault
234+ }
235+ } else {
236+ destroy dustVault
237+ }
238+ }
239+
240+ destroy resultVaults
241+ self .positionClosed = true
242+ return <- collateralVault
243+ }
113244 /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
114245 access (contract ) fun burnCallback () {
115246 FlowYieldVaultsAutoBalancers ._cleanupAutoBalancer (id : self .id ()! )
@@ -301,9 +432,46 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
301432 uniqueID : uniqueID
302433 )
303434
435+ // pullFromTopUpSource: false ensures Position maintains health buffer
436+ // This prevents Position from being pushed to minHealth (1.1) limit
437+ let positionSource = position .createSourceWithOptions (
438+ type : collateralType ,
439+ pullFromTopUpSource : false // ← CONSERVATIVE: maintain safety buffer
440+ )
441+
442+ // Create Collateral -> Yield swapper (reverse of yieldToCollateralSwapper)
443+ // Allows AutoBalancer to pull collateral, swap to yield token
444+ let collateralToYieldSwapper = self ._createCollateralToYieldSwapper (
445+ collateralConfig : collateralConfig ,
446+ yieldTokenEVMAddress : tokens .yieldTokenEVMAddress ,
447+ yieldTokenType : tokens .yieldTokenType ,
448+ collateralType : collateralType ,
449+ uniqueID : uniqueID
450+ )
451+
452+ // Create Position swap source for AutoBalancer deficit recovery
453+ // When AutoBalancer value drops below deposits, pulls collateral from Position
454+ let positionSwapSource = SwapConnectors .SwapSource (
455+ swapper : collateralToYieldSwapper ,
456+ source : positionSource ,
457+ uniqueID : uniqueID
458+ )
459+
304460 // Set AutoBalancer sink for overflow -> recollateralize
305461 balancerIO .autoBalancer .setSink (positionSwapSink , updateSinkID : true )
306462
463+ // Set AutoBalancer source for deficit recovery -> pull from Position
464+ balancerIO .autoBalancer .setSource (positionSwapSource , updateSourceID : true )
465+
466+ // Store yield→MOET swapper in contract config for later access during closePosition
467+ let yieldToMoetSwapperKey = FlowYieldVaultsStrategiesV2 .getYieldToMoetSwapperConfigKey (uniqueID )!
468+ FlowYieldVaultsStrategiesV2 .config [yieldToMoetSwapperKey ] = yieldToMoetSwapper
469+
470+ // @TODO implement moet to collateral swapper
471+ // let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(uniqueID)
472+ //
473+ // FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] = moetToCollateralSwapper
474+ //
307475 switch type {
308476 case Type <@FUSDEVStrategy >():
309477 return <- create FUSDEVStrategy (
@@ -514,6 +682,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
514682 }
515683 }
516684
685+ /// @TODO
686+ /// implement moet to collateral swapper
687+ // access(self) fun _createMoetToCollateralSwapper(
688+ // strategyType: Type,
689+ // tokens: FlowYieldVaultsStrategiesV2.TokenBundle,
690+ // uniqueID: DeFiActions.UniqueIdentifier
691+ // ): SwapConnectors.MultiSwapper {
692+ // // Direct MOET -> underlying via AMM
693+ // }
694+
517695 access (self ) fun _initAutoBalancerAndIO (
518696 oracle : {DeFiActions .PriceOracle },
519697 yieldTokenType : Type ,
@@ -595,6 +773,40 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
595773 uniqueID : uniqueID
596774 )
597775 }
776+
777+ /// Creates a Collateral -> Yield token swapper using UniswapV3
778+ /// This is the REVERSE of _createYieldToCollateralSwapper
779+ /// Used by AutoBalancer to pull collateral from Position and swap to yield tokens
780+ ///
781+ access (self ) fun _createCollateralToYieldSwapper (
782+ collateralConfig : FlowYieldVaultsStrategiesV2 .CollateralConfig ,
783+ yieldTokenEVMAddress : EVM .EVMAddress ,
784+ yieldTokenType : Type ,
785+ collateralType : Type ,
786+ uniqueID : DeFiActions .UniqueIdentifier
787+ ): UniswapV3SwapConnectors .Swapper {
788+ // Reverse the swap path: collateral -> yield (opposite of yield -> collateral)
789+ let forwardPath = collateralConfig .yieldToCollateralUniV3AddressPath
790+ let reversedTokenPath = forwardPath .reverse ()
791+
792+ // Reverse the fee path as well
793+ let forwardFees = collateralConfig .yieldToCollateralUniV3FeePath
794+ let reversedFeePath = forwardFees .reverse ()
795+
796+ // Verify the reversed path starts with collateral (ends with yield)
797+ assert (
798+ reversedTokenPath [reversedTokenPath .length - 1 ].equals (yieldTokenEVMAddress ),
799+ message : " Reversed path must end with yield token \( yieldTokenEVMAddress .toString ()) "
800+ )
801+
802+ return self ._createUniV3Swapper (
803+ tokenPath : reversedTokenPath ,
804+ feePath : reversedFeePath ,
805+ inVault : collateralType , // ← Input is collateral
806+ outVault : yieldTokenType , // ← Output is yield token
807+ uniqueID : uniqueID
808+ )
809+ }
598810 }
599811
600812 access (all ) entitlement Configure
@@ -810,6 +1022,20 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
8101022 )
8111023 }
8121024
1025+ access (self ) view fun getYieldToMoetSwapperConfigKey (_ uniqueID : DeFiActions .UniqueIdentifier ? ): String {
1026+ pre {
1027+ uniqueID ! = nil : " Missing UniqueIdentifier for swapper config key"
1028+ }
1029+ return " yieldToMoetSwapper_\( uniqueID ! .id .toString ()) "
1030+ }
1031+
1032+ access (self ) view fun getMoetToCollateralSwapperConfigKey (_ uniqueID : DeFiActions .UniqueIdentifier ? ): String {
1033+ pre {
1034+ uniqueID ! = nil : " Missing UniqueIdentifier for swapper config key"
1035+ }
1036+ return " moetToCollateralSwapper_\( uniqueID ! .id .toString ()) "
1037+ }
1038+
8131039 init (
8141040 univ3FactoryEVMAddress : String ,
8151041 univ3RouterEVMAddress : String ,
0 commit comments