Smart Contract
FlowYieldVaultsStrategiesV2
A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2
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