Smart Contract

DeFiActions

A.0b11b1848a8aa2c0.DeFiActions

Deployed

3h ago
Feb 26, 2026, 04:02:35 PM UTC

Dependents

0 imports
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