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