diff --git a/cadence/contracts/FlowALPRebalancerv1.cdc b/cadence/contracts/FlowALPRebalancerv1.cdc index b4a50495..c65756a0 100644 --- a/cadence/contracts/FlowALPRebalancerv1.cdc +++ b/cadence/contracts/FlowALPRebalancerv1.cdc @@ -283,8 +283,8 @@ access(all) contract FlowALPRebalancerv1 { /// Update schedule and fee config. Cancels existing scheduled transactions and schedules the next run with the new config. access(Configure) fun setRecurringConfig(_ config: {RecurringConfig}) { - self.recurringConfig = config self.cancelAllScheduledTransactions() + self.recurringConfig = config let nextScheduledTimestamp = self.scheduleNextRebalance() if nextScheduledTimestamp == nil { panic("Failed to schedule next rebalance after setting recurring config") diff --git a/cadence/tests/paid_auto_balance_test.cdc b/cadence/tests/paid_auto_balance_test.cdc index 989aa44a..e55440c4 100644 --- a/cadence/tests/paid_auto_balance_test.cdc +++ b/cadence/tests/paid_auto_balance_test.cdc @@ -47,14 +47,14 @@ access(all) fun setup() { let evts = Test.eventsOfType(Type()) let paidRebalancerUUID = evts[0] as! FlowALPRebalancerv1.CreatedRebalancer createSupervisor( - signer: userAccount, + signer: userAccount, cronExpression: "0 * * * *", cronHandlerStoragePath: cronHandlerStoragePath, keeperExecutionEffort: 1000, executorExecutionEffort: 1000, supervisorStoragePath: supervisorStoragePath ) - + snapshot = getCurrentBlockHeight() } @@ -167,6 +167,44 @@ access(all) fun test_fix_reschedule_no_funds() { Test.assertEqual(2, evts.length) } +// FLO-17 regression: when setRecurringConfig is called, cancel must use the OLD config's funder +// so that pre-paid fees are refunded to the original payer, not the new funder. +access(all) fun test_flo17_refund_goes_to_old_funder_not_new_funder() { + // The rebalancer was created in setup() with protocolAccount as the txFunder. + // A scheduled transaction with fees pre-paid from protocolAccount already exists. + let createdEvts = Test.eventsOfType(Type()) + Test.assertEqual(1, createdEvts.length) + let e = createdEvts[0] as! FlowALPRebalancerPaidv1.CreatedRebalancerPaid + + // Create a new funder account — this should NOT receive the refund for previously paid fees. + let newFunderAccount = Test.createAccount() + let _ = mintFlow(to: newFunderAccount, amount: 100.0) + + let oldFunderBalanceBefore = getBalance(address: protocolAccount.address, vaultPublicPath: /public/flowTokenBalance)! + let newFunderBalanceBefore = getBalance(address: newFunderAccount.address, vaultPublicPath: /public/flowTokenBalance)! + + // Switch the recurring config to use newFunderAccount as the fee payer going forward. + // This calls setRecurringConfig, which cancels the existing scheduled tx and refunds its fee. + changePaidFunder( + adminSigner: protocolAccount, + newFunderSigner: newFunderAccount, + uuid: e.uuid, + interval: 100, + expectFailure: false + ) + + let oldFunderBalanceAfter = getBalance(address: protocolAccount.address, vaultPublicPath: /public/flowTokenBalance)! + let newFunderBalanceAfter = getBalance(address: newFunderAccount.address, vaultPublicPath: /public/flowTokenBalance)! + + // The pre-paid fee must be refunded to the OLD funder (protocolAccount), not the new one. + Test.assert( + oldFunderBalanceAfter > oldFunderBalanceBefore, + message: "FLO-17: old funder should receive refund on config change, balance before=\(oldFunderBalanceBefore) after=\(oldFunderBalanceAfter)" + ) + // New funder must not receive a windfall + Test.assert(newFunderBalanceBefore >= newFunderBalanceAfter) +} + access(all) fun test_change_recurring_config_as_user() { var evts = Test.eventsOfType(Type()) Test.assertEqual(1, evts.length) diff --git a/cadence/tests/test_helpers_rebalance.cdc b/cadence/tests/test_helpers_rebalance.cdc index 381998d7..6eaed8b2 100644 --- a/cadence/tests/test_helpers_rebalance.cdc +++ b/cadence/tests/test_helpers_rebalance.cdc @@ -39,6 +39,24 @@ fun addPaidRebalancerToSupervisor( Test.expect(setRes, Test.beSucceeded()) } +access(all) +fun changePaidFunder( + adminSigner: Test.TestAccount, + newFunderSigner: Test.TestAccount, + uuid: UInt64, + interval: UInt64, + expectFailure: Bool +) { + let txn = Test.Transaction( + code: Test.readFile("./transactions/rebalancer/change_paid_funder.cdc"), + authorizers: [adminSigner.address, newFunderSigner.address], + signers: [adminSigner, newFunderSigner], + arguments: [uuid, interval] + ) + let result = Test.executeTransaction(txn) + Test.expect(result, expectFailure ? Test.beFailed() : Test.beSucceeded()) +} + access(all) fun changePaidInterval( signer: Test.TestAccount, diff --git a/cadence/tests/transactions/rebalancer/change_paid_funder.cdc b/cadence/tests/transactions/rebalancer/change_paid_funder.cdc new file mode 100644 index 00000000..3831b6c0 --- /dev/null +++ b/cadence/tests/transactions/rebalancer/change_paid_funder.cdc @@ -0,0 +1,40 @@ +import "FungibleToken" +import "FungibleTokenConnectors" +import "FlowALPRebalancerv1" +import "FlowALPRebalancerPaidv1" +import "FlowToken" +import "FlowTransactionScheduler" + +// Changes the recurring config for a paid rebalancer, using a different account as txFunder. +// `admin` must hold FlowALPRebalancerPaidv1.Admin; `newFunder` provides the new fee vault. +transaction(uuid: UInt64, interval: UInt64) { + let adminCap: Capability<&FlowALPRebalancerPaidv1.Admin> + let newFunderVaultCap: Capability + + prepare(admin: auth(IssueStorageCapabilityController) &Account, newFunder: auth(IssueStorageCapabilityController) &Account) { + self.adminCap = admin.capabilities.storage.issue<&FlowALPRebalancerPaidv1.Admin>( + FlowALPRebalancerPaidv1.adminStoragePath + ) + assert(self.adminCap.check(), message: "Invalid admin capability") + + self.newFunderVaultCap = newFunder.capabilities.storage.issue( + /storage/flowTokenVault + ) + assert(self.newFunderVaultCap.check(), message: "Invalid new funder vault capability") + } + + execute { + let sinkSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, max: nil, vault: self.newFunderVaultCap, uniqueID: nil + ) + let config = FlowALPRebalancerv1.RecurringConfigImplv1( + interval: interval, + priority: FlowTransactionScheduler.Priority.Medium, + executionEffort: 1000, + estimationMargin: 1.05, + forceRebalance: false, + txFunder: sinkSource + ) + self.adminCap.borrow()!.updateRecurringConfig(uuid: uuid, recurringConfig: config) + } +}