Skip to content

Commit 830cf9e

Browse files
committed
Merge remote-tracking branch 'origin/v0' into nialexsan/mainnet-fork-test
2 parents c9c535f + d346a22 commit 830cf9e

61 files changed

Lines changed: 3539 additions & 1697 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cadence_tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ on:
44
push:
55
branches:
66
- main
7+
- v0
78
pull_request:
89
branches:
910
- main
11+
- v0
1012

1113
jobs:
1214
tests:

.github/workflows/e2e_tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ on:
44
push:
55
branches:
66
- main
7+
- v0
78
pull_request:
89
branches:
910
- main
11+
- v0
1012

1113
jobs:
1214
e2e-tests:

.github/workflows/incrementfi_tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ on:
44
push:
55
branches:
66
- main
7+
- v0
78
pull_request:
89
branches:
910
- main
11+
- v0
1012

1113
jobs:
1214
tests:

.github/workflows/punchswap.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ on:
44
push:
55
branches:
66
- main
7+
- v0
78
pull_request:
89
branches:
910
- main
11+
- v0
1012

1113
jobs:
1214
tests:

.gitmodules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
[submodule "lib/FlowALP"]
2323
path = lib/FlowALP
2424
url = git@github.com:onflow/FlowALP.git
25+
branch = v0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Mock FungibleToken implementations representing:
8686
| USDC | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14 | 0xF1815bd50389c46847f0Bda824eC8da914045D14 |
8787
| USDF | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed | 0x2aabea2058b5ac2d339b163c6ab6f2b6d53aabed |
8888
| PYUSD0 | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750 | 0x99aF3EeA856556646C98c8B9b2548Fe815240750 |
89-
| cbBTC | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_a0197b2044d28b08be34d98b23c9312158ea9a18 | 0xA0197b2044D28b08Be34d98b23c9312158Ea9A18 |
89+
| WBTC | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579 | 0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579 |
9090
| wETH | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590 | 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 |
9191
| FUSDEV (ERC4626) | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_d069d989e2f44b70c65347d1853c0c67e10a9f8d | 0xd069d989e2F44B70c65347d1853C0c67e10a9F8D |
9292

cadence/contracts/AutoBalancers.cdc

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

0 commit comments

Comments
 (0)