Smart Contract

FlowYieldVaultsAutoBalancers

A.b1d63873c3cc9f79.FlowYieldVaultsAutoBalancers

Valid From

136,355,574

Deployed

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

Dependents

4 imports
1// standards
2import Burner from 0xf233dcee88fe0abe
3import FungibleToken from 0xf233dcee88fe0abe
4// DeFiActions
5import DeFiActions from 0x6d888f175c158410
6import FlowTransactionScheduler from 0xe467b9dd11fa00df
7// Registry for global yield vault mapping
8import FlowYieldVaultsSchedulerRegistry from 0xb1d63873c3cc9f79
9
10/// FlowYieldVaultsAutoBalancers
11///
12/// This contract deals with the storage, retrieval and cleanup of DeFiActions AutoBalancers as they are used in
13/// FlowYieldVaults defined Strategies.
14///
15/// AutoBalancers are stored in contract account storage at paths derived by their related DeFiActions.UniqueIdentifier.id
16/// which identifies all DeFiActions components in the stack related to their composite Strategy.
17///
18/// When a YieldVault and necessarily the related Strategy is closed & burned, the related AutoBalancer and its Capabilities
19/// are destroyed and deleted.
20///
21/// Scheduling approach:
22/// - AutoBalancers are configured with a recurringConfig at creation
23/// - After creation, scheduleNextRebalance(nil) starts the self-scheduling chain
24/// - The registry tracks all live yield vault IDs for global mapping
25/// - Cleanup unregisters from the registry
26///
27access(all) contract FlowYieldVaultsAutoBalancers {
28
29    /// The path prefix used for StoragePath & PublicPath derivations
30    access(all) let pathPrefix: String
31
32    /* --- PUBLIC METHODS --- */
33
34    /// Returns the path (StoragePath or PublicPath) at which an AutoBalancer is stored with the associated
35    /// UniqueIdentifier.id.
36    access(all) view fun deriveAutoBalancerPath(id: UInt64, storage: Bool): Path {
37        return storage ? StoragePath(identifier: "\(self.pathPrefix)\(id)")! : PublicPath(identifier: "\(self.pathPrefix)\(id)")!
38    }
39
40    /// Returns an unauthorized reference to an AutoBalancer with the given UniqueIdentifier.id value. If none is
41    /// configured, `nil` will be returned.
42    access(all) fun borrowAutoBalancer(id: UInt64): &DeFiActions.AutoBalancer? {
43        let publicPath = self.deriveAutoBalancerPath(id: id, storage: false) as! PublicPath
44        return self.account.capabilities.borrow<&DeFiActions.AutoBalancer>(publicPath)
45    }
46
47    /// Checks if an AutoBalancer has at least one active (Scheduled) transaction.
48    /// Used by Supervisor to detect stuck yield vaults that need recovery.
49    ///
50    /// @param id: The yield vault/AutoBalancer ID
51    /// @return Bool: true if there's at least one Scheduled transaction, false otherwise
52    ///
53    access(all) fun hasActiveSchedule(id: UInt64): Bool {
54        let autoBalancer = self.borrowAutoBalancer(id: id)
55        if autoBalancer == nil {
56            return false
57        }
58        
59        let txnIDs = autoBalancer!.getScheduledTransactionIDs()
60        for txnID in txnIDs {
61            if autoBalancer!.borrowScheduledTransaction(id: txnID)?.status() == FlowTransactionScheduler.Status.Scheduled {
62                return true
63            }
64        }
65        return false
66    }
67
68    /// Checks if an AutoBalancer is overdue for execution.
69    /// A yield vault is considered overdue if:
70    /// - It has a recurring config
71    /// - The next expected execution time has passed
72    /// - It has no active schedule
73    ///
74    /// @param id: The yield vault/AutoBalancer ID
75    /// @return Bool: true if yield vault is overdue and stuck, false otherwise
76    ///
77    access(all) fun isStuckYieldVault(id: UInt64): Bool {
78        let autoBalancer = self.borrowAutoBalancer(id: id)
79        if autoBalancer == nil {
80            return false
81        }
82        
83        // Check if yield vault has recurring config (should be executing periodically)
84        let config = autoBalancer!.getRecurringConfig()
85        if config == nil {
86            return false // Not configured for recurring, can't be "stuck"
87        }
88        
89        // Check if there's an active schedule
90        if self.hasActiveSchedule(id: id) {
91            return false // Has active schedule, not stuck
92        }
93        
94        // Check if yield vault is overdue
95        let nextExpected = autoBalancer!.calculateNextExecutionTimestampAsConfigured()
96        if nextExpected == nil {
97            return true // Can't calculate next time, likely stuck
98        }
99        
100        // If next expected time has passed and no active schedule, yield vault is stuck
101        return nextExpected! < getCurrentBlock().timestamp
102    }
103
104    /* --- INTERNAL METHODS --- */
105
106    /// Configures a new AutoBalancer in storage, configures its public Capability, and sets its inner authorized
107    /// Capability. If an AutoBalancer is stored with an associated UniqueID value, the operation reverts.
108    ///
109    /// @param oracle: The oracle used to query deposited & withdrawn value and to determine if a rebalance should execute
110    /// @param vaultType: The type of Vault wrapped by the AutoBalancer
111    /// @param lowerThreshold: The percentage below base value at which a rebalance pulls from rebalanceSource
112    /// @param upperThreshold: The percentage above base value at which a rebalance pushes to rebalanceSink
113    /// @param rebalanceSink: An optional DeFiActions Sink to which excess value is directed when rebalancing
114    /// @param rebalanceSource: An optional DeFiActions Source from which value is withdrawn when rebalancing
115    /// @param recurringConfig: Optional configuration for automatic recurring rebalancing via FlowTransactionScheduler
116    /// @param uniqueID: The DeFiActions UniqueIdentifier used for identifying this AutoBalancer
117    ///
118    access(account) fun _initNewAutoBalancer(
119        oracle: {DeFiActions.PriceOracle},
120        vaultType: Type,
121        lowerThreshold: UFix64,
122        upperThreshold: UFix64,
123        rebalanceSink: {DeFiActions.Sink}?,
124        rebalanceSource: {DeFiActions.Source}?,
125        recurringConfig: DeFiActions.AutoBalancerRecurringConfig?,
126        uniqueID: DeFiActions.UniqueIdentifier
127    ): auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) &DeFiActions.AutoBalancer {
128
129        // derive paths & prevent collision
130        let storagePath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: true) as! StoragePath
131        let publicPath = self.deriveAutoBalancerPath(id: uniqueID.id, storage: false) as! PublicPath
132        var storedType = self.account.storage.type(at: storagePath)
133        var publishedCap = self.account.capabilities.exists(publicPath)
134        assert(storedType == nil,
135            message: "Storage collision when creating AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(storagePath)")
136        assert(!publishedCap,
137            message: "Published Capability collision found when publishing AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(publicPath)")
138
139        // create & save AutoBalancer with optional recurring config
140        let autoBalancer <- DeFiActions.createAutoBalancer(
141                oracle: oracle,
142                vaultType: vaultType,
143                lowerThreshold: lowerThreshold,
144                upperThreshold: upperThreshold,
145                rebalanceSink: rebalanceSink,
146                rebalanceSource: rebalanceSource,
147                recurringConfig: recurringConfig,
148                uniqueID: uniqueID
149            )
150        self.account.storage.save(<-autoBalancer, to: storagePath)
151        let autoBalancerRef = self._borrowAutoBalancer(uniqueID.id)
152
153        // issue & publish public capability
154        let publicCap = self.account.capabilities.storage.issue<&DeFiActions.AutoBalancer>(storagePath)
155        self.account.capabilities.publish(publicCap, at: publicPath)
156
157        // issue private capability & set within AutoBalancer
158        let authorizedCap = self.account.capabilities.storage.issue<auth(FungibleToken.Withdraw, FlowTransactionScheduler.Execute) &DeFiActions.AutoBalancer>(storagePath)
159        autoBalancerRef.setSelfCapability(authorizedCap)
160
161        // ensure proper configuration before closing
162        storedType = self.account.storage.type(at: storagePath)
163        publishedCap = self.account.capabilities.exists(publicPath)
164        assert(storedType == Type<@DeFiActions.AutoBalancer>(),
165            message: "Error when configuring AutoBalancer for UniqueIdentifier.id \(uniqueID.id) at path \(storagePath)")
166        assert(publishedCap,
167            message: "Error when publishing AutoBalancer Capability for UniqueIdentifier.id \(uniqueID.id) at path \(publicPath)")
168
169        // Issue handler capability for the AutoBalancer (for FlowTransactionScheduler execution)
170        let handlerCap = self.account.capabilities.storage
171            .issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(storagePath)
172
173        // Issue schedule capability for the AutoBalancer (for Supervisor to call scheduleNextRebalance directly)
174        let scheduleCap = self.account.capabilities.storage
175            .issue<auth(DeFiActions.Schedule) &DeFiActions.AutoBalancer>(storagePath)
176
177        // Register yield vault in registry for global mapping of live yield vault IDs
178        FlowYieldVaultsSchedulerRegistry.register(yieldVaultID: uniqueID.id, handlerCap: handlerCap, scheduleCap: scheduleCap)
179
180        // Start the native AutoBalancer self-scheduling chain if recurringConfig was provided
181        // This schedules the first rebalance; subsequent ones are scheduled automatically
182        // by the AutoBalancer after each execution (via recurringConfig)
183        if recurringConfig != nil {
184            let scheduleError = autoBalancerRef.scheduleNextRebalance(whileExecuting: nil)
185            if scheduleError != nil {
186                panic("Failed to schedule first rebalance for AutoBalancer \(uniqueID.id): ".concat(scheduleError!))
187            }
188        }
189
190        return autoBalancerRef
191    }
192
193    /// Returns an authorized reference on the AutoBalancer with the associated UniqueIdentifier.id. If none is found,
194    /// the operation reverts.
195    access(account)
196    fun _borrowAutoBalancer(_ id: UInt64): auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) &DeFiActions.AutoBalancer {
197        let storagePath = self.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
198        return self.account.storage.borrow<auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) &DeFiActions.AutoBalancer>(
199                from: storagePath
200            ) ?? panic("Could not borrow reference to AutoBalancer with UniqueIdentifier.id \(id) from StoragePath \(storagePath)")
201    }
202
203    /// Called by strategies defined in the FlowYieldVaults account which leverage account-hosted AutoBalancers when a
204    /// Strategy is burned
205    access(account) fun _cleanupAutoBalancer(id: UInt64) {
206        // Unregister from registry (removes from global yield vault mapping)
207        FlowYieldVaultsSchedulerRegistry.unregister(yieldVaultID: id)
208
209        let storagePath = self.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
210        let publicPath = self.deriveAutoBalancerPath(id: id, storage: false) as! PublicPath
211        // unpublish the public AutoBalancer Capability
212        let _ = self.account.capabilities.unpublish(publicPath)
213        
214        // Collect controller IDs first (can't modify during iteration)
215        var controllersToDelete: [UInt64] = []
216        self.account.capabilities.storage.forEachController(forPath: storagePath, fun(_ controller: &StorageCapabilityController): Bool {
217            controllersToDelete.append(controller.capabilityID)
218            return true
219        })
220        // Delete controllers after iteration
221        for controllerID in controllersToDelete {
222            if let controller = self.account.capabilities.storage.getController(byCapabilityID: controllerID) {
223                controller.delete()
224            }
225        }
226        
227        // load & burn the AutoBalancer (this also handles any pending scheduled transactions via burnCallback)
228        let autoBalancer <-self.account.storage.load<@DeFiActions.AutoBalancer>(from: storagePath)
229        Burner.burn(<-autoBalancer)
230    }
231
232    init() {
233        self.pathPrefix = "FlowYieldVaultsAutoBalancer_"
234    }
235}
236