Smart Contract
DeFiActions
A.0b11b1848a8aa2c0.DeFiActions
1import Burner from 0x9a0766d93b6608b7
2import ViewResolver from 0x631e88ae7f1d7c20
3import FlowToken from 0x7e60df042a9c0868
4import FungibleToken from 0x9a0766d93b6608b7
5import FlowTransactionScheduler from 0x8c5303eaa26202d6
6
7import DeFiActionsUtils from 0x0b11b1848a8aa2c0
8
9/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
11/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12///
13/// [BETA] DeFiActions
14///
15/// DeFiActions is a library of small DeFi components that act as glue to connect typical DeFi primitives (dexes, lending
16/// pools, farms) into individual aggregations.
17///
18/// The core component of DeFiActions is the “Connector”; a conduit between the more complex pieces of the DeFi puzzle.
19/// Connectors aren't to do anything especially complex, but make it simple and straightforward to connect the
20/// traditional DeFi pieces together into new, custom aggregations.
21///
22/// Connectors should be thought of analogously with the small text processing tools of Unix that are mostly meant to be
23/// connected with pipe operations instead of being operated individually. All Connectors are either a “Source” or
24/// “Sink”.
25///
26access(all) contract DeFiActions {
27
28 /* --- FIELDS --- */
29
30 /// The current ID assigned to UniqueIdentifiers as they are initialized
31 /// It is incremented by 1 every time a UniqueIdentifier is created so each ID is only ever used once
32 access(all) var currentID: UInt64
33 /// The AuthenticationToken Capability required to create a UniqueIdentifier
34 access(self) let authTokenCap: Capability<auth(Identify) &AuthenticationToken>
35 /// The StoragePath for the AuthenticationToken resource
36 access(self) let AuthTokenStoragePath: StoragePath
37
38 /* --- INTERFACE-LEVEL EVENTS --- */
39
40 /// Emitted when value is deposited to a Sink
41 access(all) event Deposited(
42 type: String,
43 amount: UFix64,
44 fromUUID: UInt64,
45 uniqueID: UInt64?,
46 sinkType: String
47 )
48 /// Emitted when value is withdrawn from a Source
49 access(all) event Withdrawn(
50 type: String,
51 amount: UFix64,
52 withdrawnUUID: UInt64,
53 uniqueID: UInt64?,
54 sourceType: String
55 )
56 /// Emitted when a Swapper executes a Swap
57 access(all) event Swapped(
58 inVault: String,
59 outVault: String,
60 inAmount: UFix64,
61 outAmount: UFix64,
62 inUUID: UInt64,
63 outUUID: UInt64,
64 uniqueID: UInt64?,
65 swapperType: String
66 )
67 /// Emitted when a Flasher executes a flash loan
68 access(all) event Flashed(
69 requestedAmount: UFix64,
70 borrowType: String,
71 uniqueID: UInt64?,
72 flasherType: String
73 )
74 /// Emitted when an IdentifiableResource's UniqueIdentifier is aligned with another DFA component
75 access(all) event UpdatedID(
76 oldID: UInt64?,
77 newID: UInt64?,
78 component: String,
79 uuid: UInt64?
80 )
81 /// Emitted when an AutoBalancer is created
82 access(all) event CreatedAutoBalancer(
83 lowerThreshold: UFix64,
84 upperThreshold: UFix64,
85 vaultType: String,
86 vaultUUID: UInt64,
87 uuid: UInt64,
88 uniqueID: UInt64?
89 )
90 /// Emitted when AutoBalancer.rebalance() is called
91 access(all) event Rebalanced(
92 amount: UFix64,
93 value: UFix64,
94 unitOfAccount: String,
95 isSurplus: Bool,
96 vaultType: String,
97 vaultUUID: UInt64,
98 balancerUUID: UInt64,
99 address: Address?,
100 uniqueID: UInt64?
101 )
102 /// Emitted when an AutoBalancer fails to self-schedule a recurring rebalance
103 access(all) event FailedRecurringSchedule(
104 whileExecuting: UInt64,
105 balancerUUID: UInt64,
106 address: Address?,
107 error: String,
108 uniqueID: UInt64?
109 )
110
111 /// Emitted when Liquidator.liquidate is called
112 access(all) event Liquidated()
113
114 /* --- CONSTRUCTS --- */
115
116 access(all) entitlement Identify
117
118 /// AuthenticationToken
119 ///
120 /// A resource intended to ensure UniqueIdentifiers are only created by the DeFiActions contract
121 ///
122 access(all) resource AuthenticationToken {}
123
124 /// UniqueIdentifier
125 ///
126 /// This construct enables protocols to trace stack operations via DeFiActions interface-level events, identifying
127 /// them by UniqueIdentifier IDs. IdentifiableResource Implementations should ensure that access to them is
128 /// encapsulated by the structures they are used to identify.
129 ///
130 access(all) struct UniqueIdentifier {
131 /// The ID value of this UniqueIdentifier
132 access(all) let id: UInt64
133 /// The AuthenticationToken Capability required to create this UniqueIdentifier. Since this is a struct which
134 /// can be created in any context, this authorized Capability ensures that the UniqueIdentifier can only be
135 /// created by the DeFiActions contract, thus preventing forged UniqueIdentifiers from being created.
136 access(self) let authCap: Capability<auth(Identify) &AuthenticationToken>
137
138 access(contract) view init(_ id: UInt64, _ authCap: Capability<auth(Identify) &AuthenticationToken>) {
139 pre {
140 authCap.check(): "Invalid AuthenticationToken Capability provided"
141 }
142 self.id = id
143 self.authCap = authCap
144 }
145 }
146
147 /// ComponentInfo
148 ///
149 /// A struct containing minimal information about a DeFiActions component and its inner components
150 ///
151 access(all) struct ComponentInfo {
152 /// The type of the component
153 access(all) let type: Type
154 /// The UniqueIdentifier.id of the component
155 access(all) let id: UInt64?
156 /// The inner component types of the serving component
157 access(all) let innerComponents: [ComponentInfo]
158 init(
159 type: Type,
160 id: UInt64?,
161 innerComponents: [ComponentInfo]
162 ) {
163 self.type = type
164 self.id = id
165 self.innerComponents = innerComponents
166 }
167 }
168
169 /// Extend entitlement allowing for the authorized copying of UniqueIdentifiers from existing components
170 access(all) entitlement Extend
171
172 /// IdentifiableResource
173 ///
174 /// A resource interface containing a UniqueIdentifier and convenience getters about it
175 ///
176 access(all) struct interface IdentifiableStruct {
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: UniqueIdentifier?
180 /// Convenience method returning the inner UniqueIdentifier's id or `nil` if none is set.
181 ///
182 /// NOTE: This interface method may be spoofed if the function is overridden, so callers should not rely on it
183 /// for critical identification unless the implementation itself is known and trusted
184 access(all) view fun id(): UInt64? {
185 return self.uniqueID?.id
186 }
187 /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
188 /// each inner component in the stack.
189 access(all) fun getComponentInfo(): ComponentInfo
190 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
191 /// a DeFiActions stack. See DeFiActions.align() for more information.
192 access(contract) view fun copyID(): UniqueIdentifier? {
193 post {
194 result?.id == self.uniqueID?.id:
195 "UniqueIdentifier of \(self.getType().identifier) was not successfully copied"
196 }
197 }
198 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
199 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
200 access(contract) fun setID(_ id: UniqueIdentifier?) {
201 post {
202 self.uniqueID?.id == id?.id:
203 "UniqueIdentifier of \(self.getType().identifier) was not successfully set"
204 DeFiActions.emitUpdatedID(
205 oldID: before(self.uniqueID?.id),
206 newID: self.uniqueID?.id,
207 component: self.getType().identifier,
208 uuid: nil // no UUID for structs
209 ): "Unknown error emitting DeFiActions.UpdatedID from IdentifiableStruct \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED")
210 }
211 }
212 }
213
214 /// IdentifiableResource
215 ///
216 /// A resource interface containing a UniqueIdentifier and convenience getters about it
217 ///
218 access(all) resource interface IdentifiableResource {
219 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
220 /// specific Identifier to associated connectors on construction
221 access(contract) var uniqueID: UniqueIdentifier?
222 /// Convenience method returning the inner UniqueIdentifier's id or `nil` if none is set.
223 ///
224 /// NOTE: This interface method may be spoofed if the function is overridden, so callers should not rely on it
225 /// for critical identification unless the implementation itself is known and trusted
226 access(all) view fun id(): UInt64? {
227 return self.uniqueID?.id
228 }
229 /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
230 /// each inner component in the stack.
231 access(all) fun getComponentInfo(): ComponentInfo
232 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
233 /// a DeFiActions stack. See DeFiActions.align() for more information.
234 access(contract) view fun copyID(): UniqueIdentifier? {
235 post {
236 result?.id == self.uniqueID?.id:
237 "UniqueIdentifier of \(self.getType().identifier) was not successfully copied"
238 }
239 }
240 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
241 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
242 access(contract) fun setID(_ id: UniqueIdentifier?) {
243 post {
244 self.uniqueID?.id == id?.id:
245 "UniqueIdentifier of \(self.getType().identifier) was not successfully set"
246 DeFiActions.emitUpdatedID(
247 oldID: before(self.uniqueID?.id),
248 newID: self.uniqueID?.id,
249 component: self.getType().identifier,
250 uuid: self.uuid
251 ): "Unknown error emitting DeFiActions.UpdatedID from IdentifiableStruct \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED")
252 }
253 }
254 }
255
256 /// Sink
257 ///
258 /// A Sink Connector (or just “Sink”) is analogous to the Fungible Token Receiver interface that accepts deposits of
259 /// funds. It differs from the standard Receiver interface in that it is a struct interface (instead of resource
260 /// interface) and allows for the graceful handling of Sinks that have a limited capacity on the amount they can
261 /// accept for deposit. Implementations should therefore avoid the possibility of reversion with graceful fallback
262 /// on unexpected conditions, executing no-ops instead of reverting.
263 ///
264 access(all) struct interface Sink : IdentifiableStruct {
265 /// Returns the Vault type accepted by this Sink
266 access(all) view fun getSinkType(): Type
267 /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity
268 access(all) fun minimumCapacity(): UFix64
269 /// Deposits up to the Sink's capacity from the provided Vault
270 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
271 pre {
272 from.getType() == self.getSinkType():
273 "Invalid vault provided for deposit - \(from.getType().identifier) is not \(self.getSinkType().identifier)"
274 }
275 post {
276 DeFiActions.emitDeposited(
277 type: from.getType().identifier,
278 beforeBalance: before(from.balance),
279 afterBalance: from.balance,
280 fromUUID: from.uuid,
281 uniqueID: self.uniqueID?.id,
282 sinkType: self.getType().identifier
283 ): "Unknown error emitting DeFiActions.Withdrawn from Sink \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED")
284 }
285 }
286 }
287
288 /// Source
289 ///
290 /// A Source Connector (or just “Source”) is analogous to the Fungible Token Provider interface that provides funds
291 /// on demand. It differs from the standard Provider interface in that it is a struct interface (instead of resource
292 /// interface) and allows for graceful handling of the case that the Source might not know exactly the total amount
293 /// of funds available to be withdrawn. Implementations should therefore avoid the possibility of reversion with
294 /// graceful fallback on unexpected conditions, executing no-ops or returning an empty Vault instead of reverting.
295 ///
296 access(all) struct interface Source : IdentifiableStruct {
297 /// Returns the Vault type provided by this Source
298 access(all) view fun getSourceType(): Type
299 /// Returns an estimate of how much of the associated Vault Type can be provided by this Source
300 access(all) fun minimumAvailable(): UFix64
301 /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be
302 /// returned
303 access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
304 post {
305 result.getType() == self.getSourceType():
306 "Invalid vault provided for withdraw - \(result.getType().identifier) is not \(self.getSourceType().identifier)"
307 DeFiActions.emitWithdrawn(
308 type: result.getType().identifier,
309 amount: result.balance,
310 withdrawnUUID: result.uuid,
311 uniqueID: self.uniqueID?.id ?? nil,
312 sourceType: self.getType().identifier
313 ): "Unknown error emitting DeFiActions.Withdrawn from Source \(self.getType().identifier) with ID ".concat(self.id()?.toString() ?? "UNASSIGNED")
314 }
315 }
316 }
317
318 /// Quote
319 ///
320 /// An interface for an estimate to be returned by a Swapper when asking for a swap estimate. This may be helpful
321 /// for passing additional parameters to a Swapper relevant to the use case. Implementations may choose to add
322 /// fields relevant to their Swapper implementation and downcast in swap() and/or swapBack() scope.
323 /// By convention, a Quote with inAmount==outAmount==0 indicates no estimated swap price is available.
324 ///
325 access(all) struct interface Quote {
326 /// The quoted pre-swap Vault type
327 access(all) let inType: Type
328 /// The quoted post-swap Vault type
329 access(all) let outType: Type
330 /// The quoted amount of pre-swap currency
331 access(all) let inAmount: UFix64
332 /// The quoted amount of post-swap currency for the defined inAmount
333 access(all) let outAmount: UFix64
334 }
335
336 /// Swapper
337 ///
338 /// A basic interface for a struct that swaps between tokens. Implementations may choose to adapt this interface
339 /// to fit any given swap protocol or set of protocols.
340 ///
341 access(all) struct interface Swapper : IdentifiableStruct {
342 /// The type of Vault this Swapper accepts when performing a swap
343 access(all) view fun inType(): Type
344 /// The type of Vault this Swapper provides when performing a swap
345 access(all) view fun outType(): Type
346 /// Provides a quote for how many input tokens can be swapped for `forDesired` output tokens.
347 /// The reverse flag simply inverts inType/outType and inAmount/outAmount in the quote.
348 /// Interpretation:
349 /// - reverse=false -> I want to provide `quote.inAmount` input tokens and receive `forDesired` output tokens.
350 /// - reverse=true -> I want to provide `forDesired` output tokens and receive `quote.inAmount` input tokens.
351 access(all) fun quoteIn(forDesired: UFix64, reverse: Bool): {Quote}
352 /// The estimated amount delivered out for a provided input balance
353 /// Provides a quote for how many output tokens can be swapped for `forProvided` input tokens.
354 /// The reverse flag simply inverts inType/outType and inAmount/outAmount in the quote.
355 /// Interpretation:
356 /// - reverse=false -> I want to provide `forProvided` input tokens and receive `quote.outAmount` output tokens.
357 /// - reverse=true -> I want to provide `quote.outAmount` output tokens and receive `forProvided` input tokens.
358 access(all) fun quoteOut(forProvided: UFix64, reverse: Bool): {Quote}
359 /// Performs a swap taking a Vault of type inVault, outputting a resulting outVault. Implementations may choose
360 /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
361 /// to use multiple Flow swap protocols.
362 access(all) fun swap(quote: {Quote}?, inVault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
363 pre {
364 inVault.getType() == self.inType():
365 "Invalid vault provided for swap - \(inVault.getType().identifier) is not \(self.inType().identifier)"
366 (quote?.inType ?? inVault.getType()) == inVault.getType():
367 "Quote.inType type \(quote!.inType.identifier) does not match the provided inVault \(inVault.getType().identifier)"
368 }
369 post {
370 result.getType() == self.outType():
371 "Invalid swap() result - \(result.getType().identifier) is not \(self.outType().identifier)"
372 emit Swapped(
373 inVault: before(inVault.getType().identifier),
374 outVault: result.getType().identifier,
375 inAmount: before(inVault.balance),
376 outAmount: result.balance,
377 inUUID: before(inVault.uuid),
378 outUUID: result.uuid,
379 uniqueID: self.uniqueID?.id ?? nil,
380 swapperType: self.getType().identifier
381 )
382 }
383 }
384 /// Performs a swap taking a Vault of type outVault, outputting a resulting inVault. Implementations may choose
385 /// to swap along a pre-set path or an optimal path of a set of paths or even set of contained Swappers adapted
386 /// to use multiple Flow swap protocols.
387 access(all) fun swapBack(quote: {Quote}?, residual: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
388 pre {
389 residual.getType() == self.outType():
390 "Invalid vault provided for swapBack - \(residual.getType().identifier) is not \(self.outType().identifier)"
391 }
392 post {
393 result.getType() == self.inType():
394 "Invalid swapBack() result - \(result.getType().identifier) is not \(self.inType().identifier)"
395 emit Swapped(
396 inVault: before(residual.getType().identifier),
397 outVault: result.getType().identifier,
398 inAmount: before(residual.balance),
399 outAmount: result.balance,
400 inUUID: before(residual.uuid),
401 outUUID: result.uuid,
402 uniqueID: self.uniqueID?.id ?? nil,
403 swapperType: self.getType().identifier
404 )
405 }
406 }
407 }
408
409 /// SwapperProvider
410 ///
411 /// An interface for a wrapper around one or more Swappers.
412 /// For example, a DEX which supports multiple trading pairs is conceptually a SwapperProvider which
413 /// can provide a Swapper for each of its supported trading pairs.
414 ///
415 access(all) struct interface SwapperProvider {
416 /// Returns a Swapper for the given trade pair, if the pair is supported.
417 /// Otherwise returns nil.
418 access(all) fun getSwapper(inType: Type, outType: Type): {DeFiActions.Swapper}?
419 }
420
421 /// PriceOracle
422 ///
423 /// An interface for a price oracle adapter. Implementations should adapt this interface to various price feed
424 /// oracles deployed on Flow
425 ///
426 access(all) struct interface PriceOracle : IdentifiableStruct {
427 /// Returns the asset type serving as the price basis - e.g. USD in FLOW/USD
428 access(all) view fun unitOfAccount(): Type
429 /// Returns the latest price data for a given asset denominated in unitOfAccount() if available, otherwise `nil`
430 /// should be returned. Callers should note that although an optional is supported, implementations may choose
431 /// to revert.
432 access(all) fun price(ofToken: Type): UFix64? {
433 post {
434 result == nil || result! > 0.0:
435 "PriceOracle must return a price greater than 0.0 if available"
436 }
437 }
438 }
439
440 /// Flasher
441 ///
442 /// An interface for a flash loan adapter. Implementations should adapt this interface to various flash loan
443 /// protocols deployed on Flow
444 ///
445 access(all) struct interface Flasher : IdentifiableStruct {
446 /// Returns the asset type this Flasher can issue as a flash loan
447 access(all) view fun borrowType(): Type
448 /// Returns the estimated fee for a flash loan of the specified amount
449 access(all) fun calculateFee(loanAmount: UFix64): UFix64
450 /// Performs a flash loan of the specified amount. The callback function is passed the fee amount, a Vault
451 /// containing the loan, and the data. The callback function should return a Vault containing the loan + fee.
452 access(all) fun flashLoan(
453 amount: UFix64,
454 data: AnyStruct?,
455 callback: fun(UFix64, @{FungibleToken.Vault}, AnyStruct?): @{FungibleToken.Vault} // fee, loan, data
456 ) {
457 post {
458 emit Flashed(
459 requestedAmount: amount,
460 borrowType: self.borrowType().identifier,
461 uniqueID: self.uniqueID?.id ?? nil,
462 flasherType: self.getType().identifier
463 )
464 }
465 }
466 }
467
468 /// Liquidator
469 ///
470 /// A Liquidator connector enables the liquidation of funds. The general use case is withdrawing all
471 /// available funds from a connected liquidity source.
472 ///
473 access(all) struct interface Liquidator : IdentifiableStruct {
474 /// Returns the type this Liquidator provides on liquidation
475 access(all) view fun getLiquidationType(): Type
476 /// Returns the amount available for liquidation
477 access(all) fun liquidationAmount(): UFix64
478 /// Liquidates available funds. It's up to the implementation to cast and utilize the provided data
479 /// if any is provided.
480 access(FungibleToken.Withdraw) fun liquidate(data: AnyStruct?): @{FungibleToken.Vault} {
481 post {
482 result.getType() == self.getLiquidationType():
483 "Invalid liquidation - expected \(self.getLiquidationType().identifier) but returned \(result.getType().identifier)"
484 emit Liquidated()
485 }
486 }
487 }
488
489 /*******************************************************************************************************************
490 NOTICE: The AutoBalancer will extend the FlowCallbackScheduler.CallbackHandler interface which is not yet
491 finalized. To avoid the need for re-deploying with that interface and related fields managing ScheduleCallback
492 structs, the AutoBalancer and its connectors are omitted from the DeFiActions contract on Testnet & Mainnet
493 until the FlowCallbackScheduler contract is available.
494 *******************************************************************************************************************/
495
496 /// AutoBalancerSink
497 ///
498 /// A DeFiActions Sink enabling the deposit of funds to an underlying AutoBalancer resource. As written, this Source
499 /// may be used with externally defined AutoBalancer implementations
500 ///
501 access(all) struct AutoBalancerSink : Sink {
502 /// The Type this Sink accepts
503 access(self) let type: Type
504 /// An authorized Capability on the underlying AutoBalancer where funds are deposited
505 access(self) let autoBalancer: Capability<&AutoBalancer>
506 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
507 /// specific Identifier to associated connectors on construction
508 access(contract) var uniqueID: UniqueIdentifier?
509
510 init(autoBalancer: Capability<&AutoBalancer>, uniqueID: UniqueIdentifier?) {
511 pre {
512 autoBalancer.check():
513 "Invalid AutoBalancer Capability Provided"
514 }
515 self.type = autoBalancer.borrow()!.vaultType()
516 self.autoBalancer = autoBalancer
517 self.uniqueID = uniqueID
518 }
519
520 /// Returns the Vault type accepted by this Sink
521 access(all) view fun getSinkType(): Type {
522 return self.type
523 }
524 /// Returns an estimate of how much can be withdrawn from the depositing Vault for this Sink to reach capacity
525 /// can currently only be UFix64.max or 0.0
526 access(all) fun minimumCapacity(): UFix64 {
527 return self.autoBalancer.check() ? UFix64.max : 0.0
528 }
529 /// Deposits up to the Sink's capacity from the provided Vault
530 access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
531 if let ab = self.autoBalancer.borrow() {
532 ab.deposit(from: <-from.withdraw(amount: from.balance))
533 }
534 return
535 }
536 /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
537 /// each inner component in the stack.
538 access(all) fun getComponentInfo(): ComponentInfo {
539 return ComponentInfo(
540 type: self.getType(),
541 id: self.id(),
542 innerComponents: []
543 )
544 }
545 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
546 /// a DeFiActions stack. See DeFiActions.align() for more information.
547 access(contract) view fun copyID(): UniqueIdentifier? {
548 return self.uniqueID
549 }
550 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
551 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
552 access(contract) fun setID(_ id: UniqueIdentifier?) {
553 self.uniqueID = id
554 }
555 }
556
557 /// AutoBalancerSource
558 ///
559 /// A DeFiActions Source targeting an underlying AutoBalancer resource. As written, this Source may be used with
560 /// externally defined AutoBalancer implementations
561 ///
562 access(all) struct AutoBalancerSource : Source {
563 /// The Type this Source provides
564 access(self) let type: Type
565 /// An authorized Capability on the underlying AutoBalancer where funds are sourced
566 access(self) let autoBalancer: Capability<auth(FungibleToken.Withdraw) &AutoBalancer>
567 /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
568 /// specific Identifier to associated connectors on construction
569 access(contract) var uniqueID: UniqueIdentifier?
570
571 init(autoBalancer: Capability<auth(FungibleToken.Withdraw) &AutoBalancer>, uniqueID: UniqueIdentifier?) {
572 pre {
573 autoBalancer.check():
574 "Invalid AutoBalancer Capability Provided"
575 }
576 self.type = autoBalancer.borrow()!.vaultType()
577 self.autoBalancer = autoBalancer
578 self.uniqueID = uniqueID
579 }
580
581 /// Returns the Vault type provided by this Source
582 access(all) view fun getSourceType(): Type {
583 return self.type
584 }
585 /// Returns an estimate of how much of the associated Vault Type can be provided by this Source
586 access(all) fun minimumAvailable(): UFix64 {
587 if let ab = self.autoBalancer.borrow() {
588 return ab.vaultBalance()
589 }
590 return 0.0
591 }
592 /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault should be
593 /// returned
594 access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
595 if let ab = self.autoBalancer.borrow() {
596 return <-ab.withdraw(
597 amount: maxAmount <= ab.vaultBalance() ? maxAmount : ab.vaultBalance()
598 )
599 }
600 return <- DeFiActionsUtils.getEmptyVault(self.type)
601 }
602 /// Returns a ComponentInfo struct containing information about this component and a list of ComponentInfo for
603 /// each inner component in the stack.
604 access(all) fun getComponentInfo(): ComponentInfo {
605 return ComponentInfo(
606 type: self.getType(),
607 id: self.id(),
608 innerComponents: []
609 )
610 }
611 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
612 /// a DeFiActions stack. See DeFiActions.align() for more information.
613 access(contract) view fun copyID(): UniqueIdentifier? {
614 return self.uniqueID
615 }
616 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
617 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
618 access(contract) fun setID(_ id: UniqueIdentifier?) {
619 self.uniqueID = id
620 }
621 }
622
623 /// Entitlement used by the AutoBalancer to set inner Sink and Source
624 access(all) entitlement Auto
625 access(all) entitlement Set
626 access(all) entitlement Get
627 access(all) entitlement Configure
628 access(all) entitlement Schedule
629
630 /// AutoBalancerRecurringConfig
631 ///
632 /// A struct containing the configuration so that a recurring rebalance of an AutoBalancer can be scheduled
633 ///
634 access(all) struct AutoBalancerRecurringConfig {
635 /// How frequently the rebalance will be executed (in seconds)
636 access(all) let interval: UInt64
637 /// The priority of the rebalance
638 access(all) let priority: FlowTransactionScheduler.Priority
639 /// The execution effort of the rebalance
640 access(all) let executionEffort: UInt64
641 /// The AutoBalancer UUID that this config is assigned to
642 access(all) var assignedAutoBalancer: UInt64?
643 /// The force rebalance flag
644 access(contract) let forceRebalance: Bool
645 /// The txnFunder used to fund the rebalance - must provide FLOW and accept FLOW
646 access(contract) var txnFunder: {Sink, Source}
647
648 init(
649 interval: UInt64,
650 priority: FlowTransactionScheduler.Priority,
651 executionEffort: UInt64,
652 forceRebalance: Bool,
653 txnFunder: {Sink, Source}
654 ) {
655 pre {
656 interval > UInt64(0):
657 "Invalid interval: \(interval) - must be greater than 0"
658 interval < UInt64(UFix64.max) - UInt64(getCurrentBlock().timestamp):
659 "Invalid interval: \(interval) - must be less than the maximum interval of \(UInt64(UFix64.max) - UInt64(getCurrentBlock().timestamp))"
660 txnFunder.getSourceType() == Type<@FlowToken.Vault>():
661 "Invalid txnFunder: \(txnFunder.getSourceType().identifier) - must provide FLOW but provides \(txnFunder.getSourceType().identifier)"
662 txnFunder.getSinkType() == Type<@FlowToken.Vault>():
663 "Invalid txnFunder: \(txnFunder.getSinkType().identifier) - must accept FLOW but accepts \(txnFunder.getSinkType().identifier)"
664 }
665 let schedulerConfig = FlowTransactionScheduler.getConfig()
666 let minEffort = schedulerConfig.minimumExecutionEffort
667 assert(executionEffort >= minEffort,
668 message: "Invalid execution effort: \(executionEffort) - must be greater than or equal to the minimum execution effort of \(minEffort)")
669 assert(executionEffort <= schedulerConfig.maximumIndividualEffort,
670 message: "Invalid execution effort: \(executionEffort) - must be less than or equal to the maximum individual effort of \(schedulerConfig.maximumIndividualEffort)")
671
672 self.interval = interval
673 self.priority = priority
674 self.executionEffort = executionEffort
675 self.forceRebalance = forceRebalance
676 self.txnFunder = txnFunder
677 self.assignedAutoBalancer = nil
678 }
679
680 /// Sets the AutoBalancer's UUID that this config is assigned to when this AutoBalancerRecurringConfig is set
681 access(contract) fun setAssignedAutoBalancer(_ uuid: UInt64) {
682 pre {
683 self.assignedAutoBalancer == nil || self.assignedAutoBalancer == uuid:
684 "Invalid AutoBalancer UUID \(uuid): AutoBalancerConfig.assignedAutoBalancer is already set to \(self.assignedAutoBalancer!)"
685 }
686 self.assignedAutoBalancer = uuid
687 }
688 }
689
690 /// AutoBalancer
691 ///
692 /// A resource designed to enable permissionless rebalancing of value around a wrapped Vault. An
693 /// AutoBalancer can be a critical component of DeFiActions stacks by allowing for strategies to compound, repay
694 /// loans or direct accumulated value to other sub-systems and/or user Vaults.
695 ///
696 access(all) resource AutoBalancer :
697 IdentifiableResource,
698 FungibleToken.Receiver,
699 FungibleToken.Provider,
700 ViewResolver.Resolver,
701 Burner.Burnable,
702 FlowTransactionScheduler.TransactionHandler
703 {
704 /// The value in deposits & withdrawals over time denominated in oracle.unitOfAccount()
705 access(self) var _valueOfDeposits: UFix64
706 /// The percentage low and high thresholds defining when a rebalance executes
707 /// Index 0 is low, index 1 is high
708 access(self) var _rebalanceRange: [UFix64; 2]
709 /// Oracle used to track the baseValue for deposits & withdrawals over time
710 access(self) let _oracle: {PriceOracle}
711 /// The inner Vault's Type captured for the ResourceDestroyed event
712 access(self) let _vaultType: Type
713 /// Vault used to deposit & withdraw from made optional only so the Vault can be burned via Burner.burn() if the
714 /// AutoBalancer is burned and the Vault's burnCallback() can be called in the process
715 access(self) var _vault: @{FungibleToken.Vault}?
716 /// An optional Sink used to deposit excess funds from the inner Vault once the converted value exceeds the
717 /// rebalance range. This Sink may be used to compound yield into a position or direct excess value to an
718 /// external Vault
719 access(self) var _rebalanceSink: {Sink}?
720 /// An optional Source used to deposit excess funds to the inner Vault once the converted value is below the
721 /// rebalance range
722 access(self) var _rebalanceSource: {Source}?
723 /// Capability on this AutoBalancer instance
724 access(self) var _selfCap: Capability<auth(FungibleToken.Withdraw, FlowTransactionScheduler.Execute) &AutoBalancer>?
725 /// The timestamp of the last rebalance
726 access(self) var _lastRebalanceTimestamp: UFix64
727 /// An optional recurring config for the AutoBalancer
728 access(self) var _recurringConfig: AutoBalancerRecurringConfig?
729 /// ScheduledTransaction objects used to manage automated rebalances
730 access(self) var _scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction}
731 /// An optional UniqueIdentifier tying this AutoBalancer to a given stack
732 access(contract) var uniqueID: UniqueIdentifier?
733
734 /// Emitted when the AutoBalancer is destroyed
735 access(all) event ResourceDestroyed(
736 uuid: UInt64 = self.uuid,
737 vaultType: String = self._vaultType.identifier,
738 balance: UFix64? = self._vault?.balance,
739 uniqueID: UInt64? = self.uniqueID?.id
740 )
741
742 init(
743 lower: UFix64,
744 upper: UFix64,
745 oracle: {PriceOracle},
746 vaultType: Type,
747 outSink: {Sink}?,
748 inSource: {Source}?,
749 recurringConfig: AutoBalancerRecurringConfig?,
750 uniqueID: UniqueIdentifier?
751 ) {
752 pre {
753 lower < upper && 0.01 <= lower && lower < 1.0 && 1.0 < upper && upper < 2.0:
754 "Invalid rebalanceRange [lower, upper]: [\(lower), \(upper)] - thresholds must be set such that 0.01 <= lower < 1.0 and 1.0 < upper < 2.0 relative to value of deposits"
755 DeFiActionsUtils.definingContractIsFungibleToken(vaultType):
756 "The contract defining Vault \(vaultType.identifier) does not conform to FungibleToken contract interface"
757 recurringConfig?.assignedAutoBalancer == nil || recurringConfig?.assignedAutoBalancer == self.uuid:
758 "Invalid recurringConfig: \(recurringConfig!.assignedAutoBalancer!) - must be assigned to this AutoBalancer"
759 }
760 assert(oracle.price(ofToken: vaultType) != nil,
761 message: "Provided Oracle \(oracle.getType().identifier) could not provide a price for vault \(vaultType.identifier)")
762
763 self._valueOfDeposits = 0.0
764 self._rebalanceRange = [lower, upper]
765 self._oracle = oracle
766 self._vault <- DeFiActionsUtils.getEmptyVault(vaultType)
767 self._vaultType = vaultType
768 self._rebalanceSink = outSink
769 self._rebalanceSource = inSource
770 self._selfCap = nil
771 self._lastRebalanceTimestamp = getCurrentBlock().timestamp
772 self._recurringConfig = recurringConfig
773 self._recurringConfig?.setAssignedAutoBalancer(self.uuid)
774 self._scheduledTransactions <- {}
775 self.uniqueID = uniqueID
776
777 emit CreatedAutoBalancer(
778 lowerThreshold: lower,
779 upperThreshold: upper,
780 vaultType: vaultType.identifier,
781 vaultUUID: self._borrowVault().uuid,
782 uuid: self.uuid,
783 uniqueID: self.id()
784 )
785 }
786
787 /* Core AutoBalancer Functionality */
788
789 /// Returns the balance of the inner Vault
790 ///
791 /// @return the current balance of the inner Vault
792 ///
793 access(all) view fun vaultBalance(): UFix64 {
794 return self._borrowVault().balance
795 }
796 /// Returns the Type of the inner Vault
797 ///
798 /// @return the Type of the inner Vault
799 ///
800 access(all) view fun vaultType(): Type {
801 return self._borrowVault().getType()
802 }
803 /// Returns the low and high rebalance thresholds as a fixed length UFix64 containing [low, high]
804 ///
805 /// @return a sorted fixed-length array containing the relative lower and upper thresholds conditioning
806 /// rebalance execution
807 ///
808 access(all) view fun rebalanceThresholds(): [UFix64; 2] {
809 return self._rebalanceRange
810 }
811 /// Returns the value of all accounted deposits/withdraws as they have occurred denominated in unitOfAccount.
812 /// The returned value is the value as tracked historically, not necessarily the current value of the inner
813 /// Vault's balance.
814 ///
815 /// @return the historical value of deposits
816 ///
817 access(all) view fun valueOfDeposits(): UFix64 {
818 return self._valueOfDeposits
819 }
820 /// Returns the token Type serving as the price basis of this AutoBalancer
821 ///
822 /// @return the price denomination of value of the underlying vault as returned from the inner PriceOracle
823 ///
824 access(all) view fun unitOfAccount(): Type {
825 return self._oracle.unitOfAccount()
826 }
827 /// Returns the current value of the inner Vault's balance. If a price is not available from the AutoBalancer's
828 /// PriceOracle, `nil` is returned
829 ///
830 /// @return the current value of the inner's Vault's balance denominated in unitOfAccount() if a price is
831 /// available, `nil` otherwise
832 ///
833 access(all) fun currentValue(): UFix64? {
834 if let price = self._oracle.price(ofToken: self.vaultType()) {
835 return price * self._borrowVault().balance
836 }
837 return nil
838 }
839 /// Returns a ComponentInfo struct containing information about this AutoBalancer and its inner DFA components
840 ///
841 /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
842 /// each inner component in the stack.
843 ///
844 access(all) fun getComponentInfo(): ComponentInfo {
845 // get the inner components
846 let oracle = self._borrowOracle()
847 let inner: [ComponentInfo] = [oracle.getComponentInfo()]
848
849 // get the info for the optional inner components if they exist
850 let maybeSink = self._borrowSink()
851 let maybeSource = self._borrowSource()
852 if let sink = maybeSink {
853 inner.append(sink.getComponentInfo())
854 }
855 if let source = maybeSource {
856 inner.append(source.getComponentInfo())
857 }
858
859 // create the ComponentInfo for the AutoBalancer and insert it at the beginning of the list
860 return ComponentInfo(
861 type: self.getType(),
862 id: self.id(),
863 innerComponents: inner
864 )
865 }
866 /// Convenience method issuing a Sink allowing for deposits to this AutoBalancer. If the AutoBalancer's
867 /// Capability on itself is not set or is invalid, `nil` is returned.
868 ///
869 /// @return a Sink routing deposits to this AutoBalancer
870 ///
871 access(all) fun createBalancerSink(): {Sink}? {
872 if self._selfCap == nil || !self._selfCap!.check() {
873 return nil
874 }
875 return AutoBalancerSink(autoBalancer: self._selfCap!, uniqueID: self.uniqueID)
876 }
877 /// Convenience method issuing a Source enabling withdrawals from this AutoBalancer. If the AutoBalancer's
878 /// Capability on itself is not set or is invalid, `nil` is returned.
879 ///
880 /// @return a Source routing withdrawals from this AutoBalancer
881 ///
882 access(Get) fun createBalancerSource(): {Source}? {
883 if self._selfCap == nil || !self._selfCap!.check() {
884 return nil
885 }
886 return AutoBalancerSource(autoBalancer: self._selfCap!, uniqueID: self.uniqueID)
887 }
888 /// A setter enabling an AutoBalancer to set a Sink to which overflow value should be deposited
889 ///
890 /// @param sink: The optional Sink DeFiActions connector from which funds are sourced when this AutoBalancer
891 /// current value rises above the upper threshold relative to its valueOfDeposits(). If `nil`, overflown
892 /// value will not rebalance
893 ///
894 access(Set) fun setSink(_ sink: {Sink}?, updateSinkID: Bool) {
895 if sink != nil && updateSinkID {
896 let toUpdate = &sink! as auth(Extend) &{IdentifiableStruct}
897 let toAlign = &self as auth(Identify) &{IdentifiableResource}
898 DeFiActions.alignID(toUpdate: toUpdate, with: toAlign)
899 }
900 self._rebalanceSink = sink
901 }
902 /// A setter enabling an AutoBalancer to set a Source from which underflow value should be withdrawn
903 ///
904 /// @param source: The optional Source DeFiActions connector from which funds are sourced when this AutoBalancer
905 /// current value falls below the lower threshold relative to its valueOfDeposits(). If `nil`, underflown
906 /// value will not rebalance
907 ///
908 access(Set) fun setSource(_ source: {Source}?, updateSourceID: Bool) {
909 if source != nil && updateSourceID {
910 let toUpdate = &source! as auth(Extend) &{IdentifiableStruct}
911 let toAlign = &self as auth(Identify) &{IdentifiableResource}
912 DeFiActions.alignID(toUpdate: toUpdate, with: toAlign)
913 }
914 self._rebalanceSource = source
915 }
916 /// Enables the setting of a Capability on the AutoBalancer for the distribution of Sinks & Sources targeting
917 /// the AutoBalancer instance. Due to the mechanisms of Capabilities, this must be done after the AutoBalancer
918 /// has been saved to account storage and an authorized Capability has been issued.
919 access(Set) fun setSelfCapability(_ cap: Capability<auth(FungibleToken.Withdraw, FlowTransactionScheduler.Execute) &AutoBalancer>) {
920 pre {
921 self._selfCap == nil || self._selfCap!.check() != true:
922 "Internal AutoBalancer Capability has been set and is still valid - cannot be re-assigned"
923 cap.check(): "Invalid AutoBalancer Capability provided"
924 self.getType() == cap.borrow()!.getType() && self.uuid == cap.borrow()!.uuid:
925 "Provided Capability does not target this AutoBalancer of type \(self.getType().identifier) with UUID \(self.uuid) - "
926 .concat("provided Capability for AutoBalancer of type \(cap.borrow()!.getType().identifier) with UUID \(cap.borrow()!.uuid)")
927 }
928 self._selfCap = cap
929 }
930 /// Sets the rebalance range of this AutoBalancer
931 ///
932 /// @param range: a sorted array containing lower and upper thresholds that condition rebalance execution. The
933 /// thresholds must be values such that 0.01 <= range[0] < 1.0 && 1.0 < range[1] < 2.0
934 ///
935 access(Set) fun setRebalanceRange(_ range: [UFix64; 2]) {
936 pre {
937 range[0] < range[1] && 0.01 <= range[0] && range[0] < 1.0 && 1.0 < range[1] && range[1] < 2.0:
938 "Invalid rebalanceRange [lower, upper]: [\(range[0]), \(range[1])] - thresholds must be set such that 0.01 <= range[0] < 1.0 and 1.0 < range[1] < 2.0 relative to value of deposits"
939 }
940 self._rebalanceRange = range
941 }
942 /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
943 /// a DeFiActions stack. See DeFiActions.align() for more information.
944 access(contract) view fun copyID(): UniqueIdentifier? {
945 return self.uniqueID
946 }
947 /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
948 /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
949 access(contract) fun setID(_ id: UniqueIdentifier?) {
950 self.uniqueID = id
951 }
952 /// Returns the timestamp of the last rebalance
953 ///
954 /// @return the timestamp of the last rebalance
955 ///
956 access(all) view fun getLastRebalanceTimestamp(): UFix64 {
957 return self._lastRebalanceTimestamp
958 }
959 /// Allows for external parties to call on the AutoBalancer and execute a rebalance according to it's rebalance
960 /// parameters. This method must be called by external party regularly in order for rebalancing to occur.
961 ///
962 /// @param force: if false, rebalance will occur only when beyond upper or lower thresholds; if true, rebalance
963 /// will execute as long as a price is available via the oracle and the current value is non-zero
964 ///
965 access(Auto) fun rebalance(force: Bool) {
966 self._lastRebalanceTimestamp = getCurrentBlock().timestamp
967
968 let currentPrice = self._oracle.price(ofToken: self._vaultType)
969 if currentPrice == nil {
970 return // no price available -> do nothing
971 }
972 let currentValue = self.currentValue()!
973 // calculate the difference between the current value and the historical value of deposits
974 var valueDiff: UFix64 = currentValue < self._valueOfDeposits ? self._valueOfDeposits - currentValue : currentValue - self._valueOfDeposits
975 // if deficit detected, choose lower threshold, otherwise choose upper threshold
976 let isDeficit = currentValue < self._valueOfDeposits
977 let threshold = isDeficit ? (1.0 - self._rebalanceRange[0]) : (self._rebalanceRange[1] - 1.0)
978
979 if currentPrice == 0.0 || valueDiff == 0.0 || ((valueDiff / self._valueOfDeposits) < threshold && !force) {
980 // division by zero, no difference, or difference does not exceed rebalance ratio & not forced -> no-op
981 return
982 }
983
984 let vault = self._borrowVault()
985 var amount = self.toUFix64(UFix128(valueDiff) / UFix128(currentPrice!))
986 var executed = false
987 let maybeRebalanceSource = &self._rebalanceSource as auth(FungibleToken.Withdraw) &{Source}?
988 let maybeRebalanceSink = &self._rebalanceSink as &{Sink}?
989 if isDeficit && maybeRebalanceSource != nil {
990 // rebalance back up to baseline sourcing funds from _rebalanceSource
991 let depositVault <- maybeRebalanceSource!.withdrawAvailable(maxAmount: amount)
992 amount = depositVault.balance // update the rebalanced amount based on actual deposited amount
993 vault.deposit(from: <-depositVault)
994 executed = true
995 } else if !isDeficit && maybeRebalanceSink != nil {
996 // rebalance back down to baseline depositing excess to _rebalanceSink
997 if amount > vault.balance {
998 amount = vault.balance // protect underflow
999 }
1000 let surplus <- vault.withdraw(amount: amount)
1001 maybeRebalanceSink!.depositCapacity(from: &surplus as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
1002 executed = true
1003 if surplus.balance == 0.0 {
1004 Burner.burn(<-surplus) // could destroy
1005 } else {
1006 amount = amount - surplus.balance // update the rebalanced amount
1007 valueDiff = valueDiff - (surplus.balance * currentPrice!) // update the value difference
1008 vault.deposit(from: <-surplus) // deposit any excess not taken by the Sink
1009 }
1010 }
1011 // emit event only if rebalance was executed
1012 if executed {
1013 emit Rebalanced(
1014 amount: amount,
1015 value: valueDiff,
1016 unitOfAccount: self.unitOfAccount().identifier,
1017 isSurplus: !isDeficit,
1018 vaultType: self.vaultType().identifier,
1019 vaultUUID: self._borrowVault().uuid,
1020 balancerUUID: self.uuid,
1021 address: self.owner?.address,
1022 uniqueID: self.id()
1023 )
1024 }
1025 }
1026
1027 /* FlowTransactionScheduler.TransactionHandler conformance & related logic */
1028
1029 /// Intended to be used by the FlowTransactionScheduler to execute the rebalance.
1030 ///
1031 /// NOTE: if transactions are scheduled externally, they will not automatically schedule the next execution even
1032 /// if the AutoBalancer is configured as recurring. This enables external parties to schedule transactions
1033 /// independently as either one-offs or manage recurring schedules by their own means.
1034 ///
1035 /// @param id: The id of the scheduled transaction
1036 /// @param data: The data that was passed when the transaction was originally scheduled
1037 ///
1038 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
1039 // execute as declared, otherwise execute as currently configured, otherwise default to false
1040 let dataDict = data as? {String: AnyStruct} ?? {}
1041 let force = dataDict["force"] as? Bool ?? self._recurringConfig?.forceRebalance as? Bool ?? false
1042
1043 self.rebalance(force: force)
1044
1045 // If configured as recurring, schedule the next execution only if this is an internally-managed
1046 // scheduled transaction. Externally-scheduled transactions are treated as "fire once" to support
1047 // external scheduling logic that manages its own recurring behavior.
1048 if self._recurringConfig != nil {
1049 let isInternallyManaged = self.borrowScheduledTransaction(id: id) != nil
1050 if isInternallyManaged {
1051 let err = self.scheduleNextRebalance(whileExecuting: id)
1052 if err != nil {
1053 emit FailedRecurringSchedule(
1054 whileExecuting: id,
1055 balancerUUID: self.uuid,
1056 address: self.owner?.address,
1057 error: err!,
1058 uniqueID: self.uniqueID?.id
1059 )
1060 }
1061 }
1062 }
1063 // clean up internally-managed historical scheduled transactions
1064 self._cleanupScheduledTransactions()
1065 }
1066 /// Schedules the next execution of the rebalance if the AutoBalancer is configured as such and there is not
1067 /// already a scheduled transaction within the desired interval. This method is written to fail as gracefully as
1068 /// possible, reporting any failures to schedule the next execution to the as an event. This allows
1069 /// `executeTransaction` to continue execution even if the next execution cannot be scheduled while still
1070 /// informing of the failure via `FailedRecurringSchedule` event.
1071 ///
1072 /// @param whileExecuting: The ID of the transaction that is currently executing or nil if called externally
1073 ///
1074 /// @return String?: The error message, or nil if the next execution was scheduled
1075 ///
1076 access(Schedule) fun scheduleNextRebalance(whileExecuting: UInt64?): String? {
1077 // get the next execution timestamp
1078 var timestamp = self.calculateNextExecutionTimestampAsConfigured()
1079 // perform pre-flight checks before estimating the transaction fees
1080 var errorMessage: String? = nil
1081 if self._recurringConfig == nil {
1082 errorMessage = "MISSING_RECURRING_CONFIG"
1083 } else if timestamp == nil {
1084 errorMessage = "NEXT_EXECUTION_TIMESTAMP_UNAVAILABLE"
1085 } else if self._selfCap?.check() != true {
1086 errorMessage = "INVALID_SELF_CAPABILITY"
1087 }
1088 if errorMessage != nil {
1089 return errorMessage
1090 }
1091
1092 // check for other scheduled transactions within the desired interval
1093 for id in self._scheduledTransactions.keys {
1094 if id == whileExecuting {
1095 continue
1096 }
1097 let scheduledTxn = self.borrowScheduledTransaction(id: id)!
1098 if scheduledTxn.status() == FlowTransactionScheduler.Status.Scheduled {
1099 // found another scheduled transaction within the configured interval
1100 if scheduledTxn.timestamp <= timestamp! {
1101 return nil
1102 }
1103 }
1104 }
1105
1106 // fallback in event there was an issue with assigning the last rebalance timestamp or last rebalance was
1107 // executed long ago
1108 let config = self._recurringConfig!
1109 let now = getCurrentBlock().timestamp
1110 if timestamp! < now {
1111 // protect overflow & update timestamp value
1112 if UInt64(UFix64.max) - UInt64(now) < UInt64(config.interval) {
1113 return "INTERVAL_OVERFLOW"
1114 }
1115 timestamp = now + UFix64(config.interval)
1116 }
1117
1118 // estimate the transaction fees
1119 let estimate = FlowTransactionScheduler.estimate(
1120 data: config.forceRebalance,
1121 timestamp: timestamp!,
1122 priority: config.priority,
1123 executionEffort: config.executionEffort
1124 )
1125 // post-estimate check if the estimate is valid & that the funder has enough funds of the correct type
1126 // NOTE: low priority estimates always receive non-nil errors but are still valid if fee is also non-nil
1127 if config.txnFunder.getSourceType() != Type<@FlowToken.Vault>() {
1128 return "INVALID_FEE_TYPE"
1129 }
1130 if estimate.flowFee == nil {
1131 return estimate.error ?? "ESTIMATE_FAILED"
1132 }
1133 if config.txnFunder.minimumAvailable() < (estimate.flowFee! * 1.05) {
1134 // Check with 5% margin buffer to match withdrawal
1135 return "INSUFFICIENT_FEES_AVAILABLE"
1136 }
1137
1138 // withdraw the fees from the funder with a margin buffer (fee estimation can vary slightly)
1139 // Add 5% margin to handle estimation variance
1140 let feeWithMargin = estimate.flowFee! * 1.05
1141 let fees <- config.txnFunder.withdrawAvailable(maxAmount: feeWithMargin) as! @FlowToken.Vault
1142 if fees.balance < estimate.flowFee! {
1143 config.txnFunder.depositCapacity(from: &fees as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
1144 destroy fees
1145 return "INSUFFICIENT_FEES_PROVIDED"
1146 } else {
1147 // all checks passed - schedule the transaction & capture the scheduled transaction
1148 let txn <- FlowTransactionScheduler.schedule(
1149 handlerCap: self._selfCap!,
1150 data: { "force": config.forceRebalance },
1151 timestamp: timestamp!,
1152 priority: config.priority,
1153 executionEffort: config.executionEffort,
1154 fees: <-fees
1155 )
1156 let txnID = txn.id
1157 self._scheduledTransactions[txnID] <-! txn
1158 return nil
1159 }
1160 }
1161 /// Returns the IDs of the scheduled transactions.
1162 /// NOTE: this does not include externally scheduled transactions
1163 ///
1164 /// @return [UInt64]: The IDs of the scheduled transactions
1165 ///
1166 access(all) view fun getScheduledTransactionIDs(): [UInt64] {
1167 return self._scheduledTransactions.keys
1168 }
1169 /// Borrows a reference to the internally-managed scheduled transaction or nil if not found.
1170 /// NOTE: this does not include externally scheduled transactions
1171 ///
1172 /// @param id: The ID of the scheduled transaction
1173 ///
1174 /// @return &FlowTransactionScheduler.ScheduledTransaction?: The reference to the scheduled transaction, or nil
1175 /// if the scheduled transaction is not found
1176 ///
1177 access(all) view fun borrowScheduledTransaction(id: UInt64): &FlowTransactionScheduler.ScheduledTransaction? {
1178 return &self._scheduledTransactions[id]
1179 }
1180 /// Calculates the next execution timestamp for a recurring rebalance if the AutoBalancer is configured as such.
1181 /// Returns nil if either unconfigured for recurring rebalancing or the interval is greater than the maximum
1182 /// possible timestamp.
1183 ///
1184 /// @return UFix64?: The next execution timestamp, or nil if a recurring rebalance is not configured
1185 ///
1186 access(all) view fun calculateNextExecutionTimestampAsConfigured(): UFix64? {
1187 if let config = self._recurringConfig {
1188 // protect overflow
1189 return (UInt64(UFix64.max) - UInt64(self._lastRebalanceTimestamp)) >= UInt64(config.interval)
1190 ? self._lastRebalanceTimestamp + UFix64(config.interval)
1191 : nil
1192 }
1193 return nil
1194 }
1195 /// Returns the recurring config for the AutoBalancer
1196 ///
1197 /// @return AutoBalancerRecurringConfig?: The recurring config, or nil if recurring rebalancing is not configured
1198 ///
1199 access(all) view fun getRecurringConfig(): AutoBalancerRecurringConfig? {
1200 return self._recurringConfig
1201 }
1202 /// Sets the recurring config for the AutoBalancer
1203 ///
1204 /// @param config: The recurring config to set, or nil to disable recurring rebalancing
1205 ///
1206 access(Configure) fun setRecurringConfig(_ config: AutoBalancerRecurringConfig?) {
1207 pre {
1208 config?.assignedAutoBalancer == nil || config?.assignedAutoBalancer == self.uuid:
1209 "Invalid recurring config - must be assigned to this AutoBalancer"
1210 }
1211 config?.setAssignedAutoBalancer(self.uuid)
1212 self._recurringConfig = config
1213 }
1214 /// Cancels a scheduled transaction returning nil if a scheduled transaction is not found. Refunds are deposited
1215 /// to the configured txn fee funder primarily, returning any excess to the caller.
1216 ///
1217 /// @param id: The ID of the scheduled transaction to cancel
1218 ///
1219 /// @return @FlowToken.Vault?: The refunded vault, or nil if a scheduled transaction is not found
1220 ///
1221 access(FlowTransactionScheduler.Cancel) fun cancelScheduledTransaction(id: UInt64): @FlowToken.Vault? {
1222 if self._scheduledTransactions[id] == nil {
1223 return nil
1224 }
1225 let txn <- self._scheduledTransactions.remove(key: id)
1226 let refund <- FlowTransactionScheduler.cancel(scheduledTx: <-txn!)
1227 if let config = self._recurringConfig {
1228 config.txnFunder.depositCapacity(from: &refund as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
1229 }
1230 return <- refund
1231 }
1232 /// Cleans up the internally-managed scheduled transactions
1233 access(self) fun _cleanupScheduledTransactions() {
1234 // limit to prevent running into computation limits
1235 let limit = 50
1236 var iter = 0
1237 // iterate over the scheduled transactions and remove those that are not scheduled
1238 for id in self._scheduledTransactions.keys {
1239 iter = iter + 1
1240 if iter > limit {
1241 break
1242 }
1243 let ref = &self._scheduledTransactions[id] as &FlowTransactionScheduler.ScheduledTransaction?
1244 if ref?.status() != FlowTransactionScheduler.Status.Scheduled {
1245 let txn <- self._scheduledTransactions.remove(key: id)
1246 destroy txn
1247 }
1248 }
1249 }
1250
1251 /* ViewResolver.Resolver conformance */
1252
1253 /// Passthrough to inner Vault's view Types adding also the AutoBalancerRecurringConfig type
1254 access(all) view fun getViews(): [Type] {
1255 return [Type<AutoBalancerRecurringConfig>()].concat(self._borrowVault().getViews())
1256 }
1257 /// Passthrough to inner Vault's view resolution serving also the AutoBalancerRecurringConfig type
1258 access(all) fun resolveView(_ view: Type): AnyStruct? {
1259 if view == Type<AutoBalancerRecurringConfig>() {
1260 return self._recurringConfig
1261 } else {
1262 return self._borrowVault().resolveView(view)
1263 }
1264 }
1265
1266 /* FungibleToken.Receiver & .Provider conformance */
1267
1268 /// Only the nested Vault type is supported by this AutoBalancer for deposits & withdrawal for the sake of
1269 /// single asset accounting
1270 access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
1271 return { self.vaultType(): true }
1272 }
1273 /// True if the provided Type is the nested Vault Type, false otherwise
1274 access(all) view fun isSupportedVaultType(type: Type): Bool {
1275 return self.getSupportedVaultTypes()[type] == true
1276 }
1277 /// Passthrough to the inner Vault's isAvailableToWithdraw() method
1278 access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool {
1279 return self._borrowVault().isAvailableToWithdraw(amount: amount)
1280 }
1281 /// Deposits the provided Vault to the nested Vault if it is of the same Type, reverting otherwise. In the
1282 /// process, the current value of the deposited amount (denominated in unitOfAccount) increments the
1283 /// AutoBalancer's baseValue. If a price is not available via the internal PriceOracle, the operation reverts.
1284 access(all) fun deposit(from: @{FungibleToken.Vault}) {
1285 pre {
1286 from.getType() == self.vaultType():
1287 "Invalid Vault type \(from.getType().identifier) deposited - this AutoBalancer only accepts \(self.vaultType().identifier)"
1288 }
1289 // assess value & complete deposit - if none available, revert
1290 let price = self._oracle.price(ofToken: from.getType())
1291 ?? panic("No price available for \(from.getType().identifier) to assess value of deposit")
1292 self._valueOfDeposits = self._valueOfDeposits + (from.balance * price)
1293 self._borrowVault().deposit(from: <-from)
1294 }
1295 /// Returns the requested amount of the nested Vault type, reducing the baseValue by the current value
1296 /// (denominated in unitOfAccount) of the token amount. The AutoBalancer's valueOfDeposits is decremented
1297 /// in proportion to the amount withdrawn relative to the inner Vault's balance
1298 access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} {
1299 pre {
1300 amount <= self.vaultBalance(): "Withdraw amount \(amount) exceeds current vault balance \(self.vaultBalance())"
1301 }
1302 if amount == 0.0 {
1303 return <- self._borrowVault().createEmptyVault()
1304 }
1305
1306 // adjust historical value of deposits proportionate to the amount withdrawn & return withdrawn vault
1307 let amount128 = UFix128(amount)
1308 let vaultBalance128 = UFix128(self.vaultBalance())
1309 let proportion: UFix64 = 1.0 - self.toUFix64(amount128 / vaultBalance128)
1310 let newValue = self._valueOfDeposits * proportion
1311 self._valueOfDeposits = newValue
1312 return <- self._borrowVault().withdraw(amount: amount)
1313 }
1314
1315 /* Burnable.Burner conformance */
1316
1317 /// Executed in Burner.burn(). Passes along the inner vault to be burned, executing the inner Vault's
1318 /// burnCallback() logic
1319 access(contract) fun burnCallback() {
1320 let vault <- self._vault <- nil
1321 Burner.burn(<-vault) // executes the inner Vault's burnCallback()
1322 }
1323
1324 /* Internal */
1325
1326 /// Returns a reference to the inner Vault
1327 access(self) view fun _borrowVault(): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} {
1328 return (&self._vault)!
1329 }
1330 /// Returns a reference to the inner Vault
1331 access(self) view fun _borrowOracle(): &{PriceOracle} {
1332 return &self._oracle
1333 }
1334 /// Returns a reference to the inner Vault
1335 access(self) view fun _borrowSink(): &{Sink}? {
1336 return &self._rebalanceSink
1337 }
1338 /// Returns a reference to the inner Source
1339 access(self) view fun _borrowSource(): auth(FungibleToken.Withdraw) &{Source}? {
1340 return &self._rebalanceSource
1341 }
1342 /// Converts a UFix128 to a UFix64, rounding up if the remainder is greater than or equal to 0.5
1343 access(all) view fun toUFix64(_ value: UFix128): UFix64 {
1344 let truncated: UFix64 = UFix64(value)
1345 let truncatedAs128: UFix128 = UFix128(truncated)
1346 let remainder: UFix128 = value - truncatedAs128
1347 let ufix64Step: UFix128 = 0.00000001
1348 let ufix64HalfStep: UFix128 = ufix64Step / UFix128(2.0)
1349
1350 if remainder == UFix128(0.0) {
1351 return truncated
1352 }
1353
1354 view fun roundUp(_ base: UFix64): UFix64 {
1355 let increment: UFix64 = 0.00000001
1356 return base >= UFix64.max - increment ? UFix64.max : base + increment
1357 }
1358
1359 return remainder >= ufix64HalfStep ? roundUp(truncated) : truncated
1360 }
1361 }
1362
1363 /* --- PUBLIC METHODS --- */
1364
1365 /// Returns an AutoBalancer wrapping the provided Vault.
1366 ///
1367 /// @param oracle: The oracle used to query deposited & withdrawn value and to determine if a rebalance should execute
1368 /// @param vault: The Vault wrapped by the AutoBalancer
1369 /// @param rebalanceRange: The percentage range from the AutoBalancer's base value at which a rebalance is executed
1370 /// @param outSink: An optional DeFiActions Sink to which excess value is directed when rebalancing
1371 /// @param inSource: An optional DeFiActions Source from which value is withdrawn to the inner vault when rebalancing
1372 /// @param uniqueID: An optional DeFiActions UniqueIdentifier used for identifying rebalance events
1373 ///
1374 access(all) fun createAutoBalancer(
1375 oracle: {PriceOracle},
1376 vaultType: Type,
1377 lowerThreshold: UFix64,
1378 upperThreshold: UFix64,
1379 rebalanceSink: {Sink}?,
1380 rebalanceSource: {Source}?,
1381 recurringConfig: AutoBalancerRecurringConfig?,
1382 uniqueID: UniqueIdentifier?
1383 ): @AutoBalancer {
1384 let ab <- create AutoBalancer(
1385 lower: lowerThreshold,
1386 upper: upperThreshold,
1387 oracle: oracle,
1388 vaultType: vaultType,
1389 outSink: rebalanceSink,
1390 inSource: rebalanceSource,
1391 recurringConfig: recurringConfig,
1392 uniqueID: uniqueID
1393 )
1394 return <- ab
1395 }
1396
1397 /// Creates a new UniqueIdentifier used for identifying action stacks
1398 ///
1399 /// @return a new UniqueIdentifier
1400 ///
1401 access(all) fun createUniqueIdentifier(): UniqueIdentifier {
1402 let id = UniqueIdentifier(self.currentID, self.authTokenCap)
1403 self.currentID = self.currentID + 1
1404 return id
1405 }
1406
1407 /// Derives the path identifier for an AutoBalancer for a given vault type
1408 access(all) view fun deriveAutoBalancerPathIdentifier(vaultType: Type): String? {
1409 if !vaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()) {
1410 return nil
1411 }
1412 return "DeFiActionAutoBalancer_".concat(vaultType.identifier)
1413 }
1414
1415 /// Aligns the UniqueIdentifier of the provided component with the provided component, setting the UniqueIdentifier of
1416 /// the provided component to the UniqueIdentifier of the provided component. Parameters are AnyStruct to allow for
1417 /// alignment of both IdentifiableStruct and IdentifiableResource. However, note that the provided component must
1418 /// be an auth(Extend) &{IdentifiableStruct} or auth(Extend) &{IdentifiableResource} to be aligned.
1419 ///
1420 /// @param toUpdate: The component to update the UniqueIdentifier of. Must be an auth(Extend) &{IdentifiableStruct}
1421 /// or auth(Extend) &{IdentifiableResource}
1422 /// @param with: The component to align the UniqueIdentifier of the provided component with. Must be an
1423 /// auth(Identify) &{IdentifiableStruct} or auth(Identify) &{IdentifiableResource}
1424 ///
1425 access(all) fun alignID(toUpdate: AnyStruct, with: AnyStruct) {
1426 let maybeISToUpdate = toUpdate as? auth(Extend) &{IdentifiableStruct}
1427 let maybeIRToUpdate = toUpdate as? auth(Extend) &{IdentifiableResource}
1428 let maybeISWith = with as? auth(Identify) &{IdentifiableStruct}
1429 let maybeIRWith = with as? auth(Identify) &{IdentifiableResource}
1430
1431 if maybeISToUpdate != nil && maybeISWith != nil {
1432 maybeISToUpdate!.setID(maybeISWith!.copyID())
1433 } else if maybeISToUpdate != nil && maybeIRWith != nil {
1434 maybeISToUpdate!.setID(maybeIRWith!.copyID())
1435 } else if maybeIRToUpdate != nil && maybeISWith != nil {
1436 maybeIRToUpdate!.setID(maybeISWith!.copyID())
1437 } else if maybeIRToUpdate != nil && maybeIRWith != nil {
1438 maybeIRToUpdate!.setID(maybeIRWith!.copyID())
1439 }
1440 return
1441 }
1442
1443 /* --- INTERNAL CONDITIONAL EVENT EMITTERS --- */
1444
1445 /// Emits Deposited event if a change in balance is detected
1446 access(self) view fun emitDeposited(
1447 type: String,
1448 beforeBalance: UFix64,
1449 afterBalance: UFix64,
1450 fromUUID: UInt64,
1451 uniqueID: UInt64?,
1452 sinkType: String
1453 ): Bool {
1454 if beforeBalance == afterBalance {
1455 return true
1456 }
1457 emit Deposited(
1458 type: type,
1459 amount: beforeBalance > afterBalance ? beforeBalance - afterBalance : afterBalance - beforeBalance,
1460 fromUUID: fromUUID,
1461 uniqueID: uniqueID,
1462 sinkType: sinkType
1463 )
1464 return true
1465 }
1466
1467 /// Emits Withdrawn event if a change in balance is detected
1468 access(self) view fun emitWithdrawn(
1469 type: String,
1470 amount: UFix64,
1471 withdrawnUUID: UInt64,
1472 uniqueID: UInt64?,
1473 sourceType: String
1474 ): Bool {
1475 if amount == 0.0 {
1476 return true
1477 }
1478 emit Withdrawn(
1479 type: type,
1480 amount: amount,
1481 withdrawnUUID: withdrawnUUID,
1482 uniqueID: uniqueID,
1483 sourceType: sourceType
1484 )
1485 return true
1486 }
1487
1488 /// Emits Aligned event if a change in UniqueIdentifier is detected
1489 access(self) view fun emitUpdatedID(
1490 oldID: UInt64?,
1491 newID: UInt64?,
1492 component: String,
1493 uuid: UInt64?
1494 ): Bool {
1495 if oldID == newID {
1496 return true
1497 }
1498 emit UpdatedID(
1499 oldID: oldID,
1500 newID: newID,
1501 component: component,
1502 uuid: uuid
1503 )
1504 return true
1505 }
1506
1507 init() {
1508 self.currentID = 0
1509 self.AuthTokenStoragePath = /storage/authToken
1510
1511 self.account.storage.save(<-create AuthenticationToken(), to: self.AuthTokenStoragePath)
1512 self.authTokenCap = self.account.capabilities.storage.issue<auth(Identify) &AuthenticationToken>(self.AuthTokenStoragePath)
1513
1514 assert(self.authTokenCap.check(), message: "Failed to issue AuthenticationToken Capability")
1515 }
1516}
1517