-
Notifications
You must be signed in to change notification settings - Fork 2
Add automatic rebalancing contracts #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
e741071
8675a81
e202507
1a0a59f
11769e4
220271c
c8802a7
d5da5a5
6970361
31b6726
85683ba
6566beb
700ba93
9ac561b
26e80d4
0eb479c
ae8812c
9a296db
50ed5ec
e64034a
7d01ea8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| ## Updated Rebalance Architecture | ||
|
|
||
| The core philosophy is **decoupling**: each component operates independently with the least privilege necessary. | ||
|
|
||
| ### Key Principles | ||
|
|
||
| * **Isolation:** FCM, Rebalancer, and Supervisor are fully independent. | ||
| * **Least Privilege:** The Rebalancer can *only* trigger the `rebalance` function. | ||
| * **Resilience:** The `fixReschedule()` call is idempotent and permissionless, ensuring the system can recover without complex auth. | ||
|
|
||
| ### Rebalancer variants | ||
|
|
||
| There are two rebalancer types; they behave the same for triggering rebalances. | ||
|
|
||
| | | **Standard Rebalancer** | **Paid Rebalancer** | | ||
| |---|---|---| | ||
| | **Who pays** | User pays | Admin pays | | ||
| | **Configuration** | User can set it | Admin sets it | | ||
| | **Use case** | User wants full autonomy | Admin retains autonomy | | ||
| | **Who can withdraw** | Only user | Only user | | ||
|
|
||
| The paid rebalancer is otherwise the same: it holds a rebalance capability and runs on the same schedule/trigger model; only who pays and who controls config differ. | ||
|
|
||
| ### creating a position | ||
| ```mermaid | ||
| sequenceDiagram | ||
| actor anyone | ||
| participant FCMHelper as FCM<br/>Helper | ||
| participant FCM | ||
| participant AB as Rebalancer | ||
| participant Supervisor | ||
| anyone->>FCMHelper: createPosition() | ||
| FCMHelper->>FCM: createPosition() | ||
| FCMHelper->>AB: createRebalancer(rebalanceCapability) | ||
| FCMHelper->>Supervisor: addRebalancer(uuid) | ||
| ``` | ||
|
|
||
| ### while running | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant AB1 as AutoRebalancer1 | ||
| participant FCM | ||
| participant AB2 as AutoRebalancer2 | ||
| participant SUP as Supervisor | ||
| loop every x min | ||
| AB1->>FCM: rebalance() | ||
| end | ||
| loop every y min | ||
| AB2->>FCM: rebalance() | ||
| end | ||
| loop every z min | ||
| SUP->>AB2: fixReschedule() | ||
| SUP->>AB1: fixReschedule() | ||
| end | ||
| ``` |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,157 @@ | ||||||
| import "FlowCreditMarket" | ||||||
| import "FlowCreditMarketRebalancerV1" | ||||||
| import "FlowTransactionScheduler" | ||||||
|
|
||||||
| // FlowCreditMarketRebalancerPaidV1 — Managed rebalancer service for Flow Credit Market positions. | ||||||
| // | ||||||
| // This contract hosts scheduled rebalancers on behalf of users. Instead of users storing and | ||||||
| // configuring Rebalancer resources themselves, they call createPaidRebalancer with a position | ||||||
| // rebalance capability and receive a lightweight RebalancerPaid resource. The contract stores | ||||||
| // the underlying Rebalancer, wires it to the FlowTransactionScheduler, and applies the | ||||||
| // defaultRecurringConfig (interval, priority, txnFunder, etc.). | ||||||
| // The txnFunder set by the admin will be used to fund the rebalance transactions for the users. | ||||||
| // Users can fixReschedule by UUID or delete their RebalancerPaid to stop and remove the rebalancer. | ||||||
| // Admins control the default config and can update or remove individual paid rebalancers. | ||||||
| // See RebalanceArchitecture.md for an architecture overview. | ||||||
| access(all) contract FlowCreditMarketRebalancerPaidV1 { | ||||||
|
|
||||||
| access(all) event CreatedRebalancerPaid(uuid: UInt64) | ||||||
| access(all) event RemovedRebalancerPaid(uuid: UInt64) | ||||||
| access(all) event UpdatedDefaultRecurringConfig( | ||||||
| interval: UInt64, | ||||||
| priority: UInt8, | ||||||
| executionEffort: UInt64, | ||||||
| estimationMargin: UFix64, | ||||||
| forceRebalance: Bool, | ||||||
| ) | ||||||
|
|
||||||
| // default recurring config for all newly created paid rebalancers | ||||||
| // this entails the txnFunder, which will be used to fund the rebalance transactions | ||||||
| access(all) var defaultRecurringConfig: FlowCreditMarketRebalancerV1.RecurringConfig? | ||||||
| access(all) var adminStoragePath: StoragePath | ||||||
|
|
||||||
| access(all) fun createPaidRebalancer( | ||||||
| positionRebalanceCapability: Capability<auth(FlowCreditMarket.ERebalance) &FlowCreditMarket.Position>, | ||||||
| ): @RebalancerPaid { | ||||||
| assert(positionRebalanceCapability.check(), message: "Invalid position rebalance capability") | ||||||
| let rebalancer <- FlowCreditMarketRebalancerV1.createRebalancer( | ||||||
| recurringConfig: self.defaultRecurringConfig!, | ||||||
| positionRebalanceCapability: positionRebalanceCapability | ||||||
| ) | ||||||
| let uuid = rebalancer.uuid | ||||||
| self.storeRebalancer(rebalancer: <-rebalancer) | ||||||
| self.setSelfCapability(uuid: uuid).fixReschedule() | ||||||
| emit CreatedRebalancerPaid(uuid: uuid) | ||||||
| return <- create RebalancerPaid(rebalancerUUID: uuid) | ||||||
| } | ||||||
|
|
||||||
| access(all) resource Admin { | ||||||
| // update the default recurring config for all newly created paid rebalancers | ||||||
| access(all) fun updateDefaultRecurringConfig(recurringConfig: FlowCreditMarketRebalancerV1.RecurringConfig) { | ||||||
| FlowCreditMarketRebalancerPaidV1.defaultRecurringConfig = recurringConfig | ||||||
|
holyfuchs marked this conversation as resolved.
Outdated
|
||||||
| emit UpdatedDefaultRecurringConfig( | ||||||
| interval: recurringConfig.interval, | ||||||
| priority: recurringConfig.priority.rawValue, | ||||||
| executionEffort: recurringConfig.executionEffort, | ||||||
| estimationMargin: recurringConfig.estimationMargin, | ||||||
| forceRebalance: recurringConfig.forceRebalance, | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| access(all) fun borrowAuthorizedRebalancer( | ||||||
| uuid: UInt64, | ||||||
| ): auth(FlowCreditMarket.ERebalance, FlowCreditMarketRebalancerV1.Rebalancer.Configure) &FlowCreditMarketRebalancerV1.Rebalancer? { | ||||||
| return FlowCreditMarketRebalancerPaidV1.borrowRebalancer(uuid: uuid) | ||||||
| } | ||||||
|
|
||||||
| access(all) fun updateRecurringConfig( | ||||||
| uuid: UInt64, | ||||||
| recurringConfig: FlowCreditMarketRebalancerV1.RecurringConfig) | ||||||
| { | ||||||
| let rebalancer = FlowCreditMarketRebalancerPaidV1.borrowRebalancer(uuid: uuid)! | ||||||
| rebalancer.setRecurringConfig(recurringConfig) | ||||||
| } | ||||||
|
|
||||||
| access(account) fun removePaidRebalancer(uuid: UInt64) { | ||||||
| FlowCreditMarketRebalancerPaidV1.removePaidRebalancer(uuid: uuid) | ||||||
|
holyfuchs marked this conversation as resolved.
Outdated
|
||||||
| emit RemovedRebalancerPaid(uuid: uuid) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| access(all) entitlement Delete | ||||||
|
|
||||||
| access(all) resource RebalancerPaid { | ||||||
| // the UUID of the rebalancer this resource is associated with | ||||||
| access(all) var rebalancerUUID : UInt64 | ||||||
|
|
||||||
| init(rebalancerUUID: UInt64) { | ||||||
| self.rebalancerUUID = rebalancerUUID | ||||||
| } | ||||||
|
|
||||||
| access(Delete) fun delete() { | ||||||
| FlowCreditMarketRebalancerPaidV1.removePaidRebalancer(uuid: self.rebalancerUUID) | ||||||
| } | ||||||
|
|
||||||
| access(all) fun fixReschedule() { | ||||||
| FlowCreditMarketRebalancerPaidV1.fixReschedule(uuid: self.rebalancerUUID) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| access(all) fun fixReschedule( | ||||||
| uuid: UInt64, | ||||||
| ) { | ||||||
| let rebalancer = FlowCreditMarketRebalancerPaidV1.borrowRebalancer(uuid: uuid)! | ||||||
| rebalancer.fixReschedule() | ||||||
| } | ||||||
|
|
||||||
| access(all) view fun getPaidRebalancerPath( | ||||||
| uuid: UInt64, | ||||||
| ): StoragePath { | ||||||
| return StoragePath(identifier: "FlowCreditMarket.RebalancerPaidV1\(uuid)")! | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add account address
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use a shorter name "FCMRebalancerPaidV1_(self.account.address)_(uuid)"?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I'm a bit confused why do we need this method. This function is not being used anywhere, and we have a getPath method, but it's internal ( If it's a public method to be used by other contracts, better add comments explaining the usage.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it will be
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't fully understand how / when its needed so not sure what comment to put on it. |
||||||
| } | ||||||
|
|
||||||
| access(self) fun borrowRebalancer( | ||||||
| uuid: UInt64, | ||||||
| ): auth(FlowCreditMarket.ERebalance, FlowCreditMarketRebalancerV1.Rebalancer.Configure) &FlowCreditMarketRebalancerV1.Rebalancer? { | ||||||
| return self.account.storage.borrow<auth(FlowCreditMarket.ERebalance, FlowCreditMarketRebalancerV1.Rebalancer.Configure) &FlowCreditMarketRebalancerV1.Rebalancer>(from: self.getPath(uuid: uuid)) | ||||||
| } | ||||||
|
|
||||||
| access(self) fun removePaidRebalancer(uuid: UInt64) { | ||||||
| let rebalancer <- self.account.storage.load<@FlowCreditMarketRebalancerV1.Rebalancer>(from: self.getPath(uuid: uuid)) | ||||||
| rebalancer?.cancelAllScheduledTransactions() | ||||||
| destroy <- rebalancer | ||||||
| } | ||||||
|
|
||||||
| access(self) fun storeRebalancer( | ||||||
| rebalancer: @FlowCreditMarketRebalancerV1.Rebalancer, | ||||||
| ) { | ||||||
| let path = self.getPath(uuid: rebalancer.uuid) | ||||||
| self.account.storage.save(<-rebalancer, to: path) | ||||||
| } | ||||||
|
|
||||||
| // issue and set the capability that the scheduler will use to call back into this rebalancer | ||||||
| access(self) fun setSelfCapability( | ||||||
| uuid: UInt64, | ||||||
| ) : auth(FlowCreditMarket.ERebalance, FlowCreditMarketRebalancerV1.Rebalancer.Configure) &FlowCreditMarketRebalancerV1.Rebalancer { | ||||||
| let selfCap = self.account.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(self.getPath(uuid: uuid)) | ||||||
| // The Rebalancer is stored in the contract storage (storeRebalancer), | ||||||
| // it needs a capability pointing to itself to pass to the scheduler. | ||||||
| // We issue this capability here and set it on the Rebalancer, so that when | ||||||
| // fixReschedule is called, the Rebalancer can pass it to the transaction scheduler | ||||||
| // as a callback for executing scheduled rebalances. | ||||||
| let rebalancer = self.borrowRebalancer(uuid: uuid)! | ||||||
| rebalancer.setSelfCapability(selfCap) | ||||||
| return rebalancer | ||||||
| } | ||||||
|
|
||||||
| access(self) view fun getPath(uuid: UInt64): StoragePath { | ||||||
| return StoragePath(identifier: "FlowCreditMarket.RebalancerV1\(uuid)")! | ||||||
| } | ||||||
|
|
||||||
| init() { | ||||||
| self.adminStoragePath = StoragePath(identifier: "FlowCreditMarket.RebalancerPaidV1.Admin")! | ||||||
| self.defaultRecurringConfig = nil | ||||||
| let admin <- create Admin() | ||||||
| self.account.storage.save(<-admin, to: self.adminStoragePath) | ||||||
| } | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.