Smart Contract

PMStrategiesV1

A.b1d63873c3cc9f79.PMStrategiesV1

Valid From

142,129,002

Deployed

2w ago
Feb 13, 2026, 10:45:48 PM UTC

Dependents

4 imports
1// standards
2import FungibleToken from 0xf233dcee88fe0abe
3import EVM from 0xe467b9dd11fa00df
4// DeFiActions
5import DeFiActionsUtils from 0x6d888f175c158410
6import DeFiActions from 0x6d888f175c158410
7import SwapConnectors from 0xe1a479f0cb911df9
8import FungibleTokenConnectors from 0x0c237e1265caa7a3
9// amm integration
10import UniswapV3SwapConnectors from 0xa7825d405ac89518
11import ERC4626SwapConnectors from 0x04f5ae6bef48c1fc
12import MorphoERC4626SwapConnectors from 0x251032a66e9700ef
13import ERC4626Utils from 0x04f5ae6bef48c1fc
14// FlowYieldVaults platform
15import FlowYieldVaults from 0xb1d63873c3cc9f79
16import FlowYieldVaultsAutoBalancers from 0xb1d63873c3cc9f79
17// vm bridge
18import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
19// live oracles
20import ERC4626PriceOracles from 0x04f5ae6bef48c1fc
21
22/// PMStrategiesV1
23///
24/// This contract defines Strategies used in the FlowYieldVaults platform.
25///
26/// A Strategy instance can be thought of as objects wrapping a stack of DeFiActions connectors wired together to
27/// (optimally) generate some yield on initial deposits. Strategies can be simple such as swapping into a yield-bearing
28/// asset (such as stFLOW) or more complex DeFiActions stacks.
29///
30/// A StrategyComposer is tasked with the creation of a supported Strategy. It's within the stacking of DeFiActions
31/// connectors that the true power of the components lies.
32///
33access(all) contract PMStrategiesV1 {
34
35    access(all) let univ3FactoryEVMAddress: EVM.EVMAddress
36    access(all) let univ3RouterEVMAddress: EVM.EVMAddress
37    access(all) let univ3QuoterEVMAddress: EVM.EVMAddress
38
39    /// Canonical StoragePath where the StrategyComposerIssuer should be stored
40    access(all) let IssuerStoragePath: StoragePath
41
42    /// Contract-level config for extensibility (not yet used)
43    access(self) let config: {String: {String: AnyStruct}}
44
45    /// This strategy uses syWFLOWv vaults
46    access(all) resource syWFLOWvStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource {
47        /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
48        /// specific Identifier to associated connectors on construction
49        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
50
51        /// User-facing deposit connector
52        access(self) var sink: {DeFiActions.Sink}
53        /// User-facing withdrawal connector
54        access(self) var source: {DeFiActions.Source}
55
56        init(
57            id: DeFiActions.UniqueIdentifier,
58            sink: {DeFiActions.Sink},
59            source: {DeFiActions.Source}
60        ) {
61            self.uniqueID = id
62            self.sink = sink
63            self.source = source
64        }
65
66        // Inherited from FlowYieldVaults.Strategy default implementation
67        // access(all) view fun isSupportedCollateralType(_ type: Type): Bool
68
69        access(all) view fun getSupportedCollateralTypes(): {Type: Bool} {
70            return { self.sink.getSinkType(): true }
71        }
72        /// Returns the amount available for withdrawal via the inner Source
73        access(all) fun availableBalance(ofToken: Type): UFix64 {
74            return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0
75        }
76        /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference
77        access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
78            self.sink.depositCapacity(from: from)
79        }
80        /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported,
81        /// an empty Vault is returned.
82        access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} {
83            if ofToken != self.source.getSourceType() {
84                return <- DeFiActionsUtils.getEmptyVault(ofToken)
85            }
86            return <- self.source.withdrawAvailable(maxAmount: maxAmount)
87        }
88        /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
89        access(contract) fun burnCallback() {
90            FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
91        }
92        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
93            return DeFiActions.ComponentInfo(
94                type: self.getType(),
95                id: self.id(),
96                innerComponents: [
97                    self.sink.getComponentInfo(),
98                    self.source.getComponentInfo()
99                ]
100            )
101        }
102        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
103            return self.uniqueID
104        }
105        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
106            self.uniqueID = id
107        }
108    }
109
110    /// This strategy uses tauUSDF vaults (Tau Labs USDF Vault)
111    access(all) resource tauUSDFvStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource {
112        /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
113        /// specific Identifier to associated connectors on construction
114        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
115
116        /// User-facing deposit connector
117        access(self) var sink: {DeFiActions.Sink}
118        /// User-facing withdrawal connector
119        access(self) var source: {DeFiActions.Source}
120
121        init(
122            id: DeFiActions.UniqueIdentifier,
123            sink: {DeFiActions.Sink},
124            source: {DeFiActions.Source}
125        ) {
126            self.uniqueID = id
127            self.sink = sink
128            self.source = source
129        }
130
131        // Inherited from FlowYieldVaults.Strategy default implementation
132        // access(all) view fun isSupportedCollateralType(_ type: Type): Bool
133
134        access(all) view fun getSupportedCollateralTypes(): {Type: Bool} {
135            return { self.sink.getSinkType(): true }
136        }
137        /// Returns the amount available for withdrawal via the inner Source
138        access(all) fun availableBalance(ofToken: Type): UFix64 {
139            return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0
140        }
141        /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference
142        access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
143            self.sink.depositCapacity(from: from)
144        }
145        /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported,
146        /// an empty Vault is returned.
147        access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} {
148            if ofToken != self.source.getSourceType() {
149                return <- DeFiActionsUtils.getEmptyVault(ofToken)
150            }
151            return <- self.source.withdrawAvailable(maxAmount: maxAmount)
152        }
153        /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
154        access(contract) fun burnCallback() {
155            FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
156        }
157        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
158            return DeFiActions.ComponentInfo(
159                type: self.getType(),
160                id: self.id(),
161                innerComponents: [
162                    self.sink.getComponentInfo(),
163                    self.source.getComponentInfo()
164                ]
165            )
166        }
167        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
168            return self.uniqueID
169        }
170        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
171            self.uniqueID = id
172        }
173    }
174
175    /// This strategy uses FUSDEV vaults (Flow USD Expeditionary Vault)
176    access(all) resource FUSDEVStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource {
177        /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
178        /// specific Identifier to associated connectors on construction
179        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
180
181        /// User-facing deposit connector
182        access(self) var sink: {DeFiActions.Sink}
183        /// User-facing withdrawal connector
184        access(self) var source: {DeFiActions.Source}
185
186        init(
187            id: DeFiActions.UniqueIdentifier,
188            sink: {DeFiActions.Sink},
189            source: {DeFiActions.Source}
190        ) {
191            self.uniqueID = id
192            self.sink = sink
193            self.source = source
194        }
195
196        // Inherited from FlowYieldVaults.Strategy default implementation
197        // access(all) view fun isSupportedCollateralType(_ type: Type): Bool
198
199        access(all) view fun getSupportedCollateralTypes(): {Type: Bool} {
200            return { self.sink.getSinkType(): true }
201        }
202        /// Returns the amount available for withdrawal via the inner Source
203        access(all) fun availableBalance(ofToken: Type): UFix64 {
204            return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0
205        }
206        /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference
207        access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
208            self.sink.depositCapacity(from: from)
209        }
210        /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported,
211        /// an empty Vault is returned.
212        access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} {
213            if ofToken != self.source.getSourceType() {
214                return <- DeFiActionsUtils.getEmptyVault(ofToken)
215            }
216            return <- self.source.withdrawAvailable(maxAmount: maxAmount)
217        }
218        /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
219        access(contract) fun burnCallback() {
220            FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
221        }
222        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
223            return DeFiActions.ComponentInfo(
224                type: self.getType(),
225                id: self.id(),
226                innerComponents: [
227                    self.sink.getComponentInfo(),
228                    self.source.getComponentInfo()
229                ]
230            )
231        }
232        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
233            return self.uniqueID
234        }
235        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
236            self.uniqueID = id
237        }
238    }
239
240    /// StrategyComposer for ERC4626 vault strategies (e.g., syWFLOWvStrategy, tauUSDFvStrategy, FUSDEVStrategy).
241    access(all) resource ERC4626VaultStrategyComposer : FlowYieldVaults.StrategyComposer {
242        /// { Strategy Type: { Collateral Type: { String: AnyStruct } } }
243        access(self) let config: {Type: {Type: {String: AnyStruct}}}
244
245        init(_ config: {Type: {Type: {String: AnyStruct}}}) {
246            self.config = config
247        }
248
249        access(all) view fun getComposedStrategyTypes(): {Type: Bool} {
250            let composed: {Type: Bool} = {}
251            for t in self.config.keys {
252                composed[t] = true
253            }
254            return composed
255        }
256
257        access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} {
258            let strategyConfig = self.config[forStrategy]
259            if strategyConfig == nil {
260                return {}
261            }
262            // Return all supported collateral types from config
263            let supported: {Type: Bool} = {}
264            for collateralType in strategyConfig!.keys {
265                supported[collateralType] = true
266            }
267            return supported
268        }
269
270        /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the
271        /// provided Vault type
272        access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} {
273            let supportedInitVaults = self.getSupportedInitializationVaults(forStrategy: forStrategy)
274            if supportedInitVaults[initializedWith] == true {
275                return { initializedWith: true }
276            }
277            return {}
278        }
279
280        /// Composes a Strategy of the given type with the provided funds
281        access(all) fun createStrategy(
282            _ type: Type,
283            uniqueID: DeFiActions.UniqueIdentifier,
284            withFunds: @{FungibleToken.Vault}
285        ): @{FlowYieldVaults.Strategy} {
286            let collateralType = withFunds.getType()
287            let strategyConfig = self.config[type]
288                ?? panic("Could not find a config for Strategy \(type.identifier)")
289            let collateralConfig = strategyConfig[collateralType]
290                ?? panic("Could not find config for collateral \(collateralType.identifier) when creating Strategy \(type.identifier)")
291
292            // Get config values
293            let yieldTokenEVMAddress = collateralConfig["yieldTokenEVMAddress"] as? EVM.EVMAddress 
294                ?? panic("Could not find \"yieldTokenEVMAddress\" in config")
295            let swapFeeTier = collateralConfig["swapFeeTier"] as? UInt32 
296                ?? panic("Could not find \"swapFeeTier\" in config")
297
298            // Get underlying asset EVM address from the deposited funds type
299            let underlyingAssetEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: collateralType)
300                ?? panic("Could not get EVM address for collateral type \(collateralType.identifier)")
301
302            // assign yield token type from the tauUSDF ERC4626 vault address
303            let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress)
304                ?? panic("Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())")
305
306            // create the oracle for the assets to be held in the AutoBalancer retrieving the NAV of the 4626 vault
307            let yieldTokenOracle = ERC4626PriceOracles.PriceOracle(
308                    vault: yieldTokenEVMAddress,
309                    asset: collateralType,
310                    uniqueID: uniqueID
311                )
312
313            // configure and AutoBalancer for this stack
314            let autoBalancer = FlowYieldVaultsAutoBalancers._initNewAutoBalancer(
315                    oracle: yieldTokenOracle,       // used to determine value of deposits & when to rebalance
316                    vaultType: yieldTokenType,      // the type of Vault held by the AutoBalancer
317                    lowerThreshold: 0.95,           // set AutoBalancer to pull from rebalanceSource when balance is 5% below value of deposits
318                    upperThreshold: 1.05,           // set AutoBalancer to push to rebalanceSink when balance is 5% below value of deposits
319                    rebalanceSink: nil,             // nil on init - will be set once a PositionSink is available
320                    rebalanceSource: nil,           // nil on init - not set for Strategy
321                    recurringConfig: nil,           // disables native AutoBalancer self-scheduling, no rebalancing required after init
322                    uniqueID: uniqueID              // identifies AutoBalancer as part of this Strategy
323                )
324            // enables deposits of YieldToken to the AutoBalancer
325            let abaSink = autoBalancer.createBalancerSink() ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)")
326            // enables withdrawals of YieldToken from the AutoBalancer
327            let abaSource = autoBalancer.createBalancerSource() ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)")
328
329            // create Collateral <-> YieldToken swappers
330            //
331            // Collateral -> YieldToken - can swap via two primary routes:
332            // - via AMM swap pairing Collateral <-> YieldToken
333            // - via ERC4626 vault deposit
334            // Collateral -> YieldToken high-level Swapper contains:
335            //     - MultiSwapper aggregates across two sub-swappers
336            //         - Collateral -> YieldToken (UniV3 Swapper)
337            //         - Collateral -> YieldToken (ERC4626 Swapper)
338            let collateralToYieldAMMSwapper = UniswapV3SwapConnectors.Swapper(
339                    factoryAddress: PMStrategiesV1.univ3FactoryEVMAddress,
340                    routerAddress: PMStrategiesV1.univ3RouterEVMAddress,
341                    quoterAddress: PMStrategiesV1.univ3QuoterEVMAddress,
342                    tokenPath: [underlyingAssetEVMAddress, yieldTokenEVMAddress],
343                    feePath: [swapFeeTier],
344                    inVault: collateralType,
345                    outVault: yieldTokenType,
346                    coaCapability: PMStrategiesV1._getCOACapability(),
347                    uniqueID: uniqueID
348                )
349            // Swap Collateral -> YieldToken via ERC4626 Vault
350            // Morpho vaults use MorphoERC4626SwapConnectors; standard ERC4626 vaults use ERC4626SwapConnectors
351            var collateralToYieldSwapper: SwapConnectors.MultiSwapper? = nil
352            if type == Type<@FUSDEVStrategy>() {
353                let collateralToYieldMorphoERC4626Swapper = MorphoERC4626SwapConnectors.Swapper(
354                        vaultEVMAddress: yieldTokenEVMAddress,
355                        coa: PMStrategiesV1._getCOACapability(),
356                        feeSource: PMStrategiesV1._createFeeSource(withID: uniqueID),
357                        uniqueID: uniqueID,
358                        isReversed: false
359                    )
360                collateralToYieldSwapper = SwapConnectors.MultiSwapper(
361                        inVault: collateralType,
362                        outVault: yieldTokenType,
363                        swappers: [collateralToYieldAMMSwapper, collateralToYieldMorphoERC4626Swapper],
364                        uniqueID: uniqueID
365                    )
366            } else {
367                let collateralToYieldERC4626Swapper = ERC4626SwapConnectors.Swapper(
368                        asset: collateralType,
369                        vault: yieldTokenEVMAddress,
370                        coa: PMStrategiesV1._getCOACapability(),
371                        feeSource: PMStrategiesV1._createFeeSource(withID: uniqueID),
372                        uniqueID: uniqueID
373                    )
374                collateralToYieldSwapper = SwapConnectors.MultiSwapper(
375                        inVault: collateralType,
376                        outVault: yieldTokenType,
377                        swappers: [collateralToYieldAMMSwapper, collateralToYieldERC4626Swapper],
378                        uniqueID: uniqueID
379                    )
380            }
381
382            // create YieldToken <-> Collateral swappers
383            //
384            // YieldToken -> Collateral - can swap via two primary routes:
385            // - via AMM swap pairing YieldToken <-> Collateral
386            // - via ERC4626 vault deposit
387            // YieldToken -> Collateral high-level Swapper contains:
388            //     - MultiSwapper aggregates across two sub-swappers
389            //         - YieldToken -> Collateral (UniV3 Swapper)
390            //         - YieldToken -> Collateral (ERC4626 Swapper)
391            let yieldToCollateralAMMSwapper = UniswapV3SwapConnectors.Swapper(
392                    factoryAddress: PMStrategiesV1.univ3FactoryEVMAddress,
393                    routerAddress: PMStrategiesV1.univ3RouterEVMAddress,
394                    quoterAddress: PMStrategiesV1.univ3QuoterEVMAddress,
395                    tokenPath: [yieldTokenEVMAddress, underlyingAssetEVMAddress],
396                    feePath: [swapFeeTier],
397                    inVault: yieldTokenType,
398                    outVault: collateralType,
399                    coaCapability: PMStrategiesV1._getCOACapability(),
400                    uniqueID: uniqueID
401                )
402
403            // Reverse path: YieldToken -> Collateral
404            // Morpho vaults support direct redeem; standard ERC4626 vaults use AMM-only path
405            var yieldToCollateralSwapper: SwapConnectors.MultiSwapper? = nil
406            if type == Type<@FUSDEVStrategy>() {
407                let yieldToCollateralMorphoERC4626Swapper = MorphoERC4626SwapConnectors.Swapper(
408                        vaultEVMAddress: yieldTokenEVMAddress,
409                        coa: PMStrategiesV1._getCOACapability(),
410                        feeSource: PMStrategiesV1._createFeeSource(withID: uniqueID),
411                        uniqueID: uniqueID,
412                        isReversed: true
413                    )
414                yieldToCollateralSwapper = SwapConnectors.MultiSwapper(
415                        inVault: yieldTokenType,
416                        outVault: collateralType,
417                        swappers: [yieldToCollateralAMMSwapper, yieldToCollateralMorphoERC4626Swapper],
418                        uniqueID: uniqueID
419                    )
420            } else {
421                // Standard ERC4626: AMM-only reverse (no synchronous redeem support)
422                yieldToCollateralSwapper = SwapConnectors.MultiSwapper(
423                        inVault: yieldTokenType,
424                        outVault: collateralType,
425                        swappers: [yieldToCollateralAMMSwapper],
426                        uniqueID: uniqueID
427                    )
428            }
429
430            // init SwapSink directing swapped funds to AutoBalancer
431            //
432            // Swaps provided Collateral to YieldToken & deposits to the AutoBalancer
433            let abaSwapSink = SwapConnectors.SwapSink(swapper: collateralToYieldSwapper!, sink: abaSink, uniqueID: uniqueID)
434            // Swaps YieldToken & provides swapped Collateral, sourcing YieldToken from the AutoBalancer
435            let abaSwapSource = SwapConnectors.SwapSource(swapper: yieldToCollateralSwapper!, source: abaSource, uniqueID: uniqueID)
436
437            abaSwapSink.depositCapacity(from: &withFunds as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
438
439            assert(withFunds.balance == 0.0, message: "Vault should be empty after depositing")
440            destroy withFunds
441
442            // Use the same uniqueID passed to createStrategy so Strategy.burnCallback
443            // calls _cleanupAutoBalancer with the correct ID
444            switch type {
445            case Type<@syWFLOWvStrategy>():
446                return <-create syWFLOWvStrategy(id: uniqueID, sink: abaSwapSink, source: abaSwapSource)
447            case Type<@tauUSDFvStrategy>():
448                return <-create tauUSDFvStrategy(id: uniqueID, sink: abaSwapSink, source: abaSwapSource)
449            case Type<@FUSDEVStrategy>():
450                return <-create FUSDEVStrategy(id: uniqueID, sink: abaSwapSink, source: abaSwapSource)
451            default:
452                panic("Unsupported strategy type \(type.identifier)")
453            }
454        }
455    }
456
457    access(all) entitlement Configure
458
459    /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which
460    /// may utilize resource consumption (i.e. account storage). Since Strategy creation consumes account storage
461    /// via configured AutoBalancers
462    access(all) resource StrategyComposerIssuer : FlowYieldVaults.StrategyComposerIssuer {
463        /// { StrategyComposer Type: { Strategy Type: { Collateral Type: { String: AnyStruct } } } }
464        access(all) let configs: {Type: {Type: {Type: {String: AnyStruct}}}}
465
466        init(configs: {Type: {Type: {Type: {String: AnyStruct}}}}) {
467            self.configs = configs
468        }
469
470        access(all) view fun getSupportedComposers(): {Type: Bool} {
471            return { 
472                Type<@ERC4626VaultStrategyComposer>(): true
473            }
474        }
475        access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} {
476            pre {
477                self.getSupportedComposers()[type] == true:
478                "Unsupported StrategyComposer \(type.identifier) requested"
479                (&self.configs[type] as &{Type: {Type: {String: AnyStruct}}}?) != nil:
480                "Could not find config for StrategyComposer \(type.identifier)"
481            }
482            switch type {
483            case Type<@ERC4626VaultStrategyComposer>():
484                return <- create ERC4626VaultStrategyComposer(self.configs[type]!)
485            default:
486                panic("Unsupported StrategyComposer \(type.identifier) requested")
487            }
488        }
489        access(Configure) fun upsertConfigFor(composer: Type, config: {Type: {Type: {String: AnyStruct}}}) {
490            pre {
491                self.getSupportedComposers()[composer] == true:
492                "Unsupported StrategyComposer Type \(composer.identifier)"
493            }
494            // Validate keys
495            for stratType in config.keys {
496                assert(stratType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()),
497                    message: "Invalid config key \(stratType.identifier) - not a FlowYieldVaults.Strategy Type")
498                for collateralType in config[stratType]!.keys {
499                    assert(collateralType.isSubtype(of: Type<@{FungibleToken.Vault}>()),
500                        message: "Invalid config key at config[\(stratType.identifier)] - \(collateralType.identifier) is not a FungibleToken.Vault")
501                }
502            }
503            // Merge instead of overwrite
504            let existingComposerConfig = self.configs[composer] ?? {}
505            var mergedComposerConfig: {Type: {Type: {String: AnyStruct}}} = existingComposerConfig
506
507            for stratType in config.keys {
508                let newPerCollateral = config[stratType]!
509                let existingPerCollateral = mergedComposerConfig[stratType] ?? {}
510                var mergedPerCollateral: {Type: {String: AnyStruct}} = existingPerCollateral
511
512                for collateralType in newPerCollateral.keys {
513                    mergedPerCollateral[collateralType] = newPerCollateral[collateralType]!
514                }
515                mergedComposerConfig[stratType] = mergedPerCollateral
516            }
517
518            self.configs[composer] = mergedComposerConfig
519        }
520    }
521
522    /// Returns the COA capability for this account
523    /// TODO: this is temporary until we have a better way to pass user's COAs to inner connectors
524    access(self)
525    fun _getCOACapability(): Capability<auth(EVM.Call, EVM.Bridge, EVM.Owner) &EVM.CadenceOwnedAccount> {
526        let coaCap = self.account.capabilities.storage.issue<auth(EVM.Call, EVM.Bridge, EVM.Owner) &EVM.CadenceOwnedAccount>(/storage/evm)
527        assert(coaCap.check(), message: "Could not issue COA capability")
528        return coaCap
529    }
530
531    /// Returns a FungibleTokenConnectors.VaultSinkAndSource used to subsidize cross VM token movement in contract-
532    /// defined strategies.
533    access(self)
534    fun _createFeeSource(withID: DeFiActions.UniqueIdentifier?): {DeFiActions.Sink, DeFiActions.Source} {
535        let capPath = /storage/strategiesFeeSource
536        if self.account.storage.type(at: capPath) == nil {
537            let cap = self.account.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault)
538            self.account.storage.save(cap, to: capPath)
539        }
540        let vaultCap = self.account.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>>(from: capPath)
541            ?? panic("Could not find fee source Capability at \(capPath)")
542        return FungibleTokenConnectors.VaultSinkAndSource(
543            min: nil,
544            max: nil,
545            vault: vaultCap,
546            uniqueID: withID
547        )
548    }
549
550    init(
551        univ3FactoryEVMAddress: String,
552        univ3RouterEVMAddress: String,
553        univ3QuoterEVMAddress: String
554    ) {
555        self.univ3FactoryEVMAddress = EVM.addressFromString(univ3FactoryEVMAddress)
556        self.univ3RouterEVMAddress = EVM.addressFromString(univ3RouterEVMAddress)
557        self.univ3QuoterEVMAddress = EVM.addressFromString(univ3QuoterEVMAddress)
558        self.IssuerStoragePath = StoragePath(identifier: "PMStrategiesV1ComposerIssuer_\(self.account.address)")!
559        self.config = {}
560
561        // Start with empty configs - strategy configs are added via upsertConfigFor admin transactions
562        let configs: {Type: {Type: {Type: {String: AnyStruct}}}} = {
563                Type<@ERC4626VaultStrategyComposer>(): {}
564            }
565        self.account.storage.save(<-create StrategyComposerIssuer(configs: configs), to: self.IssuerStoragePath)
566
567        // TODO: this is temporary until we have a better way to pass user's COAs to inner connectors
568        // create a COA in this account
569        if self.account.storage.type(at: /storage/evm) == nil {
570            self.account.storage.save(<-EVM.createCadenceOwnedAccount(), to: /storage/evm)
571            let cap = self.account.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(/storage/evm)
572            self.account.capabilities.publish(cap, at: /public/evm)
573        }
574    }
575}
576