Smart Contract

FlowYieldVaultsStrategiesV1_1

A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV1_1

Valid From

139,767,556

Deployed

1w ago
Feb 16, 2026, 01:08:54 AM UTC

Dependents

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