Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion cadence/contracts/FlowALPRebalancerv1.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
42 changes: 40 additions & 2 deletions cadence/tests/paid_auto_balance_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ access(all) fun setup() {
let evts = Test.eventsOfType(Type<FlowALPRebalancerv1.CreatedRebalancer>())
let paidRebalancerUUID = evts[0] as! FlowALPRebalancerv1.CreatedRebalancer
createSupervisor(
signer: userAccount,
signer: userAccount,
cronExpression: "0 * * * *",
cronHandlerStoragePath: cronHandlerStoragePath,
keeperExecutionEffort: 1000,
executorExecutionEffort: 1000,
supervisorStoragePath: supervisorStoragePath
)

snapshot = getCurrentBlockHeight()
}

Expand Down Expand Up @@ -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<FlowALPRebalancerPaidv1.CreatedRebalancerPaid>())
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<FlowALPRebalancerv1.CreatedRebalancer>())
Test.assertEqual(1, evts.length)
Expand Down
18 changes: 18 additions & 0 deletions cadence/tests/test_helpers_rebalance.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions cadence/tests/transactions/rebalancer/change_paid_funder.cdc
Original file line number Diff line number Diff line change
@@ -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<auth(FungibleToken.Withdraw) &FlowToken.Vault>

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<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
/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)
}
}
Loading