Smart Contract

FlowYieldVaultsSchedulerV1

A.b1d63873c3cc9f79.FlowYieldVaultsSchedulerV1

Valid From

143,217,045

Deployed

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

Dependents

9 imports
1// standards
2import FungibleToken from 0xf233dcee88fe0abe
3import FlowToken from 0x1654653399040a61
4// Flow system contracts
5import FlowTransactionScheduler from 0xe467b9dd11fa00df
6// DeFiActions
7import DeFiActions from 0x6d888f175c158410
8// Registry storage (separate contract)
9import FlowYieldVaultsSchedulerRegistry from 0xb1d63873c3cc9f79
10// AutoBalancer management (for detecting stuck yield vaults)
11import FlowYieldVaultsAutoBalancers from 0xb1d63873c3cc9f79
12
13/// FlowYieldVaultsScheduler
14///
15/// This contract provides the Supervisor for recovery of stuck AutoBalancers.
16///
17/// Architecture:
18/// - AutoBalancers are configured with recurringConfig at creation in FlowYieldVaultsStrategies
19/// - AutoBalancers self-schedule subsequent executions via their native mechanism
20/// - FlowYieldVaultsAutoBalancers handles registration with the registry and starts scheduling
21/// - The Supervisor is a recovery mechanism for AutoBalancers that fail to self-schedule
22///
23/// Key Features:
24/// - Supervisor detects stuck yield vaults (failed to self-schedule) and recovers them
25/// - Uses Schedule capability to directly call AutoBalancer.scheduleNextRebalance()
26/// - Query and estimation functions for scripts
27///
28access(all) contract FlowYieldVaultsSchedulerV1 {
29
30    /* --- FIELDS --- */
31
32    /// Default recurring interval in seconds (used when not specified)
33    access(all) var DEFAULT_RECURRING_INTERVAL: UFix64
34
35    /// Default priority for recurring schedules
36    access(all) var DEFAULT_PRIORITY: UInt8  // 1 = Medium
37
38    /// Default execution effort for scheduled transactions
39    access(all) var DEFAULT_EXECUTION_EFFORT: UInt64
40
41    /// Minimum fee fallback when estimation returns nil
42    access(all) var MIN_FEE_FALLBACK: UFix64
43
44    /// Fee margin multiplier to add buffer to estimated fees (1.2 = 20% buffer)
45    access(all) var FEE_MARGIN_MULTIPLIER: UFix64
46
47    /* --- PATHS --- */
48
49    /// Storage path for the Supervisor resource
50    access(all) let SupervisorStoragePath: StoragePath
51
52    /* --- EVENTS --- */
53
54    /// Emitted when the Supervisor successfully recovers a stuck yield vault
55    access(all) event YieldVaultRecovered(
56        yieldVaultID: UInt64
57    )
58
59    /// Emitted when Supervisor fails to recover a yield vault
60    access(all) event YieldVaultRecoveryFailed(
61        yieldVaultID: UInt64,
62        error: String
63    )
64
65    /// Emitted when Supervisor detects a stuck yield vault via state-based scanning
66    access(all) event StuckYieldVaultDetected(
67        yieldVaultID: UInt64
68    )
69
70    /// Emitted when Supervisor self-reschedules
71    access(all) event SupervisorRescheduled(
72        scheduledTransactionID: UInt64,
73        timestamp: UFix64
74    )
75
76    /// Emitted when Supervisor fails to self-reschedule.
77    ///
78    /// This is primarily used to surface insufficient fee vault balance, which would otherwise
79    /// cause the Supervisor to stop monitoring without any on-chain signal.
80    access(all) event SupervisorRescheduleFailed(
81        timestamp: UFix64,
82        requiredFee: UFix64?,
83        availableBalance: UFix64?,
84        error: String
85    )
86
87    /// Entitlement to schedule transactions
88    access(all) entitlement Schedule
89
90    /* --- RESOURCES --- */
91
92    access(all) entitlement Configure
93
94    /// Supervisor - The recovery mechanism for stuck AutoBalancers
95    ///
96    /// The Supervisor:
97    /// - Detects stuck yield vaults (AutoBalancers that failed to self-schedule)
98    /// - Recovers stuck yield vaults by directly calling scheduleNextRebalance() via Schedule capability
99    /// - Can self-reschedule for perpetual operation
100    ///
101    /// Primary scheduling is done by AutoBalancers themselves via their native recurringConfig.
102    /// The Supervisor is only for recovery when that fails.
103    ///
104    access(all) resource Supervisor: FlowTransactionScheduler.TransactionHandler {
105        /// Capability to withdraw FLOW for Supervisor's own scheduling fees
106        access(self) let feesCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
107        /// Internally managed scheduled transaction for Supervisor self-rescheduling
108        access(self) var _scheduledTransaction: @FlowTransactionScheduler.ScheduledTransaction?
109
110        init(
111            feesCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
112        ) {
113            self.feesCap = feesCap
114            self._scheduledTransaction <- nil
115        }
116
117        /// Returns the ID of the internally managed scheduled transaction, or nil if not scheduled
118        ///
119        /// @return UInt64?: The ID of the internally managed scheduled transaction, or nil if not scheduled
120        access(all) view fun getScheduledTransactionID(): UInt64? {
121            return self._scheduledTransaction?.id
122        }
123
124        /* --- CONFIGURE FUNCTIONS --- */
125
126        /// Sets the default recurring interval for Supervisor self-rescheduling
127        /// @param interval: The interval to set
128        access(Configure) fun setDefaultRecurringInterval(_ interval: UFix64) {
129            FlowYieldVaultsSchedulerV1.DEFAULT_RECURRING_INTERVAL = interval
130        }
131
132        /// Sets the default execution effort for Supervisor self-rescheduling
133        /// @param effort: The execution effort to set
134        access(Configure) fun setDefaultExecutionEffort(_ effort: UInt64) {
135            FlowYieldVaultsSchedulerV1.DEFAULT_EXECUTION_EFFORT = effort
136        }
137
138        /// Sets the default minimum fee fallback for Supervisor self-rescheduling
139        /// @param fallback: The minimum fee fallback to set
140        access(Configure) fun setDefaultMinFeeFallback(_ fallback: UFix64) {
141            FlowYieldVaultsSchedulerV1.MIN_FEE_FALLBACK = fallback
142        }
143
144        /// Sets the default fee margin multiplier for Supervisor self-rescheduling
145        /// TODO: Determine if this field is even necessary
146        /// @param marginMultiplier: The margin multiplier to set
147        access(Configure) fun setDefaultFeeMarginMultiplier(_ marginMultiplier: UFix64) {
148            FlowYieldVaultsSchedulerV1.FEE_MARGIN_MULTIPLIER = marginMultiplier
149        }
150
151        /// Sets the default priority for Supervisor self-rescheduling
152        ///
153        /// @param priority: The priority to set
154        access(Configure) fun setDefaultPriority(_ priority: FlowTransactionScheduler.Priority) {
155            FlowYieldVaultsSchedulerV1.DEFAULT_PRIORITY = priority.rawValue
156        }
157
158        /* --- TRANSACTION HANDLER --- */
159
160        /// Detects and recovers stuck yield vaults by directly calling their scheduleNextRebalance().
161        ///
162        /// Detection methods:
163        /// 1. State-based: Scans for registered yield vaults with no active schedule that are overdue
164        ///
165        /// Recovery method:
166        /// - Uses Schedule capability to call AutoBalancer.scheduleNextRebalance() directly
167        /// - The AutoBalancer schedules itself using its own fee source
168        /// - This is simpler than the previous approach of Supervisor scheduling on behalf of AutoBalancer
169        ///
170        /// data accepts optional config:
171        /// {
172        ///   "priority": UInt8 (0=High,1=Medium,2=Low) - for Supervisor self-rescheduling
173        ///   "executionEffort": UInt64 - for Supervisor self-rescheduling
174        ///   "recurringInterval": UFix64 (for Supervisor self-rescheduling)
175        ///   "scanForStuck": Bool (default true - scan all registered yield vaults for stuck ones)
176        /// }
177        access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
178            let cfg = data as? {String: AnyStruct} ?? {}
179            let priorityRaw = cfg["priority"] as? UInt8 ?? FlowYieldVaultsSchedulerV1.DEFAULT_PRIORITY
180            let executionEffort = cfg["executionEffort"] as? UInt64 ?? FlowYieldVaultsSchedulerV1.DEFAULT_EXECUTION_EFFORT
181            let recurringInterval = cfg["recurringInterval"] as? UFix64
182            let scanForStuck = cfg["scanForStuck"] as? Bool ?? true
183
184            let priority = FlowTransactionScheduler.Priority(rawValue: priorityRaw)
185                ?? FlowTransactionScheduler.Priority.Medium
186
187            // STEP 1: State-based detection - scan for stuck yield vaults
188            if scanForStuck {
189                // TODO: add pagination - this will inevitably fails and at minimum creates inconsistent execution
190                //      effort between runs
191                let registeredYieldVaults = FlowYieldVaultsSchedulerRegistry.getRegisteredYieldVaultIDs()
192                var scanned = 0
193                for yieldVaultID in registeredYieldVaults {
194                    if scanned >= FlowYieldVaultsSchedulerRegistry.MAX_BATCH_SIZE {
195                        break
196                    }
197                    scanned = scanned + 1
198                    
199                    // Skip if already in pending queue
200                    // TODO: This is extremely inefficient - accessing from mapping is preferrable to iterating over
201                    //      an array
202                    if FlowYieldVaultsSchedulerRegistry.getPendingYieldVaultIDs().contains(yieldVaultID) {
203                        continue
204                    }
205
206                    // Check if yield vault is stuck (has recurring config, no active schedule, overdue)
207                    if FlowYieldVaultsAutoBalancers.isStuckYieldVault(id: yieldVaultID) {
208                        FlowYieldVaultsSchedulerRegistry.enqueuePending(yieldVaultID: yieldVaultID)
209                        emit StuckYieldVaultDetected(yieldVaultID: yieldVaultID)
210                    }
211                }
212            }
213
214            // STEP 2: Process pending yield vaults - recover them via Schedule capability
215            let pendingYieldVaults = FlowYieldVaultsSchedulerRegistry.getPendingYieldVaultIDsPaginated(page: 0, size: nil)
216            
217            for yieldVaultID in pendingYieldVaults {
218                // Get Schedule capability for this yield vault
219                let scheduleCap = FlowYieldVaultsSchedulerRegistry.getScheduleCap(yieldVaultID: yieldVaultID)
220                if scheduleCap == nil || !scheduleCap!.check() {
221                    emit YieldVaultRecoveryFailed(yieldVaultID: yieldVaultID, error: "Invalid Schedule capability")
222                    continue
223                }
224
225                // Borrow the AutoBalancer and call scheduleNextRebalance() directly
226                let autoBalancerRef = scheduleCap!.borrow()!
227
228                if let scheduleError = autoBalancerRef.scheduleNextRebalance(whileExecuting: nil) {
229                    emit YieldVaultRecoveryFailed(yieldVaultID: yieldVaultID, error: scheduleError)
230                    // Leave in pending queue for retry on next Supervisor run
231                    continue
232                }
233
234                // Successfully recovered - dequeue from pending
235                FlowYieldVaultsSchedulerRegistry.dequeuePending(yieldVaultID: yieldVaultID)
236                emit YieldVaultRecovered(yieldVaultID: yieldVaultID)
237            }
238
239            // STEP 3: Self-reschedule for perpetual operation if configured
240            if let interval = recurringInterval {
241                self.scheduleNextRecurringExecution(
242                    recurringInterval: interval,
243                    priority: priority,
244                    executionEffort: executionEffort,
245                    scanForStuck: scanForStuck
246                )
247            }
248        }
249
250        /// Self-reschedules the Supervisor for perpetual operation.
251        ///
252        /// This function handles the scheduling of the next Supervisor execution,
253        /// including fee estimation, withdrawal, and transaction scheduling.
254        ///
255        /// @param recurringInterval: The interval in seconds until the next execution
256        /// @param priority: The priority level for the scheduled transaction
257        /// @param executionEffort: The execution effort estimate for the transaction
258        /// @param scanForStuck: Whether to scan for stuck yield vaults in the next execution
259        access(Schedule) fun scheduleNextRecurringExecution(
260            recurringInterval: UFix64,
261            priority: FlowTransactionScheduler.Priority,
262            executionEffort: UInt64,
263            scanForStuck: Bool
264        ) {
265            let ref = &self._scheduledTransaction as &FlowTransactionScheduler.ScheduledTransaction?
266
267            if ref?.status() == FlowTransactionScheduler.Status.Scheduled {
268                // already scheduled - do nothing
269                return
270            }
271            let txn <- self._scheduledTransaction <- nil
272            destroy txn
273
274            let nextTimestamp = getCurrentBlock().timestamp + recurringInterval
275
276            let supervisorCap = FlowYieldVaultsSchedulerRegistry.getSupervisorCap()
277            if supervisorCap == nil {
278                emit SupervisorRescheduleFailed(
279                    timestamp: nextTimestamp,
280                    requiredFee: nil,
281                    availableBalance: nil,
282                    error: "Missing Supervisor capability"
283                )
284                return
285            }
286            if !supervisorCap!.check() {
287                emit SupervisorRescheduleFailed(
288                    timestamp: nextTimestamp,
289                    requiredFee: nil,
290                    availableBalance: nil,
291                    error: "Invalid Supervisor capability"
292                )
293                return
294            }
295
296            let est = FlowYieldVaultsSchedulerV1.estimateSchedulingCost(
297                timestamp: nextTimestamp,
298                priority: priority,
299                executionEffort: executionEffort
300            )
301            let baseFee = est.flowFee ?? FlowYieldVaultsSchedulerV1.MIN_FEE_FALLBACK
302            let required = baseFee * FlowYieldVaultsSchedulerV1.FEE_MARGIN_MULTIPLIER
303
304            let vaultRef = self.feesCap.borrow()
305            if vaultRef == nil {
306                emit SupervisorRescheduleFailed(
307                    timestamp: nextTimestamp,
308                    requiredFee: required,
309                    availableBalance: nil,
310                    error: "Could not borrow fee vault"
311                )
312                return
313            }
314            if vaultRef!.balance < required {
315                emit SupervisorRescheduleFailed(
316                    timestamp: nextTimestamp,
317                    requiredFee: required,
318                    availableBalance: vaultRef!.balance,
319                    error: "Insufficient fee vault balance"
320                )
321                return
322            }
323
324            let fees <- vaultRef!.withdraw(amount: required) as! @FlowToken.Vault
325
326            let nextData = {
327                "priority": priority.rawValue,
328                "executionEffort": executionEffort,
329                "recurringInterval": recurringInterval,
330                "scanForStuck": scanForStuck
331            }
332
333            let selfTxn <- FlowTransactionScheduler.schedule(
334                handlerCap: supervisorCap!,
335                data: nextData,
336                timestamp: nextTimestamp,
337                priority: priority,
338                executionEffort: executionEffort,
339                fees: <-fees
340            )
341
342            emit SupervisorRescheduled(
343                scheduledTransactionID: selfTxn.id,
344                timestamp: nextTimestamp
345            )
346
347            self._scheduledTransaction <-! selfTxn
348        }
349
350        /// Cancels the scheduled transaction if it is scheduled.
351        ///
352        /// @param refundReceiver: The receiver of the refunded vault, or nil to deposit to the internal feesCap
353        ///
354        /// @return @FlowToken.Vault?: The refunded vault, or nil if a scheduled transaction is not found
355        access(Schedule) fun cancelScheduledTransaction(refundReceiver: &{FungibleToken.Vault}?): @FlowToken.Vault? {
356            // nothing to cancel - nil or not scheduled
357            if self._scheduledTransaction == nil
358                || self._scheduledTransaction?.status() != FlowTransactionScheduler.Status.Scheduled {
359                return nil
360            }
361            // cancel the scheduled transaction & deposit refund to receiver if provided
362            let txnID = self.getScheduledTransactionID()!
363            let txn <- self._scheduledTransaction <- nil
364            let refund <- FlowTransactionScheduler.cancel(scheduledTx: <-txn!)
365            if let receiver = refundReceiver {
366                receiver.deposit(from: <-refund)
367            } else {
368                let feeReceiver = self.feesCap.borrow()
369                    ?? panic("Could not borrow fees receiver to deposit refund of \(refund.balance) FLOW when cancelling scheduled transaction id \(txnID)")
370                feeReceiver.deposit(from: <-refund)
371            }
372            return nil
373        }
374    }
375
376    /* --- PRIVATE FUNCTIONS --- */
377
378    /// Creates a Supervisor handler.
379    access(self) fun createSupervisor(): @Supervisor {
380        let feesCap = self.account.capabilities.storage
381            .issue<auth(FungibleToken.Withdraw) &FlowToken.Vault>(/storage/flowTokenVault)
382        return <- create Supervisor(feesCap: feesCap)
383    }
384
385    /* --- PUBLIC FUNCTIONS --- */
386
387    /// Estimates the cost of scheduling a transaction at a given timestamp
388    access(all) fun estimateSchedulingCost(
389        timestamp: UFix64,
390        priority: FlowTransactionScheduler.Priority,
391        executionEffort: UInt64
392    ): FlowTransactionScheduler.EstimatedScheduledTransaction {
393        let maximumSizeData: {String: AnyStruct} = {
394            "priority": priority.rawValue,
395            "executionEffort": executionEffort,
396            "recurringInterval": UFix64.max,
397            "scanForStuck": true
398        }
399        return FlowTransactionScheduler.estimate(
400            data: maximumSizeData,
401            timestamp: timestamp,
402            priority: priority,
403            executionEffort: executionEffort
404        )
405    }
406
407    /// Ensures the Supervisor is configured and registered.
408    /// Creates Supervisor if not exists, issues capability, and registers with Registry.
409    /// Note: This is access(all) because the Supervisor is owned by the contract account
410    /// and uses contract account funds. The function is idempotent and safe to call multiple times.
411    access(all) fun ensureSupervisorConfigured() {
412        // Create and save Supervisor if not exists
413        if self.account.storage.type(at: self.SupervisorStoragePath) == nil {
414            let supervisor <- self.createSupervisor()
415            self.account.storage.save(<-supervisor, to: self.SupervisorStoragePath)
416        }
417
418        // Check if Supervisor capability is already registered
419        if FlowYieldVaultsSchedulerRegistry.getSupervisorCap() != nil {
420            return
421        }
422
423        // Issue capability and register
424        let cap = self.account.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(
425            self.SupervisorStoragePath
426        )
427        FlowYieldVaultsSchedulerRegistry.setSupervisorCap(cap: cap)
428    }
429
430    /* --- ACCOUNT FUNCTIONS --- */
431
432    /// Borrows the Supervisor reference (account-restricted for internal use)
433    access(account) fun borrowSupervisor(): &Supervisor? {
434        return self.account.storage.borrow<&Supervisor>(from: self.SupervisorStoragePath)
435    }
436
437    /// Manually enqueues a registered yield vault to the pending queue for recovery.
438    /// This allows manual triggering of recovery for a specific yield vault.
439    ///
440    /// @param yieldVaultID: The ID of the registered yield vault to enqueue
441    ///
442    access(account) fun enqueuePendingYieldVault(yieldVaultID: UInt64) {
443        assert(
444            FlowYieldVaultsSchedulerRegistry.isRegistered(yieldVaultID: yieldVaultID),
445            message: "enqueuePendingYieldVault: YieldVault #\(yieldVaultID.toString()) is not registered"
446        )
447        FlowYieldVaultsSchedulerRegistry.enqueuePending(yieldVaultID: yieldVaultID)
448    }
449
450    init() {
451        // Initialize constants
452        self.DEFAULT_RECURRING_INTERVAL = 60.0 * 10.0  // 10 minutes
453        self.DEFAULT_PRIORITY = 1  // Medium
454        self.DEFAULT_EXECUTION_EFFORT = 800
455        self.MIN_FEE_FALLBACK = 0.00005
456        self.FEE_MARGIN_MULTIPLIER = 1.0
457
458        // Initialize paths
459        self.SupervisorStoragePath = /storage/FlowYieldVaultsSupervisor
460        
461        // Configure Supervisor at deploy time
462        self.ensureSupervisorConfigured()
463    }
464}
465