diff --git a/cadence/contracts/FlowYieldVaultsAutoBalancers.cdc b/cadence/contracts/FlowYieldVaultsAutoBalancers.cdc index 420fbbc4..258cac95 100644 --- a/cadence/contracts/FlowYieldVaultsAutoBalancers.cdc +++ b/cadence/contracts/FlowYieldVaultsAutoBalancers.cdc @@ -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 diff --git a/cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc b/cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc index 59420885..645d7e0a 100644 --- a/cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc +++ b/cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc @@ -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? - /* --- ACCOUNT-LEVEL FUNCTIONS --- */ /// Register a YieldVault and store its handler and schedule capabilities (idempotent) @@ -105,8 +102,14 @@ access(all) contract FlowYieldVaultsSchedulerRegistry { } /// Set global Supervisor capability (used for self-rescheduling) - access(account) fun setSupervisorCap(cap: Capability) { - self.supervisorCap = cap + access(account) + fun setSupervisorCap(cap: Capability) { + let storedCapPath = /storage/FlowYieldVaultsSupervisorCapability + let old = self.account.storage + .load>( + from: storedCapPath + ) + self.account.storage.save(cap,to: storedCapPath) } /* --- VIEW FUNCTIONS --- */ @@ -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? { return self.handlerCaps[yieldVaultID] @@ -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? { - return self.supervisorCap + access(account) + view fun getSupervisorCap(): Capability? { + return self.account.storage + .copy>( + from: /storage/FlowYieldVaultsSupervisorCapability + ) } init() { @@ -181,7 +193,6 @@ access(all) contract FlowYieldVaultsSchedulerRegistry { self.handlerCaps = {} self.scheduleCaps = {} self.pendingQueue = {} - self.supervisorCap = nil } } diff --git a/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc b/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc index 3a375fc2..2b040267 100644 --- a/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc +++ b/cadence/contracts/FlowYieldVaultsSchedulerV1.cdc @@ -27,25 +27,22 @@ import "FlowYieldVaultsAutoBalancers" /// access(all) contract FlowYieldVaultsSchedulerV1 { - /* --- CONSTANTS --- */ + /* --- FIELDS --- */ /// 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 --- */ @@ -81,6 +78,8 @@ access(all) contract FlowYieldVaultsSchedulerV1 { /* --- RESOURCES --- */ + access(all) entitlement Configure + /// Supervisor - The recovery mechanism for stuck AutoBalancers /// /// The Supervisor: @@ -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 + /// Internally managed scheduled transaction for Supervisor self-rescheduling access(self) var _scheduledTransaction: @FlowTransactionScheduler.ScheduledTransaction? init( @@ -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(). @@ -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 { @@ -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 } @@ -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 { @@ -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? { - return FlowYieldVaultsSchedulerRegistry.getSupervisorCap() - } + /* --- PUBLIC FUNCTIONS --- */ /// Estimates the cost of scheduling a transaction at a given timestamp access(all) fun estimateSchedulingCost( @@ -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 @@ -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) @@ -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 // 10 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 diff --git a/cadence/contracts/FlowYieldVaultsStrategies.cdc b/cadence/contracts/FlowYieldVaultsStrategies.cdc index 97542f50..f585956a 100644 --- a/cadence/contracts/FlowYieldVaultsStrategies.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategies.cdc @@ -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 @@ -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, @@ -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] @@ -545,7 +546,7 @@ access(all) contract FlowYieldVaultsStrategies { } access(all) view fun getSupportedComposers(): {Type: Bool} { - return { + return { Type<@mUSDCStrategyComposer>(): true, Type<@TracerStrategyComposer>(): true } @@ -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, forceRebalance: false, txnFunder: txnFunder ) diff --git a/cadence/scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc b/cadence/scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc index 226ed8c1..3b672c3a 100644 --- a/cadence/scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc +++ b/cadence/scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc @@ -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() } diff --git a/cadence/tests/scheduled_rebalance_integration_test.cdc b/cadence/tests/scheduled_rebalance_integration_test.cdc index 084d3df2..ac5b1ccb 100644 --- a/cadence/tests/scheduled_rebalance_integration_test.cdc +++ b/cadence/tests/scheduled_rebalance_integration_test.cdc @@ -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 @@ -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()) @@ -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()) @@ -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()) @@ -256,12 +256,3 @@ fun testMultipleExecutionsWithPriceChanges() { log("PASS: Multiple executions with price changes and verified balance changes") } - -// Main test runner -// Note: getAutoBalancerBalance helper is in test_helpers.cdc -access(all) -fun main() { - setup() - testNativeScheduledRebalancing() - testMultipleExecutionsWithPriceChanges() -} diff --git a/cadence/tests/scheduled_rebalance_scenario_test.cdc b/cadence/tests/scheduled_rebalance_scenario_test.cdc index 935aeea1..867cf6f1 100644 --- a/cadence/tests/scheduled_rebalance_scenario_test.cdc +++ b/cadence/tests/scheduled_rebalance_scenario_test.cdc @@ -183,7 +183,7 @@ fun testSingleAutoBalancerThreeExecutions() { setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.2) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.1) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let events1 = Test.eventsOfType(Type()) @@ -200,7 +200,7 @@ fun testSingleAutoBalancerThreeExecutions() { setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.5) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.3) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let events2 = Test.eventsOfType(Type()) @@ -217,7 +217,7 @@ fun testSingleAutoBalancerThreeExecutions() { setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.8) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.5) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let events3 = Test.eventsOfType(Type()) @@ -288,7 +288,7 @@ fun testThreeYieldVaultsNineExecutions() { log("\n--- ROUND 1 ---") setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.3) 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 events1 = Test.eventsOfType(Type()) @@ -308,7 +308,7 @@ fun testThreeYieldVaultsNineExecutions() { log("\n--- ROUND 2 ---") setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.6) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.4) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let events2 = Test.eventsOfType(Type()) @@ -328,7 +328,7 @@ fun testThreeYieldVaultsNineExecutions() { log("\n--- ROUND 3 ---") setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 2.0) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.6) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let events3 = Test.eventsOfType(Type()) @@ -408,7 +408,7 @@ fun testFiveYieldVaultsContinueWithoutSupervisor() { // Use significant price changes to ensure rebalancing triggers setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0 + (UFix64(round) * 0.3)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.0 + (UFix64(round) * 0.2)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() // Verify all 5 yield vaults changed balance @@ -437,7 +437,7 @@ fun testFiveYieldVaultsContinueWithoutSupervisor() { // Use significantly different prices for second set of rounds setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 2.0 + (UFix64(round) * 0.3)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.5 + (UFix64(round) * 0.2)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() // Verify all 5 yield vaults changed balance @@ -521,7 +521,7 @@ fun testFailedYieldVaultCannotRecoverWithoutSupervisor() { // Use significant price changes to ensure rebalancing triggers setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0 + (UFix64(round) * 0.3)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.0 + (UFix64(round) * 0.2)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() // Verify all 3 yield vaults changed balance @@ -570,7 +570,7 @@ fun testFailedYieldVaultCannotRecoverWithoutSupervisor() { log("\nStep 4: Waiting for pre-scheduled transactions to execute...") round = 0 while round < 3 { - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() round = round + 1 } @@ -578,7 +578,7 @@ fun testFailedYieldVaultCannotRecoverWithoutSupervisor() { // After yield vaults execute, they try to reschedule but fail due to insufficient funds // Now wait at least one MORE interval (60s) so they become overdue log("\nStep 4b: Waiting for yield vaults to become overdue (no active schedules)...") - Test.moveTime(by: 120.0) // Wait 2 intervals to ensure all yield vaults are past their next expected time + Test.moveTime(by: 2.0 * (60.0 * 10.0 + 10.0)) // Wait 2 intervals to ensure all yield vaults are past their next expected time Test.commitBlock() let eventsAfterDrain = Test.eventsOfType(Type()) @@ -607,7 +607,7 @@ fun testFailedYieldVaultCannotRecoverWithoutSupervisor() { log("\nStep 6: Waiting more (stuck yield vaults should stay stuck without Supervisor)...") round = 0 while round < 3 { - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() round = round + 1 } @@ -637,14 +637,3 @@ fun testFailedYieldVaultCannotRecoverWithoutSupervisor() { log("PASS: Failed yield vaults cannot recover without Supervisor") } - -// Main test runner -access(all) -fun main() { - setup() - testRegistryReceivesYieldVaultRegistrationAtInit() - testSingleAutoBalancerThreeExecutions() - testThreeYieldVaultsNineExecutions() - testFiveYieldVaultsContinueWithoutSupervisor() - testFailedYieldVaultCannotRecoverWithoutSupervisor() -} diff --git a/cadence/tests/scheduled_supervisor_test.cdc b/cadence/tests/scheduled_supervisor_test.cdc index 1c24c3d6..1700923d 100644 --- a/cadence/tests/scheduled_supervisor_test.cdc +++ b/cadence/tests/scheduled_supervisor_test.cdc @@ -27,13 +27,13 @@ access(all) var snapshot: UInt64 = 0 access(all) fun setup() { log("🚀 Setting up Supervisor integration test...") - + deployContracts() - + // Fund FlowYieldVaults account BEFORE any YieldVaults are created, as registerYieldVault // now atomically schedules the first execution which requires FLOW for fees mintFlow(to: flowYieldVaultsAccount, amount: 1000.0) - + // Mock Oracle setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.0) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) @@ -76,14 +76,14 @@ fun setup() { issuerStoragePath: FlowYieldVaultsStrategies.IssuerStoragePath, beFailed: false ) - + // Capture snapshot for test isolation snapshot = getCurrentBlockHeight() log("✅ Setup complete. Snapshot at block: ".concat(snapshot.toString())) } /// Test: Auto-Register and Native Scheduling -/// +/// /// NEW ARCHITECTURE: /// - AutoBalancers self-schedule via native FlowTransactionScheduler /// - The Supervisor is for recovery only (detects stuck yield vaults and seeds them) @@ -92,7 +92,7 @@ fun setup() { access(all) fun testAutoRegisterAndSupervisor() { log("\n Testing Auto-Register + Native Scheduling...") - + let user = Test.createAccount() mintFlow(to: user, amount: 1000.0) grantBeta(flowYieldVaultsAccount, user) @@ -105,7 +105,7 @@ fun testAutoRegisterAndSupervisor() { user ) Test.expect(createYieldVaultRes, Test.beSucceeded()) - + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! let yieldVaultID = yieldVaultIDs[0] log("YieldVault created: ".concat(yieldVaultID.toString())) @@ -124,18 +124,18 @@ fun testAutoRegisterAndSupervisor() { log("Step 2: Wait for native execution...") setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.8) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.5) - - Test.moveTime(by: 75.0) + + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() // 4. Verify native execution occurred let schedulerExecEvents = Test.eventsOfType(Type()) Test.assert(schedulerExecEvents.length > 0, message: "Should have FlowTransactionScheduler.Executed event") - + let rebalancedEvents = Test.eventsOfType(Type()) log("Scheduler.Executed events: ".concat(schedulerExecEvents.length.toString())) log("DeFiActions.Rebalanced events: ".concat(rebalancedEvents.length.toString())) - + log("PASS: Auto-Register + Native Scheduling") } @@ -149,11 +149,11 @@ fun testAutoRegisterAndSupervisor() { access(all) fun testMultiYieldVaultNativeScheduling() { log("\n Testing Multiple YieldVaults Native Scheduling...") - + let user = Test.createAccount() mintFlow(to: user, amount: 1000.0) grantBeta(flowYieldVaultsAccount, user) - + // Create 3 yield vaults (each auto-schedules via native mechanism) var i = 0 while i < 3 { @@ -165,7 +165,7 @@ fun testMultiYieldVaultNativeScheduling() { Test.expect(res, Test.beSucceeded()) i = i + 1 } - + let allYieldVaults = getYieldVaultIDs(address: user.address)! log("Created ".concat(allYieldVaults.length.toString()).concat(" yield vaults")) @@ -176,17 +176,17 @@ fun testMultiYieldVaultNativeScheduling() { Test.assert(regIDs.contains(tid), message: "YieldVault should be registered") } log("All yield vaults registered") - + // Wait for native execution setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.5) - Test.moveTime(by: 75.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + // Verify all executed via native scheduling let execEvents = Test.eventsOfType(Type()) Test.assert(execEvents.length >= 3, message: "Should have at least 3 executions (one per yield vault)") log("Executions: ".concat(execEvents.length.toString())) - + log("PASS: Multiple YieldVaults Native Scheduling") } @@ -194,7 +194,7 @@ fun testMultiYieldVaultNativeScheduling() { // testSingleAutoBalancerThreeExecutions in scheduled_rebalance_scenario_test.cdc /// Test: Multiple yield vaults execute independently via native scheduling -/// +/// /// NEW ARCHITECTURE: /// - Each AutoBalancer self-schedules via native mechanism /// - No Supervisor needed for normal execution @@ -219,7 +219,7 @@ fun testMultiYieldVaultIndependentExecution() { Test.expect(res, Test.beSucceeded()) i = i + 1 } - + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! log("Created ".concat(yieldVaultIDs.length.toString()).concat(" yield vaults")) @@ -234,27 +234,27 @@ fun testMultiYieldVaultIndependentExecution() { // Use VERY LARGE price changes to ensure rebalancing triggers regardless of previous state setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 3.0 * UFix64(round)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 2.5 * UFix64(round)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + let newBalance = getAutoBalancerBalance(id: trackedYieldVaultID) ?? 0.0 log("Round ".concat(round.toString()).concat(": Balance ").concat(prevBalance.toString()).concat(" -> ").concat(newBalance.toString())) Test.assert(newBalance != prevBalance, message: "Balance should change after round ".concat(round.toString()).concat(" (was: ").concat(prevBalance.toString()).concat(", now: ").concat(newBalance.toString()).concat(")")) prevBalance = newBalance - + round = round + 1 } // Count executions let execEvents = Test.eventsOfType(Type()) log("Total executions: ".concat(execEvents.length.toString())) - + // 3 yield vaults x 3 rounds = 9 minimum executions Test.assert( execEvents.length >= 9, message: "Expected at least 9 executions but found ".concat(execEvents.length.toString()) ) - + log("PASS: Multiple yield vaults executed independently with verified balance changes") } @@ -287,10 +287,10 @@ fun testPaginationStress() { let numYieldVaults = fullBatches * maxBatchSize + partialBatch // 18 yield vaults let minExecutionsPerYieldVault = 3 let minTotalExecutions = numYieldVaults * minExecutionsPerYieldVault // 54 minimum (18 x 3) - + log("\n Testing pagination with ".concat(numYieldVaults.toString()).concat(" yield vaults (").concat(fullBatches.toString()).concat("x MAX_BATCH_SIZE + ").concat(partialBatch.toString()).concat(")...")) log("Expecting at least ".concat(minTotalExecutions.toString()).concat(" total executions (").concat(minExecutionsPerYieldVault.toString()).concat(" per yield vault)")) - + let user = Test.createAccount() mintFlow(to: user, amount: 10000.0) // For 3 rounds of 18 yield vaults grantBeta(flowYieldVaultsAccount, user) @@ -308,27 +308,27 @@ fun testPaginationStress() { Test.expect(res, Test.beSucceeded()) i = i + 1 } - + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! log("Created ".concat(yieldVaultIDs.length.toString()).concat(" yield vaults")) Test.assertEqual(numYieldVaults, yieldVaultIDs.length) - + // Check registry state - all yield vaults should be registered let regIDsRes = executeScript("../scripts/flow-yield-vaults/get_registered_yield_vault_ids.cdc", []) let regIDs = regIDsRes.returnValue! as! [UInt64] log("Registered yield vaults: ".concat(regIDs.length.toString())) - + Test.assert( regIDs.length >= numYieldVaults, message: "Expected at least ".concat(numYieldVaults.toString()).concat(" registered yield vaults, got ").concat(regIDs.length.toString()) ) - + // Verify pagination works on pending queue (should be empty since all self-schedule) let pendingCountRes = executeScript("../scripts/flow-yield-vaults/get_pending_count.cdc", []) let pendingCount = pendingCountRes.returnValue! as! Int log("Pending queue size (should be 0 since all self-schedule): ".concat(pendingCount.toString())) Test.assertEqual(0, pendingCount) - + // Test paginated access - request each page up to MAX_BATCH_SIZE var page = 0 while page <= fullBatches { @@ -337,7 +337,7 @@ fun testPaginationStress() { log("Page ".concat(page.toString()).concat(" of pending queue: ").concat(pageData.length.toString()).concat(" yield vaults")) page = page + 1 } - + // Track balance for first 3 yield vaults (sample) to verify rebalancing var sampleBalances: [UFix64] = [] var sampleIdx = 0 @@ -346,7 +346,7 @@ fun testPaginationStress() { sampleIdx = sampleIdx + 1 } log("Initial sample balances (first 3 yield vaults): T0=".concat(sampleBalances[0].toString()).concat(", T1=").concat(sampleBalances[1].toString()).concat(", T2=").concat(sampleBalances[2].toString())) - + // Execute 3 rounds - verify each yield vault executes at least 3 times with balance verification log("\n--- Executing 3 rounds ---") var round = 1 @@ -354,9 +354,9 @@ fun testPaginationStress() { // Use LARGE price changes to ensure rebalancing triggers regardless of previous state setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 2.0 * UFix64(round)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.5 * UFix64(round)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + // Verify sample balances changed sampleIdx = 0 while sampleIdx < 3 { @@ -365,22 +365,22 @@ fun testPaginationStress() { sampleBalances[sampleIdx] = newBal sampleIdx = sampleIdx + 1 } - + let roundEvents = Test.eventsOfType(Type()) let expectedMinEvents = numYieldVaults * round log("Round ".concat(round.toString()).concat(": ").concat(roundEvents.length.toString()).concat(" total executions (expected >= ").concat(expectedMinEvents.toString()).concat("), sample balances verified")) - + Test.assert( roundEvents.length >= expectedMinEvents, message: "Round ".concat(round.toString()).concat(": Expected at least ").concat(expectedMinEvents.toString()).concat(" executions, got ").concat(roundEvents.length.toString()) ) round = round + 1 } - + // Final verification let finalEvents = Test.eventsOfType(Type()) log("\nFinal total executions: ".concat(finalEvents.length.toString())) - + Test.assert( finalEvents.length >= minTotalExecutions, message: "Expected at least ".concat(minTotalExecutions.toString()).concat(" total executions (").concat(numYieldVaults.toString()).concat(" yield vaults x ").concat(minExecutionsPerYieldVault.toString()).concat(" rounds), got ").concat(finalEvents.length.toString()) @@ -424,7 +424,7 @@ fun testSupervisorDoesNotDisruptHealthyYieldVaults() { user ) Test.expect(createRes, Test.beSucceeded()) - + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! let yieldVaultID = yieldVaultIDs[0] log("YieldVault created: ".concat(yieldVaultID.toString())) @@ -439,9 +439,9 @@ fun testSupervisorDoesNotDisruptHealthyYieldVaults() { // 3. Wait for some native executions log("Step 3: Waiting for native execution...") - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + let execEventsBefore = Test.eventsOfType(Type()) log("Executions so far: ".concat(execEventsBefore.length.toString())) Test.assert(execEventsBefore.length >= 1, message: "YieldVault should have executed at least once") @@ -455,11 +455,11 @@ fun testSupervisorDoesNotDisruptHealthyYieldVaults() { // Supervisor is automatically configured when FlowYieldVaultsSchedulerV1 is deployed (in init) Test.commitBlock() - + // Schedule Supervisor - let scheduledTime = getCurrentBlock().timestamp + 2000.0 + let scheduledTime = getCurrentBlockTimestamp() + (60.0 * 10.0) let schedSupRes = executeTransaction( - "../transactions/flow-yield-vaults/schedule_supervisor.cdc", + "../transactions/flow-yield-vaults/admin/schedule_supervisor.cdc", [scheduledTime, UInt8(1), UInt64(800), 0.05, 30.0, true, 10.0, false], flowYieldVaultsAccount ) @@ -468,25 +468,25 @@ fun testSupervisorDoesNotDisruptHealthyYieldVaults() { // 6. Advance time to let Supervisor run log("Step 6: Waiting for Supervisor to run...") - Test.moveTime(by: 2100.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() // 7. Verify Supervisor ran but found nothing to recover (healthy yield vault) let recoveredEvents = Test.eventsOfType(Type()) log("YieldVaultRecovered events: ".concat(recoveredEvents.length.toString())) - + // Healthy yield vaults don't need recovery // Note: recoveredEvents might be > 0 if there were stuck yield vaults from previous tests // The key verification is that our yield vault continues to execute // 8. Verify yield vault continues executing log("Step 7: Verifying yield vault continues executing...") - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let execEventsAfter = Test.eventsOfType(Type()) log("Total executions: ".concat(execEventsAfter.length.toString())) - + // Verification: We should have more executions (yield vault continued normally) Test.assert( execEventsAfter.length > execEventsBefore.length, @@ -498,7 +498,7 @@ fun testSupervisorDoesNotDisruptHealthyYieldVaults() { let finalPending = finalPendingRes.returnValue! as! Int log("Final pending queue size: ".concat(finalPending.toString())) Test.assertEqual(0, finalPending) - + log("PASS: Supervisor runs without disrupting healthy yield vaults") } @@ -523,7 +523,7 @@ access(all) fun testStuckYieldVaultDetectionLogic() { // Reset to snapshot for test isolation Test.reset(to: snapshot) - + log("\n Testing stuck yield vault detection logic...") let user = Test.createAccount() @@ -538,16 +538,16 @@ fun testStuckYieldVaultDetectionLogic() { user ) Test.expect(createRes, Test.beSucceeded()) - + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! let yieldVaultID = yieldVaultIDs[0] log("YieldVault created: ".concat(yieldVaultID.toString())) // 2. Let it execute log("Step 2: Waiting for execution...") - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + let execEvents = Test.eventsOfType(Type()) log("Executions: ".concat(execEvents.length.toString())) Test.assert(execEvents.length >= 1, message: "YieldVault should have executed") @@ -578,7 +578,7 @@ fun testStuckYieldVaultDetectionLogic() { } /// COMPREHENSIVE TEST: Insufficient Funds -> Failure -> Recovery -/// +/// /// This test validates the COMPLETE failure and recovery cycle: /// 1. Create 5 yield vaults (matches MAX_BATCH_SIZE) /// 2. Let them execute 3 rounds each (30+ executions) @@ -594,7 +594,7 @@ access(all) fun testInsufficientFundsAndRecovery() { // Reset to snapshot for isolation - this test needs a clean slate Test.reset(to: snapshot) - + log("\n========================================") log("TEST: Comprehensive Insufficient Funds -> Recovery") log("========================================") @@ -628,7 +628,7 @@ fun testInsufficientFundsAndRecovery() { Test.expect(res, Test.beSucceeded()) i = i + 1 } - + let yieldVaultIDs = getYieldVaultIDs(address: user.address)! Test.assertEqual(5, yieldVaultIDs.length) log("Created ".concat(yieldVaultIDs.length.toString()).concat(" yield vaults")) @@ -642,7 +642,7 @@ fun testInsufficientFundsAndRecovery() { // STEP 3: Let yield vaults execute 3 rounds (and Supervisor run) with balance verification // ======================================== log("\n--- STEP 3: Running 3 rounds (5 yield vaults x 3 = 15 expected executions) ---") - + // Track initial balances for all 5 yield vaults var prevBalances: [UFix64] = [] var idx = 0 @@ -651,15 +651,15 @@ fun testInsufficientFundsAndRecovery() { idx = idx + 1 } log("Initial balances tracked for 5 yield vaults") - + var round = 1 while round <= 3 { // Use LARGE price changes to ensure rebalancing triggers regardless of previous state setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.5 * UFix64(round)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.2 * UFix64(round)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + // Verify all 5 yield vaults changed balance idx = 0 while idx < 5 { @@ -675,7 +675,7 @@ fun testInsufficientFundsAndRecovery() { let execEventsBeforeDrain = Test.eventsOfType(Type()) log("Executions before drain: ".concat(execEventsBeforeDrain.length.toString())) Test.assert(execEventsBeforeDrain.length >= 15, message: "Should have at least 15 executions (5 yield vaults x 3 rounds)") - + // Verify yield vaults are registered let registeredCount = (executeScript( "../scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc", @@ -693,7 +693,7 @@ fun testInsufficientFundsAndRecovery() { [flowYieldVaultsAccount.address] ).returnValue! as! UFix64) log("Balance before drain: ".concat(balanceBeforeDrain.toString())) - + // Drain ALL FLOW (leave minimal amount) if balanceBeforeDrain > 0.01 { let drainRes = executeTransaction( @@ -717,7 +717,7 @@ fun testInsufficientFundsAndRecovery() { log("\n--- STEP 5: Waiting for failures (6 rounds) ---") var waitRound = 0 while waitRound < 6 { - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() waitRound = waitRound + 1 } @@ -762,7 +762,7 @@ fun testInsufficientFundsAndRecovery() { // ======================================== log("\n--- STEP 7: Refunding FlowYieldVaults account ---") mintFlow(to: flowYieldVaultsAccount, amount: 200.0) - + let balanceAfterRefund = (executeScript( "../scripts/flow-yield-vaults/get_flow_balance.cdc", [flowYieldVaultsAccount.address] @@ -774,22 +774,22 @@ fun testInsufficientFundsAndRecovery() { // STEP 8: START Supervisor (first time scheduling) // ======================================== log("\n--- STEP 8: Starting Supervisor (post-refund) ---") - + // Process any pending blocks first Test.commitBlock() Test.moveTime(by: 1.0) Test.commitBlock() - + // Get FRESH timestamp after block commit - let currentTs = getCurrentBlock().timestamp + let currentTs = getCurrentBlockTimestamp() log("Current timestamp: ".concat(currentTs.toString())) - + // Use VERY large offset (10000s) to ensure it's always in the future - let restartTime = currentTs + 10000.0 + let restartTime = currentTs + (60.0 * 10.0) log("Scheduling Supervisor at: ".concat(restartTime.toString())) - + let schedSupRes = executeTransaction( - "../transactions/flow-yield-vaults/schedule_supervisor.cdc", + "../transactions/flow-yield-vaults/admin/schedule_supervisor.cdc", [restartTime, UInt8(1), UInt64(5000), 0.5, 60.0, true, 30.0, true], // Higher execution effort (5000) for recovering 5 yield vaults flowYieldVaultsAccount ) @@ -800,7 +800,7 @@ fun testInsufficientFundsAndRecovery() { // STEP 9: Let Supervisor run and recover stuck yield vaults // ======================================== log("\n--- STEP 9: Letting Supervisor run and recover ---") - Test.moveTime(by: 11000.0) // Move past the 10000s scheduled time + Test.moveTime(by: Fix64(restartTime - getCurrentBlockTimestamp() + 100.0)) // Move past the 10000s scheduled time Test.commitBlock() // Check for StuckYieldVaultDetected events @@ -822,25 +822,25 @@ fun testInsufficientFundsAndRecovery() { log("\n--- STEP 10: Running 3+ rounds to verify yield vaults resumed self-scheduling ---") // After Supervisor seeds, yield vaults should resume self-scheduling and continue perpetually. // We run 4 rounds to ensure each yield vault executes at least 3 times after recovery. - + // Track balance for first yield vault to verify rebalancing actually happens let trackedYieldVaultID = yieldVaultIDs[0] var prevBalance = getAutoBalancerBalance(id: trackedYieldVaultID) ?? 0.0 log("Balance before recovery rounds (yield vault ".concat(trackedYieldVaultID.toString()).concat("): ").concat(prevBalance.toString())) - + round = 1 while round <= 4 { // Use LARGE price changes to ensure rebalancing triggers setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 5.0 * UFix64(round)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 4.0 * UFix64(round)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() - + let newBalance = getAutoBalancerBalance(id: trackedYieldVaultID) ?? 0.0 log("Recovery round ".concat(round.toString()).concat(": Balance ").concat(prevBalance.toString()).concat(" -> ").concat(newBalance.toString())) Test.assert(newBalance != prevBalance, message: "Balance should change after recovery round ".concat(round.toString())) prevBalance = newBalance - + round = round + 1 } @@ -848,7 +848,7 @@ fun testInsufficientFundsAndRecovery() { let newExecutions = execEventsFinal.length - execCountBeforeRecovery log("Final total executions: ".concat(execEventsFinal.length.toString())) log("New executions after recovery: ".concat(newExecutions.toString())) - + // After Supervisor seeds 5 yield vaults: // - 1 Supervisor execution // - 10 initial seeded executions (1 per yield vault) @@ -911,12 +911,3 @@ fun testInsufficientFundsAndRecovery() { log("- All ".concat(activeScheduleCount.toString()).concat(" yield vaults have active schedules")) log("========================================") } - -access(all) -fun main() { - setup() - testAutoRegisterAndSupervisor() - testMultiYieldVaultNativeScheduling() - testStuckYieldVaultDetectionLogic() - testInsufficientFundsAndRecovery() -} diff --git a/cadence/tests/scheduler_edge_cases_test.cdc b/cadence/tests/scheduler_edge_cases_test.cdc index 17d9decb..1ac0cdc3 100644 --- a/cadence/tests/scheduler_edge_cases_test.cdc +++ b/cadence/tests/scheduler_edge_cases_test.cdc @@ -287,7 +287,7 @@ fun testHealthyYieldVaultsSelfSchedule() { // Use LARGE price changes to ensure rebalancing triggers setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.5 * UFix64(round)) setMockOraclePrice(signer: flowYieldVaultsAccount, forTokenIdentifier: yieldTokenIdentifier, price: 1.2 * UFix64(round)) - Test.moveTime(by: 70.0) + Test.moveTime(by: 60.0 * 10.0 + 10.0) Test.commitBlock() let newBalance = getAutoBalancerBalance(id: yieldVaultID) ?? 0.0 diff --git a/cadence/tests/scripts/get_current_block_timestamp.cdc b/cadence/tests/scripts/get_current_block_timestamp.cdc new file mode 100644 index 00000000..3c575dd4 --- /dev/null +++ b/cadence/tests/scripts/get_current_block_timestamp.cdc @@ -0,0 +1,3 @@ +access(all) fun main(): UFix64 { + return getCurrentBlock().timestamp +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index ca717068..d6035333 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -26,6 +26,13 @@ fun _executeTransaction(_ path: String, _ args: [AnyStruct], _ signer: Test.Test return Test.executeTransaction(txn) } +access(all) +fun getCurrentBlockTimestamp(): UFix64 { + let res = _executeScript("./scripts/get_current_block_timestamp.cdc", []) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! UFix64 +} + access(all) fun grantProtocolBeta(_ admin: Test.TestAccount, _ grantee: Test.TestAccount): Test.TransactionResult { let signers = admin.address == grantee.address ? [admin] : [admin, grantee] @@ -530,12 +537,6 @@ fun rebalanceYieldVault(signer: Test.TestAccount, id: UInt64, force: Bool, beFai Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) } -// access(all) -// fun rebalancePosition(signer: Test.TestAccount, id: UInt64, force: Bool, beFailed: Bool) { -// let res = _executeTransaction("../../lib/FlowCreditMarket/cadence/transactions/flow-credit-market/pool-management/rebalance_auto_balancer_by_id.cdc", [id, force], signer) -// Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) -// } - /* --- Event helpers --- */ access(all) diff --git a/cadence/transactions/flow-yield-vaults/admin/destroy_and_reset_supervisor.cdc b/cadence/transactions/flow-yield-vaults/admin/destroy_and_reset_supervisor.cdc new file mode 100644 index 00000000..fd12447c --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/destroy_and_reset_supervisor.cdc @@ -0,0 +1,60 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Destroys the global Supervisor, removing the stored Capability used for scheduling and cancelling any scheduled +/// transactions internally managed by the Supervisor. After removing old Supervisor, it schedules a new one for +/// recurring execution. Configurable via arguments; sensible defaults if omitted. +/// +/// - recurringInterval: seconds between runs (e.g., 60.0) +/// - priorityRaw: The raw priority value (UInt8) for data serialization (0=High, 1=Medium, 2=Low) +/// - executionEffort: The execution effort estimate for the transaction (1-9999) +/// - scanForStuck: Whether to scan for stuck yield vaults in the next execution +transaction( + recurringInterval: UFix64, + priorityRaw: UInt8, + executionEffort: UInt64, + scanForStuck: Bool +) { + + let oldSupervisor: @FlowYieldVaultsSchedulerV1.Supervisor? + let newSupervisor: auth(FlowYieldVaultsSchedulerV1.Schedule) &FlowYieldVaultsSchedulerV1.Supervisor + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + // remove the stored Capability used for internal recurring execution + let supervisorCap = signer.storage + .load>( + from: /storage/FlowYieldVaultsSupervisorCapability + ) + // delete all controllers for the Supervisor storage path + for controller in signer.capabilities.storage.getControllers(forPath: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) { + controller.delete() + } + // load the old Supervisor + self.oldSupervisor <- signer.storage.load<@FlowYieldVaultsSchedulerV1.Supervisor>(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + // cancel the scheduled transaction - deposits refund to the supervisor.feesCap + if let ref = &self.oldSupervisor as auth(FlowYieldVaultsSchedulerV1.Schedule) &FlowYieldVaultsSchedulerV1.Supervisor? { + Burner.burn(<-ref.cancelScheduledTransaction(refundReceiver: nil)) + } + // reconfigure a new Supervisor + FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured() + + // borrow the new Supervisor to schedule the next recurring execution + self.newSupervisor = signer.storage.borrow( + from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath + ) ?? panic("Could not borrow new Supervisor - check FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured()") + } + + execute { + Burner.burn(<-self.oldSupervisor) + self.newSupervisor.scheduleNextRecurringExecution( + recurringInterval: recurringInterval, + priority: FlowTransactionScheduler.Priority.Medium, + priorityRaw: priorityRaw, + executionEffort: executionEffort, + scanForStuck: scanForStuck + ) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/destroy_supervisor.cdc b/cadence/transactions/flow-yield-vaults/admin/destroy_supervisor.cdc new file mode 100644 index 00000000..48fbd33f --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/destroy_supervisor.cdc @@ -0,0 +1,30 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Destroys the global Supervisor, removing the stored Capability used for scheduling and cancelling any scheduled +/// transactions internally managed by the Supervisor. +transaction { + + let supervisor: @FlowYieldVaultsSchedulerV1.Supervisor? + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + let supervisorCap = signer.storage + .load>( + from: /storage/FlowYieldVaultsSupervisorCapability + ) + for controller in signer.capabilities.storage.getControllers(forPath: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) { + controller.delete() + } + self.supervisor <- signer.storage.load<@FlowYieldVaultsSchedulerV1.Supervisor>(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + if let ref = &self.supervisor as auth(FlowYieldVaultsSchedulerV1.Schedule) &FlowYieldVaultsSchedulerV1.Supervisor? { + Burner.burn(<-ref.cancelScheduledTransaction(refundReceiver: nil)) + } + } + + execute { + Burner.burn(<-self.supervisor) + } +} diff --git a/cadence/transactions/flow-yield-vaults/schedule_supervisor.cdc b/cadence/transactions/flow-yield-vaults/admin/schedule_supervisor.cdc similarity index 86% rename from cadence/transactions/flow-yield-vaults/schedule_supervisor.cdc rename to cadence/transactions/flow-yield-vaults/admin/schedule_supervisor.cdc index e27877ab..ce8d9530 100644 --- a/cadence/transactions/flow-yield-vaults/schedule_supervisor.cdc +++ b/cadence/transactions/flow-yield-vaults/admin/schedule_supervisor.cdc @@ -27,12 +27,12 @@ transaction( let payment: @FlowToken.Vault let handlerCap: Capability - prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { // Obtain the global Supervisor capability from the scheduler. This is // configured by calling FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured() // (typically via the setup_supervisor.cdc transaction). - self.handlerCap = FlowYieldVaultsSchedulerV1.getSupervisorCap() - ?? panic("Supervisor not configured") + self.handlerCap = signer.storage.copy>(from: /storage/FlowYieldVaultsSupervisorCapability) + ?? panic("Could not copy FlowYieldVaultsSupervisorCapability") let vaultRef = signer.storage .borrow(from: /storage/flowTokenVault) diff --git a/cadence/transactions/flow-yield-vaults/admin/set_auto_balancer_recurring_configs.cdc b/cadence/transactions/flow-yield-vaults/admin/set_auto_balancer_recurring_configs.cdc new file mode 100644 index 00000000..3be351fd --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/set_auto_balancer_recurring_configs.cdc @@ -0,0 +1,53 @@ +import "FungibleToken" +import "FlowToken" +import "FungibleTokenConnectors" +import "DeFiActions" +import "FlowTransactionScheduler" +import "FlowYieldVaultsAutoBalancers" +import "FlowYieldVaultsSchedulerRegistry" + +/// Sets the recurring config for all AutoBalancers tied to registered yVaults. +/// NOTE: This transaction is intended for beta-level use only. Iteration in `prepare` will fail with enough yVaults. +/// +/// @param interval: The interval at which to rebalance (in seconds) +/// @param priorityRaw: The priority of the rebalance (0=High, 1=Medium, 2=Low) +/// @param executionEffort: The execution effort of the rebalance (1-9999) +/// @param forceRebalance: The force rebalance flag (true=force rebalance, false=normal rebalance) +transaction( + interval: UInt64, + priorityRaw: UInt8, + executionEffort: UInt64, + forceRebalance: Bool +) { + + prepare(signer: auth(BorrowValue, CopyValue) &Account) { + let feeCapStoragePath = /storage/strategiesFeeSource + let fundingVault = signer.storage.copy>(from: feeCapStoragePath) + ?? panic("Could not find funding vault Capability at \(feeCapStoragePath)") + let txnFunder = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: fundingVault, + uniqueID: nil + ) + let priority = FlowTransactionScheduler.Priority(rawValue: priorityRaw) + ?? panic("Invalid priority: \(priorityRaw) - must be 0=High, 1=Medium, 2=Low") + for id in FlowYieldVaultsSchedulerRegistry.getPendingYieldVaultIDs() { + let path = FlowYieldVaultsAutoBalancers.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath + if let ab = signer.storage.borrow(from: path) { + DeFiActions.alignID( + toUpdate: &txnFunder as auth(DeFiActions.Extend) &{DeFiActions.IdentifiableStruct}, + with: ab + ) + let config = DeFiActions.AutoBalancerRecurringConfig( + interval: interval, + priority: priority, + executionEffort: executionEffort, + forceRebalance: forceRebalance, + txnFunder: txnFunder + ) + ab.setRecurringConfig(config) + } + } + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/set_default_exec_effort.cdc b/cadence/transactions/flow-yield-vaults/admin/set_default_exec_effort.cdc new file mode 100644 index 00000000..c1b0248e --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/set_default_exec_effort.cdc @@ -0,0 +1,22 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Sets the default execution effort for Supervisor self-rescheduling +/// +/// @param effort: The execution effort to set +transaction(effort: UInt64) { + + let supervisor: auth(FlowYieldVaultsSchedulerV1.Configure) &FlowYieldVaultsSchedulerV1.Supervisor + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + self.supervisor = signer.storage.borrow(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + ?? panic("Could not borrow Supervisor - check FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured()") + } + + execute { + self.supervisor.setDefaultExecutionEffort(effort) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/set_default_fee_margin_multiplier.cdc b/cadence/transactions/flow-yield-vaults/admin/set_default_fee_margin_multiplier.cdc new file mode 100644 index 00000000..e64d5c86 --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/set_default_fee_margin_multiplier.cdc @@ -0,0 +1,22 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Sets the default fee margin multiplier for Supervisor self-rescheduling +/// +/// @param multiplier: The fee margin multiplier to set +transaction(multiplier: UFix64) { + + let supervisor: auth(FlowYieldVaultsSchedulerV1.Configure) &FlowYieldVaultsSchedulerV1.Supervisor + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + self.supervisor = signer.storage.borrow(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + ?? panic("Could not borrow Supervisor - check FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured()") + } + + execute { + self.supervisor.setDefaultFeeMarginMultiplier(multiplier) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/set_default_min_fee_fallback.cdc b/cadence/transactions/flow-yield-vaults/admin/set_default_min_fee_fallback.cdc new file mode 100644 index 00000000..e4eed1a9 --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/set_default_min_fee_fallback.cdc @@ -0,0 +1,22 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Sets the default minimum fee fallback for Supervisor self-rescheduling +/// +/// @param fallback: The minimum fee fallback to set +transaction(fallback: UFix64) { + + let supervisor: auth(FlowYieldVaultsSchedulerV1.Configure) &FlowYieldVaultsSchedulerV1.Supervisor + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + self.supervisor = signer.storage.borrow(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + ?? panic("Could not borrow Supervisor - check FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured()") + } + + execute { + self.supervisor.setDefaultMinFeeFallback(fallback) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/set_default_priority.cdc b/cadence/transactions/flow-yield-vaults/admin/set_default_priority.cdc new file mode 100644 index 00000000..eecf3dea --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/set_default_priority.cdc @@ -0,0 +1,22 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Sets the default priority for Supervisor self-rescheduling +/// +/// @param priorityRaw: The raw priority value (UInt8) for data serialization (0=High, 1=Medium, 2=Low) +transaction(priorityRaw: UInt8) { + + let supervisor: auth(FlowYieldVaultsSchedulerV1.Configure) &FlowYieldVaultsSchedulerV1.Supervisor + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + self.supervisor = signer.storage.borrow(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + ?? panic("Could not borrow Supervisor - check FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured()") + } + + execute { + self.supervisor.setDefaultPriority(FlowTransactionScheduler.Priority(rawValue: priorityRaw)!) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/set_default_recurring_interval.cdc b/cadence/transactions/flow-yield-vaults/admin/set_default_recurring_interval.cdc new file mode 100644 index 00000000..6e1c07ce --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/set_default_recurring_interval.cdc @@ -0,0 +1,22 @@ +import "FlowYieldVaultsSchedulerV1" +import "FlowTransactionScheduler" +import "Burner" +import "FlowToken" +import "FungibleToken" + +/// Sets the default execution interval for Supervisor self-rescheduling +/// +/// @param interval: The execution effort to set +transaction(interval: UFix64) { + + let supervisor: auth(FlowYieldVaultsSchedulerV1.Configure) &FlowYieldVaultsSchedulerV1.Supervisor + + prepare(signer: auth(LoadValue, StorageCapabilities) &Account) { + self.supervisor = signer.storage.borrow(from: FlowYieldVaultsSchedulerV1.SupervisorStoragePath) + ?? panic("Could not borrow Supervisor - check FlowYieldVaultsSchedulerV1.ensureSupervisorConfigured()") + } + + execute { + self.supervisor.setDefaultRecurringInterval(interval) + } +}