Skip to content

Commit 8fd22c5

Browse files
Merge branch 'main' into UlianaAndrukhiv/210-uncollected-protocol-fees-fix
2 parents 3592cc7 + 9277c8d commit 8fd22c5

3 files changed

Lines changed: 53 additions & 6 deletions

File tree

cadence/contracts/FlowALPRebalancerPaidv1.cdc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,21 @@ access(all) contract FlowALPRebalancerPaidv1 {
108108

109109
/// Idempotent: if no next run is scheduled, try to schedule it (e.g. after a transient failure).
110110
access(all) fun fixReschedule() {
111-
FlowALPRebalancerPaidv1.fixReschedule(positionID: self.positionID)
111+
let _ = FlowALPRebalancerPaidv1.fixReschedule(positionID: self.positionID)
112112
}
113113
}
114114

115115
/// Idempotent: for the given paid rebalancer, if there is no scheduled transaction, schedule the next run.
116116
/// Callable by anyone (e.g. the Supervisor or the RebalancerPaid owner).
117+
/// Returns true if the rebalancer was found and processed, false if the UUID is stale (rebalancer no longer exists).
117118
access(all) fun fixReschedule(
118119
positionID: UInt64,
119-
) {
120-
let rebalancer = FlowALPRebalancerPaidv1.borrowRebalancer(positionID: positionID)!
121-
rebalancer.fixReschedule()
120+
): Bool {
121+
if let rebalancer = FlowALPRebalancerPaidv1.borrowRebalancer(positionID: positionID) {
122+
rebalancer.fixReschedule()
123+
return true
124+
}
125+
return false
122126
}
123127

124128
/// Storage path where a user would store their RebalancerPaid for the given position (convention for discovery).

cadence/contracts/FlowALPSupervisorv1.cdc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,15 @@ access(all) contract FlowALPSupervisorv1 {
4545
}
4646

4747
/// Scheduler callback: on each tick, call fixReschedule on every registered paid rebalancer,
48-
/// recovering any that failed to schedule their next transaction.
48+
/// recovering any that failed to schedule their next transaction. Stale UUIDs (rebalancer
49+
/// deleted without being removed from this set) are pruned automatically.
4950
access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
5051
emit Executed(id: id)
5152
for positionID in self.paidRebalancers.keys {
52-
FlowALPRebalancerPaidv1.fixReschedule(positionID: positionID)
53+
let found = FlowALPRebalancerPaidv1.fixReschedule(positionID: positionID)
54+
if !found {
55+
let _ = self.removePaidRebalancer(positionID: positionID)
56+
}
5357
}
5458
}
5559
}

cadence/tests/paid_auto_balance_test.cdc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,45 @@ access(all) fun test_supervisor_executed() {
311311
Test.assertEqual(2, evts.length)
312312
}
313313

314+
/// Regression test for FLO-27: if a paid rebalancer is deleted without removing its UUID from
315+
/// the Supervisor's set, the next Supervisor tick must NOT panic. Before the fix,
316+
/// fixReschedule(uuid:) force-unwrapped borrowRebalancer(uuid)! which panicked on a stale UUID,
317+
/// reverting the whole executeTransaction and blocking recovery for all other rebalancers.
318+
access(all) fun test_supervisor_stale_uuid_does_not_panic() {
319+
// Get the UUID of the paid rebalancer created during setup.
320+
let createdEvts = Test.eventsOfType(Type<FlowALPRebalancerv1.CreatedRebalancer>())
321+
Test.assertEqual(1, createdEvts.length)
322+
let created = createdEvts[0] as! FlowALPRebalancerv1.CreatedRebalancer
323+
324+
// Register the UUID with the Supervisor so it will call fixReschedule on it each tick.
325+
addPaidRebalancerToSupervisor(signer: userAccount, positionID: created.positionID, supervisorStoragePath: supervisorStoragePath)
326+
327+
// Delete the paid rebalancer WITHOUT removing its UUID from the Supervisor — this leaves a
328+
// stale UUID in the Supervisor's paidRebalancers set, simulating the FLO-27 bug scenario.
329+
deletePaidRebalancer(signer: userAccount, paidRebalancerStoragePath: paidRebalancerStoragePath)
330+
331+
// Advance time to trigger the Supervisor's scheduled tick.
332+
Test.moveTime(by: 60.0 * 60.0)
333+
Test.commitBlock()
334+
335+
// The Supervisor must have executed without panicking. If fixReschedule force-unwrapped
336+
// the missing rebalancer the entire transaction would revert and Executed would not be emitted.
337+
let executedEvts = Test.eventsOfType(Type<FlowALPSupervisorv1.Executed>())
338+
Test.assert(executedEvts.length >= 1, message: "Supervisor should have executed at least 1 time")
339+
340+
// The stale UUID must have been pruned from the Supervisor's set.
341+
let removedEvts = Test.eventsOfType(Type<FlowALPSupervisorv1.RemovedPaidRebalancer>())
342+
Test.assertEqual(1, removedEvts.length)
343+
let removed = removedEvts[0] as! FlowALPSupervisorv1.RemovedPaidRebalancer
344+
Test.assertEqual(created.positionID, removed.positionID)
345+
346+
// A second tick should not emit another RemovedPaidRebalancer — the UUID was already cleaned up.
347+
Test.moveTime(by: 60.0 * 60.0)
348+
Test.commitBlock()
349+
let removedEvts2 = Test.eventsOfType(Type<FlowALPSupervisorv1.RemovedPaidRebalancer>())
350+
Test.assertEqual(1, removedEvts2.length)
351+
}
352+
314353
access(all) fun test_supervisor() {
315354
Test.moveTime(by: 100.0)
316355
Test.commitBlock()

0 commit comments

Comments
 (0)