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