@@ -4,6 +4,8 @@ import "FungibleToken"
44// DeFiActions
55import " DeFiActions"
66import " FlowTransactionScheduler"
7+ // Registry for global tide mapping
8+ import " FlowVaultsSchedulerRegistry"
79
810/// FlowVaultsAutoBalancers
911///
@@ -14,7 +16,13 @@ import "FlowTransactionScheduler"
1416/// which identifies all DeFiActions components in the stack related to their composite Strategy.
1517///
1618/// When a Tide and necessarily the related Strategy is closed & burned, the related AutoBalancer and its Capabilities
17- /// are destroyed and deleted
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 tide IDs for global mapping
25+ /// - Cleanup unregisters from the registry
1826///
1927access (all ) contract FlowVaultsAutoBalancers {
2028
@@ -36,19 +44,89 @@ access(all) contract FlowVaultsAutoBalancers {
3644 return self .account .capabilities .borrow <&DeFiActions.AutoBalancer >(publicPath )
3745 }
3846
47+ /// Checks if an AutoBalancer has at least one active (Scheduled) transaction.
48+ /// Used by Supervisor to detect stuck tides that need recovery.
49+ ///
50+ /// @param id: The tide/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 let txnRef = autoBalancer ! .borrowScheduledTransaction (id : txnID ) {
62+ if txnRef .status () == FlowTransactionScheduler .Status .Scheduled {
63+ return true
64+ }
65+ }
66+ }
67+ return false
68+ }
69+
70+ /// Checks if an AutoBalancer is overdue for execution.
71+ /// A tide is considered overdue if:
72+ /// - It has a recurring config
73+ /// - The next expected execution time has passed
74+ /// - It has no active schedule
75+ ///
76+ /// @param id: The tide/AutoBalancer ID
77+ /// @return Bool: true if tide is overdue and stuck, false otherwise
78+ ///
79+ access (all ) fun isStuckTide (id : UInt64 ): Bool {
80+ let autoBalancer = self .borrowAutoBalancer (id : id )
81+ if autoBalancer == nil {
82+ return false
83+ }
84+
85+ // Check if tide has recurring config (should be executing periodically)
86+ let config = autoBalancer ! .getRecurringConfig ()
87+ if config == nil {
88+ return false // Not configured for recurring, can't be "stuck"
89+ }
90+
91+ // Check if there's an active schedule
92+ if self .hasActiveSchedule (id : id ) {
93+ return false // Has active schedule, not stuck
94+ }
95+
96+ // Check if tide is overdue
97+ let nextExpected = autoBalancer ! .calculateNextExecutionTimestampAsConfigured ()
98+ if nextExpected == nil {
99+ return true // Can't calculate next time, likely stuck
100+ }
101+
102+ // If next expected time has passed and no active schedule, tide is stuck
103+ return nextExpected ! < getCurrentBlock ().timestamp
104+ }
105+
39106 /* --- INTERNAL METHODS --- */
40107
41108 /// Configures a new AutoBalancer in storage, configures its public Capability, and sets its inner authorized
42109 /// Capability. If an AutoBalancer is stored with an associated UniqueID value, the operation reverts.
110+ ///
111+ /// @param oracle: The oracle used to query deposited & withdrawn value and to determine if a rebalance should execute
112+ /// @param vaultType: The type of Vault wrapped by the AutoBalancer
113+ /// @param lowerThreshold: The percentage below base value at which a rebalance pulls from rebalanceSource
114+ /// @param upperThreshold: The percentage above base value at which a rebalance pushes to rebalanceSink
115+ /// @param rebalanceSink: An optional DeFiActions Sink to which excess value is directed when rebalancing
116+ /// @param rebalanceSource: An optional DeFiActions Source from which value is withdrawn when rebalancing
117+ /// @param recurringConfig: Optional configuration for automatic recurring rebalancing via FlowTransactionScheduler
118+ /// @param uniqueID: The DeFiActions UniqueIdentifier used for identifying this AutoBalancer
119+ ///
43120 access (account ) fun _initNewAutoBalancer (
44121 oracle : {DeFiActions .PriceOracle },
45122 vaultType : Type ,
46123 lowerThreshold : UFix64 ,
47124 upperThreshold : UFix64 ,
48125 rebalanceSink : {DeFiActions .Sink }? ,
49126 rebalanceSource : {DeFiActions .Source }? ,
127+ recurringConfig : DeFiActions .AutoBalancerRecurringConfig ? ,
50128 uniqueID : DeFiActions .UniqueIdentifier
51- ): auth (DeFiActions.Auto , DeFiActions.Set , DeFiActions.Get , FungibleToken.Withdraw ) &DeFiActions.AutoBalancer {
129+ ): auth (DeFiActions.Auto , DeFiActions.Set , DeFiActions.Get , DeFiActions.Schedule , FungibleToken.Withdraw ) &DeFiActions.AutoBalancer {
52130
53131 // derive paths & prevent collision
54132 let storagePath = self .deriveAutoBalancerPath (id : uniqueID .id , storage : true ) as ! StoragePath
@@ -60,15 +138,15 @@ access(all) contract FlowVaultsAutoBalancers {
60138 assert (! publishedCap ,
61139 message : " Published Capability collision found when publishing AutoBalancer for UniqueIdentifier.id \( uniqueID .id ) at path \( publicPath ) " )
62140
63- // create & save AutoBalancer
141+ // create & save AutoBalancer with optional recurring config
64142 let autoBalancer <- DeFiActions .createAutoBalancer (
65143 oracle : oracle ,
66144 vaultType : vaultType ,
67145 lowerThreshold : lowerThreshold ,
68146 upperThreshold : upperThreshold ,
69147 rebalanceSink : rebalanceSink ,
70148 rebalanceSource : rebalanceSource ,
71- recurringConfig : nil ,
149+ recurringConfig : recurringConfig ,
72150 uniqueID : uniqueID
73151 )
74152 self .account .storage .save (<- autoBalancer , to : storagePath )
@@ -89,32 +167,64 @@ access(all) contract FlowVaultsAutoBalancers {
89167 message : " Error when configuring AutoBalancer for UniqueIdentifier.id \( uniqueID .id ) at path \( storagePath ) " )
90168 assert (publishedCap ,
91169 message : " Error when publishing AutoBalancer Capability for UniqueIdentifier.id \( uniqueID .id ) at path \( publicPath ) " )
170+
171+ // Issue handler capability for the AutoBalancer (for FlowTransactionScheduler execution)
172+ let handlerCap = self .account .capabilities .storage
173+ .issue <auth (FlowTransactionScheduler.Execute ) &{FlowTransactionScheduler .TransactionHandler }>(storagePath )
174+
175+ // Issue schedule capability for the AutoBalancer (for Supervisor to call scheduleNextRebalance directly)
176+ let scheduleCap = self .account .capabilities .storage
177+ .issue <auth (DeFiActions.Schedule ) &DeFiActions.AutoBalancer >(storagePath )
178+
179+ // Register tide in registry for global mapping of live tide IDs
180+ FlowVaultsSchedulerRegistry .register (tideID : uniqueID .id , handlerCap : handlerCap , scheduleCap : scheduleCap )
181+
182+ // Start the native AutoBalancer self-scheduling chain
183+ // This schedules the first rebalance; subsequent ones are scheduled automatically
184+ // by the AutoBalancer after each execution (via recurringConfig)
185+ let scheduleError = autoBalancerRef .scheduleNextRebalance (whileExecuting : nil )
186+ if scheduleError ! = nil {
187+ panic (" Failed to schedule first rebalance for AutoBalancer \( uniqueID .id ) : " .concat (scheduleError ! ))
188+ }
189+
92190 return autoBalancerRef
93191 }
94192
95193 /// Returns an authorized reference on the AutoBalancer with the associated UniqueIdentifier.id. If none is found,
96194 /// the operation reverts.
97195 access (account )
98- fun _borrowAutoBalancer (_ id : UInt64 ): auth (DeFiActions.Auto , DeFiActions.Set , DeFiActions.Get , FungibleToken.Withdraw ) &DeFiActions.AutoBalancer {
196+ fun _borrowAutoBalancer (_ id : UInt64 ): auth (DeFiActions.Auto , DeFiActions.Set , DeFiActions.Get , DeFiActions.Schedule , FungibleToken.Withdraw ) &DeFiActions.AutoBalancer {
99197 let storagePath = self .deriveAutoBalancerPath (id : id , storage : true ) as ! StoragePath
100- return self .account .storage .borrow <auth (DeFiActions.Auto , DeFiActions.Set , DeFiActions.Get , FungibleToken.Withdraw ) &DeFiActions.AutoBalancer >(
198+ return self .account .storage .borrow <auth (DeFiActions.Auto , DeFiActions.Set , DeFiActions.Get , DeFiActions.Schedule , FungibleToken.Withdraw ) &DeFiActions.AutoBalancer >(
101199 from : storagePath
102200 ) ?? panic (" Could not borrow reference to AutoBalancer with UniqueIdentifier.id \( id ) from StoragePath \( storagePath ) " )
103201 }
104202
105203 /// Called by strategies defined in the FlowVaults account which leverage account-hosted AutoBalancers when a
106204 /// Strategy is burned
107205 access (account ) fun _cleanupAutoBalancer (id : UInt64 ) {
206+ // Unregister from registry (removes from global tide mapping)
207+ FlowVaultsSchedulerRegistry .unregister (tideID : id )
208+
108209 let storagePath = self .deriveAutoBalancerPath (id : id , storage : true ) as ! StoragePath
109210 let publicPath = self .deriveAutoBalancerPath (id : id , storage : false ) as ! PublicPath
110211 // unpublish the public AutoBalancer Capability
111- self .account .capabilities .unpublish (publicPath )
112- // delete any CapabilityControllers targetting the AutoBalancer
212+ let _ = self .account .capabilities .unpublish (publicPath )
213+
214+ // Collect controller IDs first (can't modify during iteration)
215+ var controllersToDelete : [UInt64 ] = []
113216 self .account .capabilities .storage .forEachController (forPath : storagePath , fun (_ controller : &StorageCapabilityController ): Bool {
114- controller . delete ( )
217+ controllersToDelete . append ( controller . capabilityID )
115218 return true
116219 })
117- // load & burn the AutoBalancer
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)
118228 let autoBalancer <-self .account .storage .load <@DeFiActions.AutoBalancer >(from : storagePath )
119229 Burner .burn (<- autoBalancer )
120230 }
0 commit comments