Smart Contract
FlowYieldVaultsAutoBalancers
A.b1d63873c3cc9f79.FlowYieldVaultsAutoBalancers
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