Smart Contract

FlowYieldVaultsStrategiesV2

A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2

Valid From

143,267,112

Deployed

4d ago
Feb 24, 2026, 01:06:44 PM UTC

Dependents

1 imports
1// standards
2import FungibleToken from 0xf233dcee88fe0abe
3import EVM from 0xe467b9dd11fa00df
4// DeFiActions
5import DeFiActionsUtils from 0x6d888f175c158410
6import DeFiActions from 0x6d888f175c158410
7import SwapConnectors from 0xe1a479f0cb911df9
8import FungibleTokenConnectors from 0x0c237e1265caa7a3
9// amm integration
10import UniswapV3SwapConnectors from 0xa7825d405ac89518
11import ERC4626SwapConnectors from 0x04f5ae6bef48c1fc
12import MorphoERC4626SwapConnectors from 0x251032a66e9700ef
13import ERC4626Utils from 0x04f5ae6bef48c1fc
14// Lending protocol
15import FlowALPv0 from 0x6b00ff876c299c61
16// FlowYieldVaults platform
17import FlowYieldVaults from 0xb1d63873c3cc9f79
18import FlowYieldVaultsAutoBalancers from 0xb1d63873c3cc9f79
19// scheduler
20import FlowTransactionScheduler from 0xe467b9dd11fa00df
21// tokens
22import MOET from 0x6b00ff876c299c61
23// vm bridge
24import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
25// live oracles
26import ERC4626PriceOracles from 0x04f5ae6bef48c1fc
27
28/// FlowYieldVaultsStrategiesV2
29///
30/// This contract defines Strategies used in the FlowYieldVaults platform.
31///
32/// A Strategy instance can be thought of as objects wrapping a stack of DeFiActions connectors wired together to
33/// (optimally) generate some yield on initial deposits. Strategies can be simple such as swapping into a yield-bearing
34/// asset (such as stFLOW) or more complex DeFiActions stacks.
35///
36/// A StrategyComposer is tasked with the creation of a supported Strategy. It's within the stacking of DeFiActions
37/// connectors that the true power of the components lies.
38///
39access(all) contract FlowYieldVaultsStrategiesV2 {
40
41    access(all) let univ3FactoryEVMAddress: EVM.EVMAddress
42    access(all) let univ3RouterEVMAddress: EVM.EVMAddress
43    access(all) let univ3QuoterEVMAddress: EVM.EVMAddress
44
45    access(all) let config: {String: AnyStruct} 
46
47    /// Canonical StoragePath where the StrategyComposerIssuer should be stored
48    access(all) let IssuerStoragePath: StoragePath
49
50    access(all) struct CollateralConfig {
51        access(all) let yieldTokenEVMAddress: EVM.EVMAddress
52        access(all) let yieldToCollateralUniV3AddressPath: [EVM.EVMAddress]
53        access(all) let yieldToCollateralUniV3FeePath: [UInt32]
54
55        init(
56            yieldTokenEVMAddress: EVM.EVMAddress,
57            yieldToCollateralUniV3AddressPath: [EVM.EVMAddress],
58            yieldToCollateralUniV3FeePath: [UInt32]
59        ) {
60            pre {
61                yieldToCollateralUniV3AddressPath.length > 1:
62                    "Invalid UniV3 path length"
63                yieldToCollateralUniV3FeePath.length == yieldToCollateralUniV3AddressPath.length - 1:
64                    "Invalid UniV3 fee path length"
65                yieldToCollateralUniV3AddressPath[0].equals(yieldTokenEVMAddress):
66                    "UniV3 path must start with yield token"
67            }
68
69            self.yieldTokenEVMAddress = yieldTokenEVMAddress
70            self.yieldToCollateralUniV3AddressPath = yieldToCollateralUniV3AddressPath
71            self.yieldToCollateralUniV3FeePath = yieldToCollateralUniV3FeePath
72        }
73    }
74
75    /// This strategy uses FUSDEV vault
76    access(all) resource FUSDEVStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource {
77        /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
78        /// specific Identifier to associated connectors on construction
79        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
80        access(self) let position: @FlowALPv0.Position
81        access(self) var sink: {DeFiActions.Sink}
82        access(self) var source: {DeFiActions.Source}
83
84        init(id: DeFiActions.UniqueIdentifier, collateralType: Type, position: @FlowALPv0.Position) {
85            self.uniqueID = id
86            self.sink = position.createSink(type: collateralType)
87            self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true)
88            self.position <-position
89        }
90
91        // Inherited from FlowYieldVaults.Strategy default implementation
92        // access(all) view fun isSupportedCollateralType(_ type: Type): Bool
93
94        access(all) view fun getSupportedCollateralTypes(): {Type: Bool} {
95            return { self.sink.getSinkType(): true }
96        }
97        /// Returns the amount available for withdrawal via the inner Source
98        access(all) fun availableBalance(ofToken: Type): UFix64 {
99            return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0
100        }
101        /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference
102        access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
103            self.sink.depositCapacity(from: from)
104        }
105        /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported,
106        /// an empty Vault is returned.
107        access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} {
108            if ofToken != self.source.getSourceType() {
109                return <- DeFiActionsUtils.getEmptyVault(ofToken)
110            }
111            return <- self.source.withdrawAvailable(maxAmount: maxAmount)
112        }
113        /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
114        access(contract) fun burnCallback() {
115            FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
116        }
117        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
118            return DeFiActions.ComponentInfo(
119                type: self.getType(),
120                id: self.id(),
121                innerComponents: [
122                    self.sink.getComponentInfo(),
123                    self.source.getComponentInfo()
124                ]
125            )
126        }
127        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
128            return self.uniqueID
129        }
130        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
131            self.uniqueID = id
132        }
133    }
134
135    access(all) struct TokenBundle {
136        access(all) let moetTokenType: Type
137        access(all) let moetTokenEVMAddress: EVM.EVMAddress
138
139        access(all) let yieldTokenType: Type
140        access(all) let yieldTokenEVMAddress: EVM.EVMAddress
141
142        access(all) let underlying4626AssetType: Type
143        access(all) let underlying4626AssetEVMAddress: EVM.EVMAddress
144
145        init(
146            moetTokenType: Type,
147            moetTokenEVMAddress: EVM.EVMAddress,
148            yieldTokenType: Type,
149            yieldTokenEVMAddress: EVM.EVMAddress,
150            underlying4626AssetType: Type,
151            underlying4626AssetEVMAddress: EVM.EVMAddress
152        ) {
153            self.moetTokenType = moetTokenType
154            self.moetTokenEVMAddress = moetTokenEVMAddress
155            self.yieldTokenType = yieldTokenType
156            self.yieldTokenEVMAddress = yieldTokenEVMAddress
157            self.underlying4626AssetType = underlying4626AssetType
158            self.underlying4626AssetEVMAddress = underlying4626AssetEVMAddress
159        }
160    }
161
162    /// Returned bundle for stored AutoBalancer interactions (reference + caps)
163    access(all) struct AutoBalancerIO {
164        access(all) let autoBalancer:
165            auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw)
166            &DeFiActions.AutoBalancer
167
168        access(all) let sink: {DeFiActions.Sink}
169        access(all) let source: {DeFiActions.Source}
170
171        init(
172            autoBalancer: auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) &DeFiActions.AutoBalancer,
173            sink: {DeFiActions.Sink},
174            source: {DeFiActions.Source}
175        ) {
176            self.sink = sink
177            self.source = source
178            self.autoBalancer = autoBalancer
179        }
180    }
181
182    /// This StrategyComposer builds a Strategy that uses MorphoERC4626 vault
183    access(all) resource MorphoERC4626StrategyComposer : FlowYieldVaults.StrategyComposer {
184        /// { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } }
185        access(self) let config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}
186
187        init(_ config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}) {
188            self.config = config
189        }
190
191        /// Returns the Types of Strategies composed by this StrategyComposer
192        access(all) view fun getComposedStrategyTypes(): {Type: Bool} {
193            let composed: {Type: Bool} = {}
194            for t in self.config.keys {
195                composed[t] = true
196            }
197            return composed
198        }
199
200        /// Returns the Vault types which can be used to initialize a given Strategy
201        access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} {
202            let supported: {Type: Bool} = {}
203            if let strategyConfig = &self.config[forStrategy] as &{Type: FlowYieldVaultsStrategiesV2.CollateralConfig}? {
204                for collateralType in strategyConfig.keys {
205                    supported[collateralType] = true
206                }
207            }
208            return supported
209        }
210
211        access(self) view fun _supportsCollateral(forStrategy: Type, collateral: Type): Bool {
212            if let strategyConfig = self.config[forStrategy] {
213                return strategyConfig[collateral] != nil
214            }
215            return false
216        }
217
218        /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the
219        /// provided Vault type
220        access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} {
221            return self._supportsCollateral(forStrategy: forStrategy, collateral: initializedWith)
222                ? { initializedWith: true }
223                : {}
224        }
225
226        /// Composes a Strategy of the given type with the provided funds
227        access(all) fun createStrategy(
228            _ type: Type,
229            uniqueID: DeFiActions.UniqueIdentifier,
230            withFunds: @{FungibleToken.Vault}
231        ): @{FlowYieldVaults.Strategy} {
232            pre {
233                self.config[type] != nil: "Unsupported strategy type \(type.identifier)"
234            }
235            let collateralType = withFunds.getType()
236
237            let collateralConfig = self._getCollateralConfig(
238                strategyType: type,
239                collateralType: collateralType
240            )
241
242            let tokens = self._resolveTokenBundle(collateralConfig: collateralConfig)
243
244            // Oracle used by AutoBalancer (tracks NAV of ERC4626 vault)
245            let yieldTokenOracle = self._createYieldTokenOracle(
246                yieldTokenEVMAddress: tokens.yieldTokenEVMAddress,
247                underlyingAssetType: tokens.underlying4626AssetType,
248                uniqueID: uniqueID
249            )
250
251            // Create recurring config for automatic rebalancing
252            let recurringConfig = FlowYieldVaultsStrategiesV2._createRecurringConfig(withID: uniqueID)
253
254            // Create/store/publish/register AutoBalancer (returns authorized ref)
255            let balancerIO = self._initAutoBalancerAndIO(
256                oracle: yieldTokenOracle,
257                yieldTokenType: tokens.yieldTokenType,
258                recurringConfig: recurringConfig,
259                uniqueID: uniqueID
260            )
261
262            // Swappers: MOET <-> YIELD (YIELD is ERC4626 vault token)
263            let moetToYieldSwapper = self._createMoetToYieldSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID)
264
265            let yieldToMoetSwapper = self._createYieldToMoetSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID)
266
267            // AutoBalancer-directed swap IO
268            let abaSwapSink = SwapConnectors.SwapSink(
269                swapper: moetToYieldSwapper,
270                sink: balancerIO.sink,
271                uniqueID: uniqueID
272            )
273            let abaSwapSource = SwapConnectors.SwapSource(
274                swapper: yieldToMoetSwapper,
275                source: balancerIO.source,
276                uniqueID: uniqueID
277            )
278
279            // Open FlowALPv0 position
280            let position <- self._openCreditPosition(
281                funds: <-withFunds,
282                issuanceSink: abaSwapSink,
283                repaymentSource: abaSwapSource
284            )
285
286            // Position Sink/Source (only Sink needed here, Source stays inside Strategy impl)
287            let positionSink = position.createSinkWithOptions(type: collateralType, pushToDrawDownSink: true)
288
289            // Yield -> Collateral swapper for recollateralization
290            let yieldToCollateralSwapper = self._createYieldToCollateralSwapper(
291                collateralConfig: collateralConfig,
292                yieldTokenEVMAddress: tokens.yieldTokenEVMAddress,
293                yieldTokenType: tokens.yieldTokenType,
294                collateralType: collateralType,
295                uniqueID: uniqueID
296            )
297
298            let positionSwapSink = SwapConnectors.SwapSink(
299                swapper: yieldToCollateralSwapper,
300                sink: positionSink,
301                uniqueID: uniqueID
302            )
303
304            // Set AutoBalancer sink for overflow -> recollateralize
305            balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true)
306
307            switch type {
308            case Type<@FUSDEVStrategy>():
309                return <-create FUSDEVStrategy(
310                    id: uniqueID,
311                    collateralType: collateralType,
312                    position: <-position
313                )
314            default:
315                panic("Unsupported strategy type \(type.identifier)")
316            }
317        }
318
319        /* ===========================
320           Helpers
321           =========================== */
322
323        access(self) fun _getCollateralConfig(
324            strategyType: Type,
325            collateralType: Type
326        ): FlowYieldVaultsStrategiesV2.CollateralConfig {
327            let strategyConfig = self.config[strategyType]
328                ?? panic(
329                    "Could not find a config for Strategy \(strategyType.identifier) initialized with \(collateralType.identifier)"
330                )
331
332            return strategyConfig[collateralType]
333                ?? panic(
334                    "Could not find config for collateral \(collateralType.identifier) when creating Strategy \(strategyType.identifier)"
335                )
336        }
337
338        access(self) fun _resolveTokenBundle(
339            collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig
340        ): FlowYieldVaultsStrategiesV2.TokenBundle {
341            // MOET
342            let moetTokenType = Type<@MOET.Vault>()
343            let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType)
344                ?? panic("Token Vault type \(moetTokenType.identifier) has not yet been registered with the VMbridge")
345
346            // YIELD (ERC4626 vault token)
347            let yieldTokenEVMAddress = collateralConfig.yieldTokenEVMAddress
348            let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress)
349                ?? panic(
350                    "Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())"
351                )
352
353            // UNDERLYING asset of the ERC4626 vault
354            let underlying4626AssetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: yieldTokenEVMAddress)
355                ?? panic(
356                    "Could not get the underlying asset's EVM address for ERC4626Vault \(yieldTokenEVMAddress.toString())"
357                )
358            let underlying4626AssetType = FlowEVMBridgeConfig.getTypeAssociated(with: underlying4626AssetEVMAddress)
359                ?? panic(
360                    "Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())"
361                )
362
363            return FlowYieldVaultsStrategiesV2.TokenBundle(
364                moetTokenType: moetTokenType,
365                moetTokenEVMAddress: moetTokenEVMAddress,
366                yieldTokenType: yieldTokenType,
367                yieldTokenEVMAddress: yieldTokenEVMAddress,
368                underlying4626AssetType: underlying4626AssetType,
369                underlying4626AssetEVMAddress: underlying4626AssetEVMAddress
370            )
371        }
372
373        access(self) fun _createYieldTokenOracle(
374            yieldTokenEVMAddress: EVM.EVMAddress,
375            underlyingAssetType: Type,
376            uniqueID: DeFiActions.UniqueIdentifier
377        ): ERC4626PriceOracles.PriceOracle {
378            return ERC4626PriceOracles.PriceOracle(
379                vault: yieldTokenEVMAddress,
380                asset: underlyingAssetType,
381                uniqueID: uniqueID
382            )
383        }
384
385        access(self) fun _createUniV3Swapper(
386            tokenPath: [EVM.EVMAddress],
387            feePath: [UInt32],
388            inVault: Type,
389            outVault: Type,
390            uniqueID: DeFiActions.UniqueIdentifier
391        ): UniswapV3SwapConnectors.Swapper {
392            return UniswapV3SwapConnectors.Swapper(
393                factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress,
394                routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress,
395                quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress,
396                tokenPath: tokenPath,
397                feePath: feePath,
398                inVault: inVault,
399                outVault: outVault,
400                coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(),
401                uniqueID: uniqueID
402            )
403        }
404
405        access(self) fun _createMoetToYieldSwapper(
406            strategyType: Type,
407            tokens: FlowYieldVaultsStrategiesV2.TokenBundle,
408            uniqueID: DeFiActions.UniqueIdentifier
409        ): SwapConnectors.MultiSwapper {
410            // Direct MOET -> YIELD via AMM
411            let moetToYieldAMM = self._createUniV3Swapper(
412                tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress],
413                feePath: [100],
414                inVault: tokens.moetTokenType,
415                outVault: tokens.yieldTokenType,
416                uniqueID: uniqueID
417            )
418
419            // MOET -> UNDERLYING via AMM
420            let moetToUnderlying = self._createUniV3Swapper(
421                tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress],
422                feePath: [100],
423                inVault: tokens.moetTokenType,
424                outVault: tokens.underlying4626AssetType,
425                uniqueID: uniqueID
426            )
427
428            // UNDERLYING -> YIELD via ERC4626 vault
429            // Morpho vaults use MorphoERC4626SwapConnectors; standard ERC4626 vaults use ERC4626SwapConnectors
430            var underlyingTo4626: {DeFiActions.Swapper}? = nil
431            if strategyType == Type<@FUSDEVStrategy>() {
432                underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper(
433                    vaultEVMAddress: tokens.yieldTokenEVMAddress,
434                    coa: FlowYieldVaultsStrategiesV2._getCOACapability(),
435                    feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID),
436                    uniqueID: uniqueID,
437                    isReversed: false
438                )
439            } else {
440                underlyingTo4626 = ERC4626SwapConnectors.Swapper(
441                    asset: tokens.underlying4626AssetType,
442                    vault: tokens.yieldTokenEVMAddress,
443                    coa: FlowYieldVaultsStrategiesV2._getCOACapability(),
444                    feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID),
445                    uniqueID: uniqueID
446                )
447            }
448
449            let seq = SwapConnectors.SequentialSwapper(
450                swappers: [moetToUnderlying, underlyingTo4626!],
451                uniqueID: uniqueID
452            )
453
454            return SwapConnectors.MultiSwapper(
455                inVault: tokens.moetTokenType,
456                outVault: tokens.yieldTokenType,
457                swappers: [moetToYieldAMM, seq],
458                uniqueID: uniqueID
459            )
460        }
461
462        access(self) fun _createYieldToMoetSwapper(
463            strategyType: Type,
464            tokens: FlowYieldVaultsStrategiesV2.TokenBundle,
465            uniqueID: DeFiActions.UniqueIdentifier
466        ): SwapConnectors.MultiSwapper {
467            // Direct YIELD -> MOET via AMM
468            let yieldToMoetAMM = self._createUniV3Swapper(
469                tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress],
470                feePath: [100],
471                inVault: tokens.yieldTokenType,
472                outVault: tokens.moetTokenType,
473                uniqueID: uniqueID
474            )
475
476            // Reverse path: Morpho vaults support direct redeem; standard ERC4626 vaults use AMM-only path
477            if strategyType == Type<@FUSDEVStrategy>() {
478                // YIELD -> UNDERLYING redeem via MorphoERC4626 vault
479                let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper(
480                    vaultEVMAddress: tokens.yieldTokenEVMAddress,
481                    coa: FlowYieldVaultsStrategiesV2._getCOACapability(),
482                    feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID),
483                    uniqueID: uniqueID,
484                    isReversed: true
485                )
486                // UNDERLYING -> MOET via AMM
487                let underlyingToMoet = self._createUniV3Swapper(
488                    tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress],
489                    feePath: [100],
490                    inVault: tokens.underlying4626AssetType,
491                    outVault: tokens.moetTokenType,
492                    uniqueID: uniqueID
493                )
494
495                let seq = SwapConnectors.SequentialSwapper(
496                    swappers: [yieldToUnderlying, underlyingToMoet],
497                    uniqueID: uniqueID
498                )
499
500                return SwapConnectors.MultiSwapper(
501                    inVault: tokens.yieldTokenType,
502                    outVault: tokens.moetTokenType,
503                    swappers: [yieldToMoetAMM, seq],
504                    uniqueID: uniqueID
505                )
506            } else {
507                // Standard ERC4626: AMM-only reverse (no synchronous redeem support)
508                return SwapConnectors.MultiSwapper(
509                    inVault: tokens.yieldTokenType,
510                    outVault: tokens.moetTokenType,
511                    swappers: [yieldToMoetAMM],
512                    uniqueID: uniqueID
513                )
514            }
515        }
516
517        access(self) fun _initAutoBalancerAndIO(
518            oracle: {DeFiActions.PriceOracle},
519            yieldTokenType: Type,
520            recurringConfig: DeFiActions.AutoBalancerRecurringConfig?,
521            uniqueID: DeFiActions.UniqueIdentifier
522        ): FlowYieldVaultsStrategiesV2.AutoBalancerIO {
523            // NOTE: This stores the AutoBalancer in FlowYieldVaultsAutoBalancers storage and returns an authorized ref.
524            let autoBalancerRef =
525                FlowYieldVaultsAutoBalancers._initNewAutoBalancer(
526                    oracle: oracle,
527                    vaultType: yieldTokenType,
528                    lowerThreshold: 0.95,
529                    upperThreshold: 1.05,
530                    rebalanceSink: nil,
531                    rebalanceSource: nil,
532                    recurringConfig: recurringConfig,
533                    uniqueID: uniqueID
534                )
535
536            let sink = autoBalancerRef.createBalancerSink()
537                ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)")
538            let source = autoBalancerRef.createBalancerSource()
539                ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)")
540
541            return FlowYieldVaultsStrategiesV2.AutoBalancerIO(
542                autoBalancer: autoBalancerRef,
543                sink: sink,
544                source: source
545            )
546        }
547
548        access(self) fun _openCreditPosition(
549            funds: @{FungibleToken.Vault},
550            issuanceSink: {DeFiActions.Sink},
551            repaymentSource: {DeFiActions.Source}
552        ): @FlowALPv0.Position {
553            let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy<
554                Capability<auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool>
555            >(from: FlowALPv0.PoolCapStoragePath)
556                ?? panic("Missing or invalid pool capability")
557
558            let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap")
559
560            let position <- poolRef.createPosition(
561                funds: <-funds,
562                issuanceSink: issuanceSink,
563                repaymentSource: repaymentSource,
564                pushToDrawDownSink: true
565            )
566
567            return <-position
568        }
569
570        access(self) fun _createYieldToCollateralSwapper(
571            collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig,
572            yieldTokenEVMAddress: EVM.EVMAddress,
573            yieldTokenType: Type,
574            collateralType: Type,
575            uniqueID: DeFiActions.UniqueIdentifier
576        ): UniswapV3SwapConnectors.Swapper {
577            // CollateralConfig.init already validates:
578            // - path length > 1
579            // - fee length == path length - 1
580            // - path[0] == yield token
581            //
582            // Keep a defensive check in case configs were migrated / constructed elsewhere.
583            let tokenPath = collateralConfig.yieldToCollateralUniV3AddressPath
584            assert(
585                tokenPath[0].equals(yieldTokenEVMAddress),
586                message:
587                    "Config mismatch: expected yield token \(yieldTokenEVMAddress.toString()) but got \(tokenPath[0].toString())"
588            )
589
590            return self._createUniV3Swapper(
591                tokenPath: tokenPath,
592                feePath: collateralConfig.yieldToCollateralUniV3FeePath,
593                inVault: yieldTokenType,
594                outVault: collateralType,
595                uniqueID: uniqueID
596            )
597        }
598    }
599
600    access(all) entitlement Configure
601
602    access(self)
603    fun makeCollateralConfig(
604        yieldTokenEVMAddress: EVM.EVMAddress,
605        yieldToCollateralAddressPath: [EVM.EVMAddress],
606        yieldToCollateralFeePath: [UInt32]
607    ): CollateralConfig {
608        pre {
609            yieldToCollateralAddressPath.length > 1:
610                "Invalid Uniswap V3 swap path length"
611            yieldToCollateralFeePath.length == yieldToCollateralAddressPath.length - 1:
612                "Uniswap V3 fee path length must be path length - 1"
613            yieldToCollateralAddressPath[0].equals(yieldTokenEVMAddress):
614                "UniswapV3 swap path must start with yield token"
615        }
616
617        return CollateralConfig(
618            yieldTokenEVMAddress:  yieldTokenEVMAddress,
619            yieldToCollateralUniV3AddressPath: yieldToCollateralAddressPath,
620            yieldToCollateralUniV3FeePath: yieldToCollateralFeePath
621        )
622    }
623    /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which
624    /// may utilize resource consumption (i.e. account storage). Since Strategy creation consumes account storage
625    /// via configured AutoBalancers
626    access(all) resource StrategyComposerIssuer : FlowYieldVaults.StrategyComposerIssuer {
627        /// { StrategyComposer Type: { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } }
628        access(all) var configs: {Type: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}}
629
630        init(configs: {Type: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}}) {
631            self.configs = configs
632        }
633
634        access(all) view fun hasConfig(
635            composer: Type,
636            strategy: Type,
637            collateral: Type
638        ): Bool {
639            if let composerConfig = self.configs[composer] {
640                if let strategyConfig = composerConfig[strategy] {
641                    return strategyConfig[collateral] != nil
642                }
643            }
644            return false
645        }
646
647        access(all) view fun getSupportedComposers(): {Type: Bool} {
648            return { 
649                Type<@MorphoERC4626StrategyComposer>(): true
650            }
651        }
652
653        access(self) view fun isSupportedComposer(_ type: Type): Bool {
654            return type == Type<@MorphoERC4626StrategyComposer>()
655        }
656        access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} {
657            pre {
658                self.isSupportedComposer(type) == true:
659                "Unsupported StrategyComposer \(type.identifier) requested"
660                self.configs[type] != nil:
661                "Could not find config for StrategyComposer \(type.identifier)"
662            }
663            switch type {
664            case Type<@MorphoERC4626StrategyComposer>():
665                return <- create MorphoERC4626StrategyComposer(self.configs[type]!)
666            default:
667                panic("Unsupported StrategyComposer \(type.identifier) requested")
668            }
669        }
670
671        access(Configure)
672        fun upsertConfigFor(
673            composer: Type,
674            config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}
675        ) {
676            pre {
677                self.isSupportedComposer(composer) == true:
678                    "Unsupported StrategyComposer Type \(composer.identifier)"
679            }
680
681            // Validate keys
682            for stratType in config.keys {
683                assert(stratType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()),
684                    message: "Invalid config key \(stratType.identifier) - not a FlowYieldVaults.Strategy Type")
685                for collateralType in config[stratType]!.keys {
686                    assert(collateralType.isSubtype(of: Type<@{FungibleToken.Vault}>()),
687                        message: "Invalid config key at config[\(stratType.identifier)] - \(collateralType.identifier) is not a FungibleToken.Vault")
688                }
689            }
690
691            // Merge instead of overwrite
692            let existingComposerConfig = self.configs[composer] ?? {}
693            var mergedComposerConfig = existingComposerConfig
694
695            for stratType in config.keys {
696                let newPerCollateral = config[stratType]!
697                let existingPerCollateral = mergedComposerConfig[stratType] ?? {}
698                var mergedPerCollateral: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} = existingPerCollateral
699
700                for collateralType in newPerCollateral.keys {
701                    mergedPerCollateral[collateralType] = newPerCollateral[collateralType]!
702                }
703                mergedComposerConfig[stratType] = mergedPerCollateral
704            }
705
706            self.configs[composer] = mergedComposerConfig
707        }
708
709        access(Configure) fun addOrUpdateCollateralConfig(
710            composer: Type,
711            strategyType: Type,
712            collateralVaultType: Type,
713            yieldTokenEVMAddress: EVM.EVMAddress,
714            yieldToCollateralAddressPath: [EVM.EVMAddress],
715            yieldToCollateralFeePath: [UInt32]
716        ) {
717            pre {
718                self.isSupportedComposer(composer) == true:
719                    "Unsupported StrategyComposer Type \(composer.identifier)"
720                strategyType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()):
721                    "Strategy type \(strategyType.identifier) is not a FlowYieldVaults.Strategy"
722                collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()):
723                    "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault"
724            }
725
726            // Base struct with shared addresses
727            var base = FlowYieldVaultsStrategiesV2.makeCollateralConfig(
728                yieldTokenEVMAddress: yieldTokenEVMAddress,
729                yieldToCollateralAddressPath: yieldToCollateralAddressPath,
730                yieldToCollateralFeePath: yieldToCollateralFeePath
731            )
732
733            // Wrap into the nested config expected by upsertConfigFor
734            let singleCollateralConfig = {
735                strategyType: {
736                    collateralVaultType: base
737                }
738            }
739
740            self.upsertConfigFor(composer: composer, config: singleCollateralConfig)
741        }
742        access(Configure) fun purgeConfig() {
743            self.configs = {
744                Type<@MorphoERC4626StrategyComposer>(): {
745                    Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}
746                }
747            }
748        }
749    }
750
751    /// Returns the COA capability for this account
752    /// TODO: this is temporary until we have a better way to pass user's COAs to inner connectors
753    access(self)
754    fun _getCOACapability(): Capability<auth(EVM.Call, EVM.Bridge, EVM.Owner) &EVM.CadenceOwnedAccount> {
755        let coaCap = self.account.capabilities.storage.issue<auth(EVM.Call, EVM.Bridge, EVM.Owner) &EVM.CadenceOwnedAccount>(/storage/evm)
756        assert(coaCap.check(), message: "Could not issue COA capability")
757        return coaCap
758    }
759
760    /// Returns a FungibleTokenConnectors.VaultSinkAndSource used to subsidize cross VM token movement in contract-
761    /// defined strategies.
762    access(self)
763    fun _createFeeSource(withID: DeFiActions.UniqueIdentifier?): {DeFiActions.Sink, DeFiActions.Source} {
764        let capPath = /storage/strategiesFeeSource
765        if self.account.storage.type(at: capPath) == nil {
766            let cap = self.account.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault)
767            self.account.storage.save(cap, to: capPath)
768        }
769        let vaultCap = self.account.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>>(from: capPath)
770            ?? panic("Could not find fee source Capability at \(capPath)")
771        return FungibleTokenConnectors.VaultSinkAndSource(
772            min: nil,
773            max: nil,
774            vault: vaultCap,
775            uniqueID: withID
776        )
777    }
778
779    /// Creates an AutoBalancerRecurringConfig for scheduled rebalancing.
780    /// The txnFunder uses the contract's FlowToken vault to pay for scheduling fees.
781    access(self)
782    fun _createRecurringConfig(withID: DeFiActions.UniqueIdentifier?): DeFiActions.AutoBalancerRecurringConfig {
783        // Create txnFunder that can provide/accept FLOW for scheduling fees
784        let txnFunder = self._createTxnFunder(withID: withID)
785        
786        return DeFiActions.AutoBalancerRecurringConfig(
787            interval: 60 * 10,  // Rebalance every 10 minutes
788            priority: FlowTransactionScheduler.Priority.Medium,
789            executionEffort: 800,
790            forceRebalance: false,
791            txnFunder: txnFunder
792        )
793    }
794
795    /// Creates a Sink+Source for the AutoBalancer to use for scheduling fees
796    access(self)
797    fun _createTxnFunder(withID: DeFiActions.UniqueIdentifier?): {DeFiActions.Sink, DeFiActions.Source} {
798        let capPath = /storage/autoBalancerTxnFunder
799        if self.account.storage.type(at: capPath) == nil {
800            let cap = self.account.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault)
801            self.account.storage.save(cap, to: capPath)
802        }
803        let vaultCap = self.account.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>>(from: capPath)
804            ?? panic("Could not find txnFunder Capability at \(capPath)")
805        return FungibleTokenConnectors.VaultSinkAndSource(
806            min: nil,
807            max: nil,
808            vault: vaultCap,
809            uniqueID: withID
810        )
811    }
812
813    init(
814        univ3FactoryEVMAddress: String,
815        univ3RouterEVMAddress: String,
816        univ3QuoterEVMAddress: String,
817    ) {
818        self.univ3FactoryEVMAddress = EVM.addressFromString(univ3FactoryEVMAddress)
819        self.univ3RouterEVMAddress = EVM.addressFromString(univ3RouterEVMAddress)
820        self.univ3QuoterEVMAddress = EVM.addressFromString(univ3QuoterEVMAddress)
821        self.IssuerStoragePath = StoragePath(identifier: "FlowYieldVaultsStrategyV2ComposerIssuer_\(self.account.address)")!
822        self.config = {}
823
824        let moetType = Type<@MOET.Vault>()
825        if FlowEVMBridgeConfig.getEVMAddressAssociated(with: Type<@MOET.Vault>()) == nil {
826            panic("Could not find EVM address for \(moetType.identifier) - ensure the asset is onboarded to the VM Bridge")
827        }
828
829        let configs = {
830                Type<@MorphoERC4626StrategyComposer>(): {
831                    Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}
832                }
833            }
834        self.account.storage.save(<-create StrategyComposerIssuer(configs: configs), to: self.IssuerStoragePath)
835
836        // TODO: this is temporary until we have a better way to pass user's COAs to inner connectors
837        // create a COA in this account
838        if self.account.storage.type(at: /storage/evm) == nil {
839            self.account.storage.save(<-EVM.createCadenceOwnedAccount(), to: /storage/evm)
840            let cap = self.account.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(/storage/evm)
841            self.account.capabilities.publish(cap, at: /public/evm)
842        }
843    }
844}
845