Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions cadence/contracts/FlowYieldVaultsAutoBalancers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ access(all) contract FlowYieldVaultsAutoBalancers {

let txnIDs = autoBalancer!.getScheduledTransactionIDs()
for txnID in txnIDs {
if let txnRef = autoBalancer!.borrowScheduledTransaction(id: txnID) {
if txnRef.status() == FlowTransactionScheduler.Status.Scheduled {
return true
}
if autoBalancer!.borrowScheduledTransaction(id: txnID)?.status() == FlowTransactionScheduler.Status.Scheduled {
return true
}
}
return false
Expand Down
27 changes: 19 additions & 8 deletions cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ access(all) contract FlowYieldVaultsSchedulerRegistry {
/// Stored as a dictionary for O(1) add/remove; iteration gives the pending set
access(self) var pendingQueue: {UInt64: Bool}

/// Global Supervisor capability (used for self-rescheduling)
access(self) var supervisorCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>?

/* --- ACCOUNT-LEVEL FUNCTIONS --- */

/// Register a YieldVault and store its handler and schedule capabilities (idempotent)
Expand Down Expand Up @@ -105,8 +102,14 @@ access(all) contract FlowYieldVaultsSchedulerRegistry {
}

/// Set global Supervisor capability (used for self-rescheduling)
access(account) fun setSupervisorCap(cap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>) {
self.supervisorCap = cap
access(account)
fun setSupervisorCap(cap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>) {
let storedCapPath = /storage/FlowYieldVaultsSupervisorCapability
let old = self.account.storage
.load<Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>>(
from: storedCapPath
)
self.account.storage.save(cap,to: storedCapPath)
}

/* --- VIEW FUNCTIONS --- */
Expand All @@ -117,6 +120,11 @@ access(all) contract FlowYieldVaultsSchedulerRegistry {
return self.yieldVaultRegistry.keys
}

/// Get the number of currently registered yield vaults
access(all) view fun getRegisteredCount(): Int {
return self.yieldVaultRegistry.length
}

/// Get handler capability for a YieldVault (AutoBalancer capability) - account restricted for internal use
access(account) view fun getHandlerCap(yieldVaultID: UInt64): Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? {
return self.handlerCaps[yieldVaultID]
Expand Down Expand Up @@ -171,8 +179,12 @@ access(all) contract FlowYieldVaultsSchedulerRegistry {

/// Get global Supervisor capability, if set
/// NOTE: Access restricted - only used internally by the scheduler
access(account) view fun getSupervisorCap(): Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? {
return self.supervisorCap
access(account)
view fun getSupervisorCap(): Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume that changing this mostly makes this more upgradable?

Copy link
Contributor Author

@sisyphusSmiling sisyphusSmiling Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the supervisor cap from a contract field to account storage primarily so that it's accessible in transaction context. Previously the scheduler contract was doing this through an access(all) method but that exposes a vulnerability. So instead, we access the authorized capability from storage. Normally, we would just reference the stored resource, but scheduling requires a capability and reissuing a capability every time creates a ton of CapabilityControllers that consumer resources.

return self.account.storage
.copy<Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>>(
from: /storage/FlowYieldVaultsSupervisorCapability
)
}

init() {
Expand All @@ -181,7 +193,6 @@ access(all) contract FlowYieldVaultsSchedulerRegistry {
self.handlerCaps = {}
self.scheduleCaps = {}
self.pendingQueue = {}
self.supervisorCap = nil
}
}

Expand Down
104 changes: 84 additions & 20 deletions cadence/contracts/FlowYieldVaultsSchedulerV1.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,19 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
/* --- CONSTANTS --- */

/// Default recurring interval in seconds (used when not specified)
access(all) let DEFAULT_RECURRING_INTERVAL: UFix64
access(all) var DEFAULT_RECURRING_INTERVAL: UFix64

/// Default priority for recurring schedules
access(all) let DEFAULT_PRIORITY: UInt8 // 1 = Medium
access(all) var DEFAULT_PRIORITY: UInt8 // 1 = Medium

/// Default execution effort for scheduled transactions
access(all) let DEFAULT_EXECUTION_EFFORT: UInt64
access(all) var DEFAULT_EXECUTION_EFFORT: UInt64

/// Minimum fee fallback when estimation returns nil
access(all) let MIN_FEE_FALLBACK: UFix64
access(all) var MIN_FEE_FALLBACK: UFix64

/// Fee margin multiplier to add buffer to estimated fees (1.2 = 20% buffer)
access(all) let FEE_MARGIN_MULTIPLIER: UFix64

/// Default lookahead seconds for scheduling first execution
access(all) let DEFAULT_LOOKAHEAD_SECS: UFix64
access(all) var FEE_MARGIN_MULTIPLIER: UFix64

/* --- PATHS --- */

Expand Down Expand Up @@ -81,6 +78,8 @@ access(all) contract FlowYieldVaultsSchedulerV1 {

/* --- RESOURCES --- */

access(all) entitlement Configure

/// Supervisor - The recovery mechanism for stuck AutoBalancers
///
/// The Supervisor:
Expand All @@ -94,6 +93,7 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
access(all) resource Supervisor: FlowTransactionScheduler.TransactionHandler {
/// Capability to withdraw FLOW for Supervisor's own scheduling fees
access(self) let feesCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
/// Internally managed scheduled transaction for Supervisor self-rescheduling
access(self) var _scheduledTransaction: @FlowTransactionScheduler.ScheduledTransaction?

init(
Expand All @@ -103,6 +103,47 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
self._scheduledTransaction <- nil
}

/// Returns the ID of the internally managed scheduled transaction, or nil if not scheduled
///
/// @return UInt64?: The ID of the internally managed scheduled transaction, or nil if not scheduled
access(all) view fun getScheduledTransactionID(): UInt64? {
return self._scheduledTransaction?.id
}

/* --- CONFIGURE FUNCTIONS --- */

/// Sets the default recurring interval for Supervisor self-rescheduling
/// @param interval: The interval to set
access(Configure) fun setDefaultRecurringInterval(_ interval: UFix64) {
FlowYieldVaultsSchedulerV1.DEFAULT_RECURRING_INTERVAL = interval
}

/// Sets the default execution effort for Supervisor self-rescheduling
/// @param effort: The execution effort to set
access(Configure) fun setDefaultExecutionEffort(_ effort: UInt64) {
FlowYieldVaultsSchedulerV1.DEFAULT_EXECUTION_EFFORT = effort
}

/// Sets the default minimum fee fallback for Supervisor self-rescheduling
/// @param fallback: The minimum fee fallback to set
access(Configure) fun setDefaultMinFeeFallback(_ fallback: UFix64) {
FlowYieldVaultsSchedulerV1.MIN_FEE_FALLBACK = fallback
}

/// Sets the default fee margin multiplier for Supervisor self-rescheduling
/// TODO: Determine if this field is even necessary
/// @param marginMultiplier: The margin multiplier to set
access(Configure) fun setDefaultFeeMarginMultiplier(_ marginMultiplier: UFix64) {
FlowYieldVaultsSchedulerV1.FEE_MARGIN_MULTIPLIER = marginMultiplier
}

/// Sets the default priority for Supervisor self-rescheduling
///
/// @param priority: The priority to set
access(Configure) fun setDefaultPriority(_ priority: FlowTransactionScheduler.Priority) {
FlowYieldVaultsSchedulerV1.DEFAULT_PRIORITY = priority.rawValue
}

/* --- TRANSACTION HANDLER --- */

/// Detects and recovers stuck yield vaults by directly calling their scheduleNextRebalance().
Expand Down Expand Up @@ -134,6 +175,8 @@ access(all) contract FlowYieldVaultsSchedulerV1 {

// STEP 1: State-based detection - scan for stuck yield vaults
if scanForStuck {
// TODO: add pagination - this will inevitably fails and at minimum creates inconsistent execution
// effort between runs
let registeredYieldVaults = FlowYieldVaultsSchedulerRegistry.getRegisteredYieldVaultIDs()
var scanned = 0
for yieldVaultID in registeredYieldVaults {
Expand All @@ -143,6 +186,8 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
scanned = scanned + 1

// Skip if already in pending queue
// TODO: This is extremely inefficient - accessing from mapping is preferrable to iterating over
// an array
if FlowYieldVaultsSchedulerRegistry.getPendingYieldVaultIDs().contains(yieldVaultID) {
continue
}
Expand Down Expand Up @@ -263,9 +308,34 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
}
}
}

/// Cancels the scheduled transaction if it is scheduled.
///
/// @param refundReceiver: The receiver of the refunded vault, or nil to deposit to the internal feesCap
///
/// @return @FlowToken.Vault?: The refunded vault, or nil if a scheduled transaction is not found
access(Schedule) fun cancelScheduledTransaction(refundReceiver: &{FungibleToken.Vault}?): @FlowToken.Vault? {
// nothing to cancel - nil or not scheduled
if self._scheduledTransaction == nil
|| self._scheduledTransaction?.status() != FlowTransactionScheduler.Status.Scheduled {
return nil
}
// cancel the scheduled transaction & deposit refund to receiver if provided
let txnID = self.getScheduledTransactionID()!
let txn <- self._scheduledTransaction <- nil
let refund <- FlowTransactionScheduler.cancel(scheduledTx: <-txn!)
if let receiver = refundReceiver {
receiver.deposit(from: <-refund)
} else {
let feeReceiver = self.feesCap.borrow()
?? panic("Could not borrow fees receiver to deposit refund of \(refund.balance) FLOW when cancelling scheduled transaction id \(txnID)")
feeReceiver.deposit(from: <-refund)
}
return nil
}
}

/* --- PRIVATE FUNCTIONS (access(self)) --- */
/* --- PRIVATE FUNCTIONS --- */

/// Creates a Supervisor handler.
access(self) fun createSupervisor(): @Supervisor {
Expand All @@ -274,12 +344,7 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
return <- create Supervisor(feesCap: feesCap)
}

/* --- PUBLIC FUNCTIONS (access(all)) --- */

/// Returns the Supervisor capability for scheduling
access(all) view fun getSupervisorCap(): Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>? {
return FlowYieldVaultsSchedulerRegistry.getSupervisorCap()
}
/* --- PUBLIC FUNCTIONS --- */

/// Estimates the cost of scheduling a transaction at a given timestamp
access(all) fun estimateSchedulingCost(
Expand All @@ -295,8 +360,6 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
)
}

/* --- ACCOUNT FUNCTIONS --- */

/// Ensures the Supervisor is configured and registered.
/// Creates Supervisor if not exists, issues capability, and registers with Registry.
/// Note: This is access(all) because the Supervisor is owned by the contract account
Expand All @@ -320,6 +383,8 @@ access(all) contract FlowYieldVaultsSchedulerV1 {
FlowYieldVaultsSchedulerRegistry.setSupervisorCap(cap: cap)
}

/* --- ACCOUNT FUNCTIONS --- */

/// Borrows the Supervisor reference (account-restricted for internal use)
access(account) fun borrowSupervisor(): &Supervisor? {
return self.account.storage.borrow<&Supervisor>(from: self.SupervisorStoragePath)
Expand All @@ -340,12 +405,11 @@ access(all) contract FlowYieldVaultsSchedulerV1 {

init() {
// Initialize constants
self.DEFAULT_RECURRING_INTERVAL = 60.0 // 60 seconds
self.DEFAULT_RECURRING_INTERVAL = 60.0 * 10.0 // 5 minutes
self.DEFAULT_PRIORITY = 1 // Medium
self.DEFAULT_EXECUTION_EFFORT = 800
self.MIN_FEE_FALLBACK = 0.00005
self.FEE_MARGIN_MULTIPLIER = 1.2
self.DEFAULT_LOOKAHEAD_SECS = 10.0
self.FEE_MARGIN_MULTIPLIER = 1.0

// Initialize paths
self.SupervisorStoragePath = /storage/FlowYieldVaultsSupervisor
Expand Down
13 changes: 7 additions & 6 deletions cadence/contracts/FlowYieldVaultsStrategies.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ access(all) contract FlowYieldVaultsStrategies {
// init YieldToken -> FLOW Swapper
let yieldToFlowSwapper = MockSwapper.Swapper(
inVault: yieldTokenType,
outVault: collateralType,
outVault: collateralType,
uniqueID: uniqueID
)
// allows for YieldToken to be deposited to the Position
Expand Down Expand Up @@ -336,6 +336,7 @@ access(all) contract FlowYieldVaultsStrategies {
}

/// Composes a Strategy of the given type with the provided funds
/// TODO: Open up for multiple collateral types
access(all) fun createStrategy(
_ type: Type,
uniqueID: DeFiActions.UniqueIdentifier,
Expand Down Expand Up @@ -496,7 +497,7 @@ access(all) contract FlowYieldVaultsStrategies {
?? panic("Could not find UniswapV3 address path for collateral type \(collateralType.identifier)")
assert(uniV3AddressPath.length > 1, message: "Invalid Uniswap V3 swap path length of \(uniV3AddressPath.length)")
assert(uniV3AddressPath[0].equals(yieldTokenEVMAddress),
message: "UniswapV3 swap path does not match - expected path[0] to be \(yieldTokenEVMAddress.toString()) but found \(uniV3AddressPath[0].toString())")
message: "UniswapV3 swap path does not match - expected path[0] to be \(yieldTokenEVMAddress.toString()) but found \(uniV3AddressPath[0].toString())")
let collateralUniV3FeePathConfig = collateralConfig["yieldToCollateralUniV3FeePaths"] as? {Type: [UInt32]}
?? panic("Could not find UniswapV3 fee paths config when creating Strategy \(type.identifier) with collateral \(collateralType.identifier)")
let uniV3FeePath = collateralUniV3FeePathConfig[collateralType]
Expand Down Expand Up @@ -545,7 +546,7 @@ access(all) contract FlowYieldVaultsStrategies {
}

access(all) view fun getSupportedComposers(): {Type: Bool} {
return {
return {
Type<@mUSDCStrategyComposer>(): true,
Type<@TracerStrategyComposer>(): true
}
Expand Down Expand Up @@ -617,11 +618,11 @@ access(all) contract FlowYieldVaultsStrategies {
fun _createRecurringConfig(withID: DeFiActions.UniqueIdentifier?): DeFiActions.AutoBalancerRecurringConfig {
// Create txnFunder that can provide/accept FLOW for scheduling fees
let txnFunder = self._createTxnFunder(withID: withID)

return DeFiActions.AutoBalancerRecurringConfig(
interval: 60, // Rebalance every 60 seconds
interval: 60 * 10, // Rebalance every 10 minutes
priority: FlowTransactionScheduler.Priority.Medium,
executionEffort: 800,
executionEffort: 999,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this updated to 999? the new logic added from this PR increased the execution effort?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can bump this back down to 800. I do think it's helpful to have some buffer on execution effort, and reducing freguency + prio should give us relief on txn costs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be settable now, so i guess it doesn't matter too much. Can adjust based on cost

forceRebalance: false,
txnFunder: txnFunder
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import "FlowYieldVaultsSchedulerRegistry"
/// @return Int: The number of registered yield vaults
///
access(all) fun main(): Int {
return FlowYieldVaultsSchedulerRegistry.getRegisteredYieldVaultIDs().length
return FlowYieldVaultsSchedulerRegistry.getRegisteredCount()
}

8 changes: 4 additions & 4 deletions cadence/tests/scheduled_rebalance_integration_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ fun testNativeScheduledRebalancing() {

// Step 5: Wait for automatic execution by emulator FVM
log("Step 4: Waiting for automatic execution...")
Test.moveTime(by: 70.0)
Test.moveTime(by: 60.0 * 10.0 + 10.0)
Test.commitBlock()

// Step 6: Check for execution events
Expand Down Expand Up @@ -216,7 +216,7 @@ fun testMultipleExecutionsWithPriceChanges() {
log("Step 2: First execution...")
setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.5)
setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.2)
Test.moveTime(by: 70.0)
Test.moveTime(by: 60.0 * 10.0 + 10.0)
Test.commitBlock()

let execEvents1 = Test.eventsOfType(Type<FlowTransactionScheduler.Executed>())
Expand All @@ -228,7 +228,7 @@ fun testMultipleExecutionsWithPriceChanges() {
log("Step 3: Second execution...")
setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 2.5)
setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 2.0)
Test.moveTime(by: 70.0)
Test.moveTime(by: 60.0 * 10.0 + 10.0)
Test.commitBlock()

let execEvents2 = Test.eventsOfType(Type<FlowTransactionScheduler.Executed>())
Expand All @@ -240,7 +240,7 @@ fun testMultipleExecutionsWithPriceChanges() {
log("Step 4: Third execution...")
setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 4.0)
setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 3.0)
Test.moveTime(by: 70.0)
Test.moveTime(by: 60.0 * 10.0 + 10.0)
Test.commitBlock()

let execEvents3 = Test.eventsOfType(Type<FlowTransactionScheduler.Executed>())
Expand Down
Loading