Smart Contract

FlowVaults

A.b1d63873c3cc9f79.FlowVaults

Valid From

131,794,187

Deployed

1w ago
Feb 19, 2026, 10:35:24 AM UTC

Dependents

17 imports
1// standards
2import FungibleToken from 0xf233dcee88fe0abe
3import Burner from 0xf233dcee88fe0abe
4import ViewResolver from 0x1d7e57aa55817448
5// DeFiActions
6import DeFiActions from 0x6d888f175c158410
7import FlowVaultsClosedBeta from 0xb1d63873c3cc9f79
8
9/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION
10/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11///
12access(all) contract FlowVaults {
13
14    /* --- FIELDS --- */
15
16    /// Canonical StoragePath for where TideManager should be stored
17    access(all) let TideManagerStoragePath: StoragePath
18    /// Canonical PublicPath for where TideManager Capability should be published
19    access(all) let TideManagerPublicPath: PublicPath
20    /// Canonical StoragePath for where StrategyFactory should be stored
21    access(all) let FactoryStoragePath: StoragePath
22    /// Canonical PublicPath for where StrategyFactory Capability should be published
23    access(all) let FactoryPublicPath: PublicPath
24
25    /* --- EVENTS --- */
26
27    access(all) event CreatedTide(id: UInt64, uuid: UInt64, strategyType: String, tokenType: String, initialAmount: UFix64, creator: Address?)
28    access(all) event DepositedToTide(id: UInt64, tokenType: String, amount: UFix64, owner: Address?, fromUUID: UInt64)
29    access(all) event WithdrawnFromTide(id: UInt64, tokenType: String, amount: UFix64, owner: Address?, toUUID: UInt64)
30    access(all) event AddedToManager(id: UInt64, owner: Address?, managerUUID: UInt64, tokenType: String)
31    access(all) event BurnedTide(id: UInt64, strategyType: String, tokenType: String, remainingBalance: UFix64)
32
33    /* --- CONSTRUCTS --- */
34
35    /// Strategy
36    ///
37    /// A Strategy is meant to encapsulate the Sink/Source entrypoints allowing for flows into and out of stacked
38    /// DeFiActions components. These compositions are intended to capitalize on some yield-bearing opportunity so that
39    /// a Strategy bears yield on that which is deposited into it, albeit not without some risk. A Strategy then can be
40    /// thought of as the top-level of a nesting of DeFiActions connectors & adapters where one can deposit & withdraw
41    /// funds into the composed DeFi workflows.
42    ///
43    /// While two types of strategies may not highly differ with respect to their fields, the stacking of DeFiActions
44    /// components & connections they provide access to likely do. This difference in wiring is why the Strategy is a
45    /// resource - because the Type and uniqueness of composition of a given Strategy must be preserved as that is its
46    /// distinguishing factor. These qualities are preserved by restricting the party who can construct it, which for
47    /// resources is within the contract that defines it.
48    ///
49    /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away
50    ///     with the added layer of abstraction introduced by a StrategyComposer.
51    access(all) resource interface Strategy : DeFiActions.IdentifiableResource, Burner.Burnable {
52        /// Returns the type of Vaults that this Strategy instance can handle
53        access(all) view fun getSupportedCollateralTypes(): {Type: Bool}
54        /// Returns whether the provided Vault type is supported by this Strategy instance
55        access(all) view fun isSupportedCollateralType(_ type: Type): Bool {
56            return self.getSupportedCollateralTypes()[type] ?? false
57        }
58        /// Returns the balance of the given token available for withdrawal. Note that this may be an estimate due to
59        /// the lack of guarantees inherent to DeFiActions Sources
60        access(all) fun availableBalance(ofToken: Type): UFix64
61        /// Deposits up to the balance of the referenced Vault into this Strategy
62        access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
63            pre {
64                self.isSupportedCollateralType(from.getType()):
65                "Cannot deposit Vault \(from.getType().identifier) to Strategy \(self.getType().identifier) - unsupported deposit type"
66            }
67        }
68        /// Withdraws from this Strategy and returns the resulting Vault of the requested token Type
69        access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} {
70            post {
71                result.getType() == ofToken:
72                "Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)"
73            }
74        }
75    }
76
77    /// StrategyComposer
78    ///
79    /// A StrategyComposer is responsible for stacking DeFiActions connectors in a manner that composes a final Strategy.
80    /// Since DeFiActions Sink/Source only support single assets and some Strategies may be multi-asset, we deal with
81    /// building a Strategy distinctly from encapsulating the top-level DFA connectors acting as entrypoints in to the
82    /// DeFiActions stack.
83    ///
84    /// TODO: Consider making Sink/Source multi-asset - we could then make Strategy a composite Sink, Source & do away
85    ///     with the added layer of abstraction introduced by a StrategyComposer.
86    access(all) resource interface StrategyComposer {
87        /// Returns the Types of Strategies composed by this StrategyComposer
88        access(all) view fun getComposedStrategyTypes(): {Type: Bool}
89        /// Returns the Vault types which can be used to initialize a given Strategy
90        access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool}
91        /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the
92        /// provided Vault type
93        access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool}
94        /// Composes a Strategy of the given type with the provided funds
95        access(all) fun createStrategy(
96            _ type: Type,
97            uniqueID: DeFiActions.UniqueIdentifier,
98            withFunds: @{FungibleToken.Vault}
99        ): @{Strategy} {
100            pre {
101                self.getSupportedInitializationVaults(forStrategy: type)[withFunds.getType()] == true:
102                "Cannot initialize Strategy \(type.identifier) with Vault \(withFunds.getType().identifier) - unsupported initialization Vault"
103                self.getComposedStrategyTypes()[type] == true:
104                "Strategy \(type.identifier) is unsupported by StrategyComposer \(self.getType().identifier)"
105            }
106        }
107    }
108
109    /// StrategyFactory
110    ///
111    /// This resource enables the management of StrategyComposers and the construction of the Strategies they compose.
112    ///
113    access(all) resource StrategyFactory {
114        /// A mapping of StrategyComposers indexed on the related Strategies they can compose
115        access(self) let composers: @{Type: {StrategyComposer}}
116
117        init() {
118            self.composers <- {}
119        }
120
121        /// Returns the Strategy types that can be produced by this StrategyFactory
122        access(all) view fun getSupportedStrategies(): [Type] {
123            return self.composers.keys
124        }
125        /// Returns the Vaults that can be used to initialize a Strategy of the given Type
126        access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} {
127            return self.composers[forStrategy]?.getSupportedInitializationVaults(forStrategy: forStrategy) ?? {}
128        }
129        /// Returns the Vaults that can be deposited to a Strategy initialized with the provided Type
130        access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} {
131            return self.composers[forStrategy]
132                ?.getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith)
133                ?? {}
134        }
135        /// Initializes a new Strategy of the given type with the provided Vault, identifying all associated DeFiActions
136        /// components by the provided UniqueIdentifier
137        access(all)
138        fun createStrategy(_ type: Type, uniqueID: DeFiActions.UniqueIdentifier, withFunds: @{FungibleToken.Vault}): @{Strategy} {
139            pre {
140                self.composers[type] != nil: "Strategy \(type.identifier) is unsupported"
141            }
142            post {
143                result.getType() == type:
144                "Invalid Strategy returned - expected \(type.identifier) but returned \(result.getType().identifier)"
145            }
146            return <- self._borrowComposer(forStrategy: type)
147                .createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds)
148        }
149        /// Sets the provided Strategy and Composer association in the StrategyFactory
150        access(Mutate) fun addStrategyComposer(_ strategy: Type, composer: @{StrategyComposer}) {
151            pre {
152                strategy.isSubtype(of: Type<@{Strategy}>()):
153                "Invalid Strategy Type \(strategy.identifier) - provided Type does not implement the Strategy interface"
154                composer.getComposedStrategyTypes()[strategy] == true:
155                "Strategy \(strategy.identifier) cannot be composed by StrategyComposer \(composer.getType().identifier)"
156            }
157            let old <- self.composers[strategy] <- composer
158            Burner.burn(<-old)
159        }
160        /// Removes the Strategy from this StrategyFactory and returns whether the value existed or not
161        access(Mutate) fun removeStrategy(_ strategy: Type): Bool {
162            if let removed <- self.composers.remove(key: strategy) {
163                Burner.burn(<-removed)
164                return true
165            }
166            return false
167        }
168        /// Returns a reference to the StrategyComposer for the requested Strategy type, reverting if none exists
169        access(self) view fun _borrowComposer(forStrategy: Type): &{StrategyComposer} {
170            return &self.composers[forStrategy] as &{StrategyComposer}?
171                ?? panic("Could not borrow StrategyComposer for Strategy \(forStrategy.identifier)")
172        }
173    }
174
175    /// StrategyComposerIssuer
176    ///
177    /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which
178    /// may utilize resource consumption (i.e. account storage). Contracts defining Strategies that do not require
179    /// such protections may wish to expose Strategy creation publicly via public Capabilities.
180    access(all) resource interface StrategyComposerIssuer {
181        /// Returns the StrategyComposer types supported by this issuer
182        access(all) view fun getSupportedComposers(): {Type: Bool}
183        /// Returns the requested StrategyComposer. If the requested type is unsupported, a revert should be expected
184        access(all) fun issueComposer(_ type: Type): @{StrategyComposer} {
185            post {
186                result.getType() == type:
187                "Invalid StrategyComposer returned - requested \(type.identifier) but returned \(result.getType().identifier)"
188            }
189        }
190    }
191
192    /// Tide
193    ///
194    /// A Tide is a resource enabling the management of a composed Strategy
195    ///
196    access(all) resource Tide : Burner.Burnable, FungibleToken.Receiver, ViewResolver.Resolver {
197        /// The UniqueIdentifier that identifies all related DeFiActions connectors used in the encapsulated Strategy
198        access(contract) let uniqueID: DeFiActions.UniqueIdentifier
199        /// The type of Vault this Tide can receive as a deposit and provides as a withdrawal
200        access(self) let vaultType: Type
201        /// The Strategy granting top-level access to the yield-bearing DeFiActions stack
202        access(self) var strategy: @{Strategy}?
203
204        init(strategyType: Type, withVault: @{FungibleToken.Vault}) {
205            self.uniqueID = DeFiActions.createUniqueIdentifier()
206            self.vaultType = withVault.getType()
207            let _strategy <- FlowVaults.createStrategy(
208                    type: strategyType,
209                    uniqueID: self.uniqueID,
210                    withFunds: <-withVault
211                )
212            assert(_strategy.isSupportedCollateralType(self.vaultType),
213                message: "Vault type \(self.vaultType.identifier) is not supported by Strategy \(strategyType.identifier)")
214            self.strategy <-_strategy
215        }
216
217        /// Returns the Tide's ID as defined by it's DeFiActions.UniqueIdentifier.id
218        access(all) view fun id(): UInt64 {
219            return self.uniqueID.id
220        }
221        /// Returns the balance of the Tide's vaultType available via the encapsulated Strategy
222        access(all) fun getTideBalance(): UFix64 {
223            return self._borrowStrategy().availableBalance(ofToken: self.vaultType)
224        }
225        /// Burner.Burnable conformance - emits the BurnedTide event when burned
226        access(contract) fun burnCallback() {
227            emit BurnedTide(
228                id: self.uniqueID.id,
229                strategyType: self.strategy.getType().identifier,
230                tokenType: self.getType().identifier,
231                remainingBalance: self.getTideBalance()
232            )
233            let _strategy <- self.strategy <- nil
234            Burner.burn(<-_strategy)
235        }
236        /// TODO: FlowVaults specific views
237        access(all) view fun getViews(): [Type] {
238            return []
239        }
240        /// TODO: FlowVaults specific view resolution
241        access(all) fun resolveView(_ view: Type): AnyStruct? {
242            return nil
243        }
244        /// Deposits the provided Vault to the Strategy
245        access(all) fun deposit(from: @{FungibleToken.Vault}) {
246            pre {
247                self.isSupportedVaultType(type: from.getType()):
248                "Deposited vault of type \(from.getType().identifier) is not supported by this Tide"
249            }
250            let amount = from.balance
251            emit DepositedToTide(id: self.uniqueID.id, tokenType: from.getType().identifier, amount: from.balance, owner: self.owner?.address, fromUUID: from.uuid)
252            self._borrowStrategy().deposit(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
253            assert(
254                from.balance == 0.0,
255                message: "Deposit amount \(amount) of \(self.vaultType.identifier) could not be deposited to Tide \(self.id())"
256            )
257            Burner.burn(<-from)
258        }
259        /// Returns the Vaults types supported by this Tide as a mapping associated with their current support status
260        access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
261            return self._borrowStrategy().getSupportedCollateralTypes()
262        }
263        /// Returns whether the given Vault type is supported by this Tide
264        access(all) view fun isSupportedVaultType(type: Type): Bool {
265            return self.getSupportedVaultTypes()[type] ?? false
266        }
267        /// Withdraws the requested amount from the Strategy
268        access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} {
269            post {
270                result.balance == amount:
271                "Invalid Vault balance returned - requested \(amount) but returned \(result.balance)"
272                self.vaultType == result.getType():
273                "Invalid Vault returned - expected \(self.vaultType.identifier) but returned \(result.getType().identifier)"
274            }
275            let available = self._borrowStrategy().availableBalance(ofToken: self.vaultType)
276            assert(amount <= available,
277                message: "Requested amount \(amount) is greater than withdrawable balance of \(available)")
278
279            let res <- self._borrowStrategy().withdraw(maxAmount: amount, ofToken: self.vaultType)
280
281            emit WithdrawnFromTide(id: self.uniqueID.id, tokenType: res.getType().identifier, amount: amount, owner: self.owner?.address, toUUID: res.uuid)
282
283            return <- res
284        }
285        /// Returns an authorized reference to the encapsulated Strategy
286        access(self) view fun _borrowStrategy(): auth(FungibleToken.Withdraw) &{Strategy} {
287            return &self.strategy as auth(FungibleToken.Withdraw) &{Strategy}?
288                ?? panic("Unknown error - could not borrow Strategy for Tide #\(self.id())")
289        }
290    }
291
292    /// TideManager
293    ///
294    /// A TideManager encapsulates nested Tide resources. Through a TideManager, one can create, manage, and close
295    /// out inner Tide resources.
296    ///
297    access(all) resource TideManager : ViewResolver.ResolverCollection {
298        /// The open Tides managed by this TideManager
299        access(self) let tides: @{UInt64: Tide}
300
301        init() {
302            self.tides <- {}
303        }
304
305        /// Borrows the unauthorized Tide with the given id, returning `nil` if none exists
306        access(all) view fun borrowTide(id: UInt64): &Tide? {
307            return &self.tides[id]
308        }
309        /// Borrows the Tide with the given ID as a ViewResolver.Resolver, returning `nil` if none exists
310        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
311            return &self.tides[id]
312        }
313        /// Returns the Tide IDs managed by this TideManager
314        access(all) view fun getIDs(): [UInt64] {
315            return self.tides.keys
316        }
317        /// Returns the number of open Tides currently managed by this TideManager
318        access(all) view fun getNumberOfTides(): Int {
319            return self.tides.length
320        }
321        /// Creates a new Tide executing the specified Strategy with the provided funds
322        access(all) fun createTide(betaRef: auth(FlowVaultsClosedBeta.Beta) &FlowVaultsClosedBeta.BetaBadge, strategyType: Type, withVault: @{FungibleToken.Vault}) {
323            pre {
324                FlowVaultsClosedBeta.validateBeta(self.owner?.address!, betaRef):
325                "Invalid Beta Ref"
326            }
327            let balance = withVault.balance
328            let type = withVault.getType()
329            let tide <-create Tide(strategyType: strategyType, withVault: <-withVault)
330
331            emit CreatedTide(
332                id: tide.uniqueID.id,
333                uuid: tide.uuid,
334                strategyType: strategyType.identifier,
335                tokenType: type.identifier,
336                initialAmount: balance,
337                creator: self.owner?.address
338            )
339
340            self.addTide(betaRef: betaRef, <-tide)
341        }
342        /// Adds an open Tide to this TideManager resource. This effectively transfers ownership of the newly added
343        /// Tide to the owner of this TideManager
344        access(all) fun addTide(betaRef: auth(FlowVaultsClosedBeta.Beta) &FlowVaultsClosedBeta.BetaBadge, _ tide: @Tide) {
345            pre {
346                self.tides[tide.uniqueID.id] == nil:
347                "Collision with Tide ID \(tide.uniqueID.id) - a Tide with this ID already exists"
348
349                FlowVaultsClosedBeta.validateBeta(self.owner?.address!, betaRef):
350                "Invalid Beta Ref"
351            }
352            emit AddedToManager(id: tide.uniqueID.id, owner: self.owner?.address, managerUUID: self.uuid, tokenType: tide.getType().identifier)
353            self.tides[tide.uniqueID.id] <-! tide
354        }
355        /// Deposits additional funds to the specified Tide, reverting if none exists with the provided ID
356        access(all) fun depositToTide(betaRef: auth(FlowVaultsClosedBeta.Beta) &FlowVaultsClosedBeta.BetaBadge, _ id: UInt64, from: @{FungibleToken.Vault}) {
357            pre {
358                self.tides[id] != nil:
359                "No Tide with ID \(id) found"
360
361                FlowVaultsClosedBeta.validateBeta(self.owner?.address!, betaRef):
362                "Invalid Beta Ref"
363            }
364            let tide = (&self.tides[id] as &Tide?)!
365            tide.deposit(from: <-from)
366        }
367        access(self) fun _withdrawTide(id: UInt64): @Tide {
368            pre {
369                self.tides[id] != nil:
370                "No Tide with ID \(id) found"
371            }
372            return <- self.tides.remove(key: id)!
373        }
374        /// Withdraws the specified Tide, reverting if none exists with the provided ID
375        access(FungibleToken.Withdraw) fun withdrawTide(betaRef: auth(FlowVaultsClosedBeta.Beta) &FlowVaultsClosedBeta.BetaBadge, id: UInt64): @Tide {
376            pre {
377                self.tides[id] != nil:
378                "No Tide with ID \(id) found"
379
380                FlowVaultsClosedBeta.validateBeta(self.owner?.address!, betaRef):
381                "Invalid Beta Ref"
382            }
383            return <- self._withdrawTide(id: id)!
384        }
385        /// Withdraws funds from the specified Tide in the given amount. The resulting Vault Type will be whatever
386        /// denomination is supported by the Tide, so callers should examine the Tide to know the resulting Vault to
387        /// expect
388        access(FungibleToken.Withdraw) fun withdrawFromTide(_ id: UInt64, amount: UFix64): @{FungibleToken.Vault} {
389            pre {
390                self.tides[id] != nil:
391                "No Tide with ID \(id) found"
392            }
393            let tide = (&self.tides[id] as auth(FungibleToken.Withdraw) &Tide?)!
394            return <- tide.withdraw(amount: amount)
395        }
396        /// Withdraws and returns all available funds from the specified Tide, destroying the Tide and access to any
397        /// Strategy-related wiring with it
398        access(FungibleToken.Withdraw) fun closeTide(_ id: UInt64): @{FungibleToken.Vault} {
399            pre {
400                self.tides[id] != nil:
401                "No Tide with ID \(id) found"
402            }
403            let tide <- self._withdrawTide(id: id)
404            let res <- tide.withdraw(amount: tide.getTideBalance())
405            Burner.burn(<-tide)
406            return <-res
407        }
408    }
409
410    /* --- PUBLIC METHODS --- */
411
412    /// Returns the Types of Strategies that can be used in Tides
413    access(all) view fun getSupportedStrategies(): [Type] {
414        return self._borrowFactory().getSupportedStrategies()
415    }
416    /// Returns the Vault types which can be used to initialize a given Strategy
417    access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} {
418        return self._borrowFactory().getSupportedInitializationVaults(forStrategy: forStrategy)
419    }
420    /// Returns the Vault types which can be deposited to a given Strategy instance if it was initialized with the
421    /// provided Vault type
422    access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} {
423        return self._borrowFactory().getSupportedInstanceVaults(forStrategy: forStrategy, initializedWith: initializedWith)
424    }
425    /// Creates a Strategy of the requested Type using the provided Vault as an initial deposit
426    access(all) fun createStrategy(type: Type, uniqueID: DeFiActions.UniqueIdentifier, withFunds: @{FungibleToken.Vault}): @{Strategy} {
427        return <- self._borrowFactory().createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds)
428    }
429    /// Creates a TideManager used to create and manage Tides
430    access(all) fun createTideManager(betaRef: auth(FlowVaultsClosedBeta.Beta) &FlowVaultsClosedBeta.BetaBadge): @TideManager {
431        return <-create TideManager()
432    }
433    /// Creates a StrategyFactory resource
434    access(all) fun createStrategyFactory(): @StrategyFactory {
435        return <- create StrategyFactory()
436    }
437
438    /* --- INTERNAL METHODS --- */
439
440    /// Returns a reference to the StrategyFactory stored in this contract's account storage
441    access(self) view fun _borrowFactory(): &StrategyFactory {
442        return self.account.storage.borrow<&StrategyFactory>(from: self.FactoryStoragePath)
443            ?? panic("Could not borrow reference to StrategyFactory from \(self.FactoryStoragePath)")
444    }
445
446    init() {
447        var pathIdentifier = "FlowVaultsTideManager_\(self.account.address)"
448        self.TideManagerStoragePath = StoragePath(identifier: pathIdentifier)!
449        self.TideManagerPublicPath = PublicPath(identifier: pathIdentifier)!
450
451        pathIdentifier = "FlowVaultsStrategyFactory_\(self.account.address)"
452        self.FactoryStoragePath = StoragePath(identifier: pathIdentifier)!
453        self.FactoryPublicPath = PublicPath(identifier: pathIdentifier)!
454
455        // configure a StrategyFactory in storage and publish a public Capability
456        self.account.storage.save(<-create StrategyFactory(), to: self.FactoryStoragePath)
457        let cap = self.account.capabilities.storage.issue<&StrategyFactory>(self.FactoryStoragePath)
458        self.account.capabilities.publish(cap, at: self.FactoryPublicPath)
459    }
460}
461