Skip to content

Conversation

@sisyphusSmiling
Copy link
Contributor

@sisyphusSmiling sisyphusSmiling commented Dec 18, 2025

Description

  • Adds scheduled rebalancing support with individualized RebalanceHandlers wired into a central per-Pool Registry
  • Support for per-Position recurring configuration as an extensible path
  • Adds support in FCM Pool for Position ID visibility to ensure non-existent pids are not registered
  • Tests passing, but I wasn't able to add tests specific to this new setup

Lifecycle

Creation

  1. Position is created
  2. Pool registers the PID in its associated Registry in account storage, optionally passing a PID-specific recurring config
  3. Registry stores the PID in its registry
  4. Registry initializes a RebalanceHandler in storage with paths derived from the Pool UUID & the Position ID
  5. Registry schedules the first scheduled rebalance based on either the custom or default recurring config

Updating Default Recurring Config

  1. Run the set_default_rebalance_recurring_config.cdc transaction with the desired values
  2. Registry stores the updated config as default
  3. Any successive RebalanceHandler scheduled executions are scheduled using the default (unless they have a custom config stored)

Steps to Initialize from Current State

  1. Update FCM contract
  2. Deploy FCMRegistry contract to FMC account (see contract init for starting default config values) - Registry is initialized in account storage
  3. Run backfill_positions.cdc transaction (--compute-limit 9999) which will register all existing PIDs in the Registry. This should spin up RebalanceHandlers for all existing Positions
  • If you run into computation limits (possible on Testnet especially)
    1. Get all Position IDs (see get_position_ids.cdc script)
    2. Pass in segments of the returned ID array in to backfill_specific_positions.cdc transaction to backfill in partitions. Again, the transaction will fail if one of the provided IDs does not exist in the Pool as a failsafe.

For contributor use:

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the standards mentioned here.
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels
  • Go WTF 💚

@sisyphusSmiling sisyphusSmiling changed the title Gio/scheduled rebalance Add scheduled rebalancing Dec 18, 2025
@sisyphusSmiling sisyphusSmiling marked this pull request as ready for review December 19, 2025 01:27
@sisyphusSmiling sisyphusSmiling requested a review from a team as a code owner December 19, 2025 01:27
@sisyphusSmiling sisyphusSmiling self-assigned this Dec 19, 2025
@sisyphusSmiling sisyphusSmiling added the enhancement New feature or request label Dec 19, 2025
}

let health = self.positionHealth(pid: pid)
let health: UFix128 = self.positionHealth(pid: pid)
Copy link
Member

Choose a reason for hiding this comment

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

Type can be inferred

Suggested change
let health: UFix128 = self.positionHealth(pid: pid)
let health = self.positionHealth(pid: pid)

/// added in the future.
access(all) resource Registry : FlowCreditMarket.IRegistry {
/// A map of registered positions by their Position ID
access(all) let registeredPositions: {UInt64: Bool}
Copy link
Member

Choose a reason for hiding this comment

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

Does this act like a set, where only the keys matter? Or can there be entries with true and false value?

If the former, maybe switch the value type to Void and use self.registeredPositions[pid] = ()

Comment on lines +175 to +185
if viewType == Type<StoragePath>() {
return FlowCreditMarketRegistry.deriveRebalanceHandlerStoragePath(poolUUID: self.poolUUID, positionID: self.positionID)
} else if viewType == Type<PublicPath>() {
return FlowCreditMarketRegistry.deriveRebalanceHandlerPublicPath(poolUUID: self.poolUUID, positionID: self.positionID)
} else if viewType == Type<MetadataViews.Display>() {
return MetadataViews.Display(
name: "Flow Credit Market Pool Position Rebalance Scheduled Transaction Handler",
description: "Scheduled Transaction Handler that can execute rebalance transactions on behalf of a Flow Credit Market Pool with UUID \(self.poolUUID) and Position ID \(self.positionID)",
thumbnail: MetadataViews.HTTPFile(url: "")
)
}
Copy link
Member

Choose a reason for hiding this comment

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

Use switch-case instead for repeated test of viewType:

Suggested change
if viewType == Type<StoragePath>() {
return FlowCreditMarketRegistry.deriveRebalanceHandlerStoragePath(poolUUID: self.poolUUID, positionID: self.positionID)
} else if viewType == Type<PublicPath>() {
return FlowCreditMarketRegistry.deriveRebalanceHandlerPublicPath(poolUUID: self.poolUUID, positionID: self.positionID)
} else if viewType == Type<MetadataViews.Display>() {
return MetadataViews.Display(
name: "Flow Credit Market Pool Position Rebalance Scheduled Transaction Handler",
description: "Scheduled Transaction Handler that can execute rebalance transactions on behalf of a Flow Credit Market Pool with UUID \(self.poolUUID) and Position ID \(self.positionID)",
thumbnail: MetadataViews.HTTPFile(url: "")
)
}
switch viewType {
case Type<StoragePath>():
return FlowCreditMarketRegistry.deriveRebalanceHandlerStoragePath(
poolUUID: self.poolUUID,
positionID: self.positionID
)
case Type<PublicPath>():
return FlowCreditMarketRegistry.deriveRebalanceHandlerPublicPath(
poolUUID: self.poolUUID,
positionID: self.positionID
)
case Type<MetadataViews.Display>():
return MetadataViews.Display(
name: "Flow Credit Market Pool Position Rebalance Scheduled Transaction Handler",
description: "Scheduled Transaction Handler that can execute rebalance transactions on behalf of a Flow Credit Market Pool with UUID \(self.poolUUID) and Position ID \(self.positionID)",
thumbnail: MetadataViews.HTTPFile(url: "")
)
}

Comment on lines +325 to +328
let ref = self.borrowScheduledTransaction(id: id)
if ref != nil && ref!.status() != FlowTransactionScheduler.Status.Scheduled {
destroy <- self.scheduledTxns.remove(key: id)
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let ref = self.borrowScheduledTransaction(id: id)
if ref != nil && ref!.status() != FlowTransactionScheduler.Status.Scheduled {
destroy <- self.scheduledTxns.remove(key: id)
}
if let ref = self.borrowScheduledTransaction(id: id) {
if ref.status() != FlowTransactionScheduler.Status.Scheduled {
destroy <- self.scheduledTxns.remove(key: id)
}
}

@nialexsan nialexsan self-assigned this Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants