From f817da369efdd7accb4d6c1e66f2450406a028cb Mon Sep 17 00:00:00 2001 From: liobrasil Date: Mon, 2 Feb 2026 16:38:18 -0400 Subject: [PATCH 1/2] fix: emit event on Supervisor reschedule failure --- .../contracts/FlowYieldVaultsSchedulerV1.cdc | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc b/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc index dd15812c..38ea4ab1 100644 --- a/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc +++ b/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc @@ -73,6 +73,17 @@ access(all) contract FlowYieldVaultsSchedulerV1 { timestamp: UFix64 ) + /// Emitted when Supervisor fails to self-reschedule. + /// + /// This is primarily used to surface insufficient fee vault balance, which would otherwise + /// cause the Supervisor to stop monitoring without any on-chain signal. + access(all) event SupervisorRescheduleFailed( + timestamp: UFix64, + requiredFee: UFix64?, + availableBalance: UFix64?, + error: String + ) + /// Entitlement to schedule transactions access(all) entitlement Schedule @@ -213,10 +224,9 @@ access(all) contract FlowYieldVaultsSchedulerV1 { // Borrow the AutoBalancer and call scheduleNextRebalance() directly let autoBalancerRef = scheduleCap!.borrow()! - let scheduleError = autoBalancerRef.scheduleNextRebalance(whileExecuting: nil) - if scheduleError != nil { - emit YieldVaultRecoveryFailed(yieldVaultID: yieldVaultID, error: scheduleError!) + if let scheduleError = autoBalancerRef.scheduleNextRebalance(whileExecuting: nil) { + emit YieldVaultRecoveryFailed(yieldVaultID: yieldVaultID, error: scheduleError) // Leave in pending queue for retry on next Supervisor run continue } @@ -262,11 +272,16 @@ access(all) contract FlowYieldVaultsSchedulerV1 { destroy txn let nextTimestamp = getCurrentBlock().timestamp + recurringInterval - let supervisorCap = FlowYieldVaultsSchedulerRegistry.getSupervisorCap() - - if supervisorCap == nil || !supervisorCap!.check() { - return - } + if let supervisorCap = FlowYieldVaultsSchedulerRegistry.getSupervisorCap() { + if !supervisorCap.check() { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: nil, + availableBalance: nil, + error: "Invalid Supervisor capability" + ) + return + } let est = FlowYieldVaultsSchedulerV1.estimateSchedulingCost( timestamp: nextTimestamp, @@ -275,12 +290,20 @@ access(all) contract FlowYieldVaultsSchedulerV1 { ) let baseFee = est.flowFee ?? FlowYieldVaultsSchedulerV1.MIN_FEE_FALLBACK let required = baseFee * FlowYieldVaultsSchedulerV1.FEE_MARGIN_MULTIPLIER + if let vaultRef = self.feesCap.borrow() { + if vaultRef.balance < required { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: required, + availableBalance: vaultRef.balance, + error: "Insufficient fee vault balance" + ) + return + } - if let vaultRef = self.feesCap.borrow() { - if vaultRef.balance >= required { let fees <- vaultRef.withdraw(amount: required) as! @FlowToken.Vault - let nextData: {String: AnyStruct} = { + let nextData = { "priority": priority.rawValue, "executionEffort": executionEffort, "recurringInterval": recurringInterval, @@ -288,7 +311,7 @@ access(all) contract FlowYieldVaultsSchedulerV1 { } let selfTxn <- FlowTransactionScheduler.schedule( - handlerCap: supervisorCap!, + handlerCap: supervisorCap, data: nextData, timestamp: nextTimestamp, priority: priority, @@ -302,7 +325,23 @@ access(all) contract FlowYieldVaultsSchedulerV1 { ) self._scheduledTransaction <-! selfTxn + } else { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: required, + availableBalance: nil, + error: "Could not borrow fee vault" + ) + return } + } else { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: nil, + availableBalance: nil, + error: "Missing Supervisor capability" + ) + return } } @@ -401,7 +440,7 @@ access(all) contract FlowYieldVaultsSchedulerV1 { access(account) fun enqueuePendingYieldVault(yieldVaultID: UInt64) { assert( FlowYieldVaultsSchedulerRegistry.isRegistered(yieldVaultID: yieldVaultID), - message: "enqueuePendingYieldVault: YieldVault #".concat(yieldVaultID.toString()).concat(" is not registered") + message: "enqueuePendingYieldVault: YieldVault #\(yieldVaultID.toString()) is not registered" ) FlowYieldVaultsSchedulerRegistry.enqueuePending(yieldVaultID: yieldVaultID) } From a7f95116d39b81cb20f6eb4ef846e710895beafe Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 4 Feb 2026 18:45:53 -0400 Subject: [PATCH 2/2] refactor: use early returns in scheduleNextRecurringExecution Replace nested if-else blocks with guard clauses for improved readability. Each failure case now returns immediately, keeping error handling close to the condition that triggers it. Co-Authored-By: Claude Opus 4.5 --- .../contracts/FlowYieldVaultsSchedulerV1.cdc | 114 +++++++++--------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc b/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc index 38ea4ab1..be81a875 100644 --- a/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc +++ b/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc @@ -272,16 +272,26 @@ access(all) contract FlowYieldVaultsSchedulerV1 { destroy txn let nextTimestamp = getCurrentBlock().timestamp + recurringInterval - if let supervisorCap = FlowYieldVaultsSchedulerRegistry.getSupervisorCap() { - if !supervisorCap.check() { - emit SupervisorRescheduleFailed( - timestamp: nextTimestamp, - requiredFee: nil, - availableBalance: nil, - error: "Invalid Supervisor capability" - ) - return - } + + let supervisorCap = FlowYieldVaultsSchedulerRegistry.getSupervisorCap() + if supervisorCap == nil { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: nil, + availableBalance: nil, + error: "Missing Supervisor capability" + ) + return + } + if !supervisorCap!.check() { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: nil, + availableBalance: nil, + error: "Invalid Supervisor capability" + ) + return + } let est = FlowYieldVaultsSchedulerV1.estimateSchedulingCost( timestamp: nextTimestamp, @@ -290,59 +300,51 @@ access(all) contract FlowYieldVaultsSchedulerV1 { ) let baseFee = est.flowFee ?? FlowYieldVaultsSchedulerV1.MIN_FEE_FALLBACK let required = baseFee * FlowYieldVaultsSchedulerV1.FEE_MARGIN_MULTIPLIER - if let vaultRef = self.feesCap.borrow() { - if vaultRef.balance < required { - emit SupervisorRescheduleFailed( - timestamp: nextTimestamp, - requiredFee: required, - availableBalance: vaultRef.balance, - error: "Insufficient fee vault balance" - ) - return - } - - let fees <- vaultRef.withdraw(amount: required) as! @FlowToken.Vault - - let nextData = { - "priority": priority.rawValue, - "executionEffort": executionEffort, - "recurringInterval": recurringInterval, - "scanForStuck": scanForStuck - } - let selfTxn <- FlowTransactionScheduler.schedule( - handlerCap: supervisorCap, - data: nextData, - timestamp: nextTimestamp, - priority: priority, - executionEffort: executionEffort, - fees: <-fees - ) - - emit SupervisorRescheduled( - scheduledTransactionID: selfTxn.id, - timestamp: nextTimestamp - ) - - self._scheduledTransaction <-! selfTxn - } else { - emit SupervisorRescheduleFailed( - timestamp: nextTimestamp, - requiredFee: required, - availableBalance: nil, - error: "Could not borrow fee vault" - ) - return - } - } else { + let vaultRef = self.feesCap.borrow() + if vaultRef == nil { emit SupervisorRescheduleFailed( timestamp: nextTimestamp, - requiredFee: nil, + requiredFee: required, availableBalance: nil, - error: "Missing Supervisor capability" + error: "Could not borrow fee vault" + ) + return + } + if vaultRef!.balance < required { + emit SupervisorRescheduleFailed( + timestamp: nextTimestamp, + requiredFee: required, + availableBalance: vaultRef!.balance, + error: "Insufficient fee vault balance" ) return } + + let fees <- vaultRef!.withdraw(amount: required) as! @FlowToken.Vault + + let nextData = { + "priority": priority.rawValue, + "executionEffort": executionEffort, + "recurringInterval": recurringInterval, + "scanForStuck": scanForStuck + } + + let selfTxn <- FlowTransactionScheduler.schedule( + handlerCap: supervisorCap!, + data: nextData, + timestamp: nextTimestamp, + priority: priority, + executionEffort: executionEffort, + fees: <-fees + ) + + emit SupervisorRescheduled( + scheduledTransactionID: selfTxn.id, + timestamp: nextTimestamp + ) + + self._scheduledTransaction <-! selfTxn } /// Cancels the scheduled transaction if it is scheduled.