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
99 changes: 53 additions & 46 deletions cadence/contracts/FlowALPRebalancerPaidv1.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import "FlowTransactionScheduler"

// FlowALPRebalancerPaidv1 — Managed rebalancer service for Flow ALP positions.
//
// Intended for use by the protocol operators only. 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 defaultRecurringConfig (interval, priority, txFunder, etc.).
// The admin's txFunder in that config is used to pay for rebalance transactions. Users can
// fixReschedule (via their RebalancerPaid) or delete RebalancerPaid to stop. Admins control the
// default config and can update or remove individual paid rebalancers. See RebalanceArchitecture.md.
// This contract hosts scheduled rebalancers on behalf of users. Anyone may call createPaidRebalancer
// (permissionless): pass a position rebalance capability and receive a lightweight RebalancerPaid
// resource. The contract stores the underlying Rebalancer, wires it to the FlowTransactionScheduler,
// and applies defaultRecurringConfig (interval, priority, txFunder, etc.).
// The admin's txFunder is used to pay for rebalance transactions. We rely on 2 things to limit how funds
// can be spent indirectly by used by creating rebalancers in this way:
// 1. This contract enforces that only one rebalancer can be created per position.
// 2. FlowALP enforces a minimum economic value per position.
// Users can fixReschedule (via their RebalancerPaid) or delete RebalancerPaid to stop. Admins control the default config and can update or remove individual paid rebalancers. See RebalanceArchitecture.md.
access(all) contract FlowALPRebalancerPaidv1 {

access(all) event CreatedRebalancerPaid(uuid: UInt64)
access(all) event RemovedRebalancerPaid(uuid: UInt64)
access(all) event CreatedRebalancerPaid(positionID: UInt64)
access(all) event RemovedRebalancerPaid(positionID: UInt64)
access(all) event UpdatedDefaultRecurringConfig(
interval: UInt64,
priority: UInt8,
Expand All @@ -30,22 +31,24 @@ access(all) contract FlowALPRebalancerPaidv1 {
access(all) var defaultRecurringConfig: {FlowALPRebalancerv1.RecurringConfig}?
access(all) var adminStoragePath: StoragePath

/// Create a paid rebalancer for the given position. Uses defaultRecurringConfig (must be set).
/// Returns a RebalancerPaid resource; the underlying Rebalancer is stored in this contract and
/// the first run is scheduled. Caller should register the returned uuid with a Supervisor.
/// Create a paid rebalancer for the given position. Permissionless: anyone may call this.
/// Uses defaultRecurringConfig (must be set by Admin). Returns a RebalancerPaid resource; the
/// underlying Rebalancer is stored in this contract and the first run is scheduled. Caller should
/// register the returned positionID with a Supervisor.
access(all) fun createPaidRebalancer(
positionRebalanceCapability: Capability<auth(FlowALPModels.ERebalance) &FlowALPv0.Position>,
): @RebalancerPaid {
assert(positionRebalanceCapability.check(), message: "Invalid position rebalance capability")
let positionID = positionRebalanceCapability.borrow()!.id
let rebalancer <- FlowALPRebalancerv1.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)
// will panic if the rebalancer already exists
self.storeRebalancer(rebalancer: <-rebalancer, positionID: positionID)
self.setSelfCapability(positionID: positionID).fixReschedule()
emit CreatedRebalancerPaid(positionID: positionID)
return <- create RebalancerPaid(positionID: positionID)
}

/// Admin resource: controls default config and per-rebalancer config; can remove paid rebalancers.
Expand All @@ -64,24 +67,24 @@ access(all) contract FlowALPRebalancerPaidv1 {

/// Borrow a paid rebalancer with Configure and ERebalance auth (e.g. for setRecurringConfig or rebalance).
access(all) fun borrowAuthorizedRebalancer(
uuid: UInt64,
positionID: UInt64,
): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? {
return FlowALPRebalancerPaidv1.borrowRebalancer(uuid: uuid)
return FlowALPRebalancerPaidv1.borrowRebalancer(positionID: positionID)
}

/// Update the RecurringConfig for a specific paid rebalancer (interval, txFunder, etc.).
access(all) fun updateRecurringConfig(
uuid: UInt64,
positionID: UInt64,
recurringConfig: {FlowALPRebalancerv1.RecurringConfig})
{
let rebalancer = FlowALPRebalancerPaidv1.borrowRebalancer(uuid: uuid)!
let rebalancer = FlowALPRebalancerPaidv1.borrowRebalancer(positionID: positionID)!
rebalancer.setRecurringConfig(recurringConfig)
}

/// Remove a paid rebalancer: cancel scheduled transactions (refund to txFunder) and destroy it.
access(account) fun removePaidRebalancer(uuid: UInt64) {
FlowALPRebalancerPaidv1.removePaidRebalancer(uuid: uuid)
emit RemovedRebalancerPaid(uuid: uuid)
access(account) fun removePaidRebalancer(positionID: UInt64) {
FlowALPRebalancerPaidv1.removePaidRebalancer(positionID: positionID)
emit RemovedRebalancerPaid(positionID: positionID)
}
}

Expand All @@ -90,76 +93,80 @@ access(all) contract FlowALPRebalancerPaidv1 {
/// User's handle to a paid rebalancer. Allows fixReschedule (recover if scheduling failed) or
/// delete (stop and remove the rebalancer; caller should also remove from Supervisor).
access(all) resource RebalancerPaid {
// the UUID of the rebalancer this resource is associated with
access(all) var rebalancerUUID : UInt64
/// The position id (from positionRebalanceCapability) this paid rebalancer is associated with.
access(all) var positionID: UInt64

init(rebalancerUUID: UInt64) {
self.rebalancerUUID = rebalancerUUID
init(positionID: UInt64) {
self.positionID = positionID
}

/// Stop and remove the paid rebalancer; scheduled transactions are cancelled and fees refunded to the admin txFunder.
access(Delete) fun delete() {
FlowALPRebalancerPaidv1.removePaidRebalancer(uuid: self.rebalancerUUID)
FlowALPRebalancerPaidv1.removePaidRebalancer(positionID: self.positionID)
}

/// Idempotent: if no next run is scheduled, try to schedule it (e.g. after a transient failure).
access(all) fun fixReschedule() {
FlowALPRebalancerPaidv1.fixReschedule(uuid: self.rebalancerUUID)
FlowALPRebalancerPaidv1.fixReschedule(positionID: self.positionID)
}
}

/// Idempotent: for the given paid rebalancer, if there is no scheduled transaction, schedule the next run.
/// Callable by anyone (e.g. the Supervisor or the RebalancerPaid owner).
access(all) fun fixReschedule(
uuid: UInt64,
positionID: UInt64,
) {
let rebalancer = FlowALPRebalancerPaidv1.borrowRebalancer(uuid: uuid)!
let rebalancer = FlowALPRebalancerPaidv1.borrowRebalancer(positionID: positionID)!
rebalancer.fixReschedule()
}

/// Storage path where a user would store their RebalancerPaid for the given uuid (convention for discovery).
/// Storage path where a user would store their RebalancerPaid for the given position (convention for discovery).
access(all) view fun getPaidRebalancerPath(
uuid: UInt64,
positionID: UInt64,
): StoragePath {
return StoragePath(identifier: "FlowALP.RebalancerPaidv1_\(self.account.address)_\(uuid)")!
return StoragePath(identifier: "FlowALP.RebalancerPaidv1_\(self.account.address)_\(positionID)")!
}

access(self) fun borrowRebalancer(
uuid: UInt64,
positionID: UInt64,
): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? {
return self.account.storage.borrow<auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer>(from: self.getPath(uuid: uuid))
return self.account.storage.borrow<auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer>(from: self.getPath(positionID: positionID))
}

access(self) fun removePaidRebalancer(uuid: UInt64) {
let rebalancer <- self.account.storage.load<@FlowALPRebalancerv1.Rebalancer>(from: self.getPath(uuid: uuid))
access(self) fun removePaidRebalancer(positionID: UInt64) {
let rebalancer <- self.account.storage.load<@FlowALPRebalancerv1.Rebalancer>(from: self.getPath(positionID: positionID))
rebalancer?.cancelAllScheduledTransactions()
destroy <- rebalancer
}

access(self) fun storeRebalancer(
rebalancer: @FlowALPRebalancerv1.Rebalancer,
positionID: UInt64,
) {
let path = self.getPath(uuid: rebalancer.uuid)
let path = self.getPath(positionID: positionID)
if self.account.storage.borrow<&FlowALPRebalancerv1.Rebalancer>(from: path) != nil {
panic("rebalancer already exists")
}
self.account.storage.save(<-rebalancer, to: path)
}

/// Issue a capability to the stored Rebalancer and set it on the Rebalancer so it can pass itself to the scheduler as the execute callback.
access(self) fun setSelfCapability(
uuid: UInt64,
positionID: UInt64,
) : auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer {
let selfCap = self.account.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(self.getPath(uuid: uuid))
let selfCap = self.account.capabilities.storage.issue<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>(self.getPath(positionID: positionID))
// 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)!
let rebalancer = self.borrowRebalancer(positionID: positionID)!
rebalancer.setSelfCapability(selfCap)
return rebalancer
}

access(self) view fun getPath(uuid: UInt64): StoragePath {
return StoragePath(identifier: "FlowALP.RebalancerPaidv1\(uuid)")!
access(self) view fun getPath(positionID: UInt64): StoragePath {
return StoragePath(identifier: "FlowALP.RebalancerPaidv1\(positionID)")!
}

init() {
Expand Down
36 changes: 18 additions & 18 deletions cadence/contracts/FlowALPSupervisorv1.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,42 @@ import "FlowALPRebalancerPaidv1"
// FlowALPSupervisorv1 — Cron-based recovery for paid rebalancers.
//
// Intended for use by the protocol operators only. The Supervisor is a TransactionHandler
// that runs on a schedule (e.g. cron). On each tick it calls fixReschedule(uuid) on every
// registered paid rebalancer UUID. That recovers rebalancers that failed to schedule their
// that runs on a schedule (e.g. cron). On each tick it calls fixReschedule(positionID) on every
// registered paid rebalancer position ID. That recovers rebalancers that failed to schedule their
// next run (e.g. temporary lack of funds), so they do not stay stuck. See RebalanceArchitecture.md.
access(all) contract FlowALPSupervisorv1 {

access(all) event Executed(id: UInt64)
access(all) event AddedPaidRebalancer(uuid: UInt64)
access(all) event RemovedPaidRebalancer(uuid: UInt64)
access(all) event AddedPaidRebalancer(positionID: UInt64)
access(all) event RemovedPaidRebalancer(positionID: UInt64)

/// Supervisor holds a set of paid rebalancer UUIDs and, when the scheduler invokes it,
/// calls FlowALPRebalancerPaidv1.fixReschedule(uuid) for each. The owner must
/// Supervisor holds a set of paid rebalancer position IDs and, when the scheduler invokes it,
/// calls FlowALPRebalancerPaidv1.fixReschedule(positionID) for each. The owner must
/// register the Supervisor with the FlowTransactionScheduler and add paid rebalancer
/// UUIDs when users create them (and remove when they are deleted).
/// position IDs when users create them (and remove when they are deleted).
access(all) resource Supervisor: FlowTransactionScheduler.TransactionHandler {

/// Set of paid rebalancer UUIDs to nudge each tick (Bool value unused; map used as set).
/// Set of paid rebalancer position IDs to nudge each tick (Bool value unused; map used as set).
access(all) let paidRebalancers: {UInt64: Bool}

init() {
self.paidRebalancers = {}
}

/// Register a paid rebalancer by UUID so the Supervisor will call fixReschedule on it each tick.
/// Register a paid rebalancer by position ID so the Supervisor will call fixReschedule on it each tick.
/// Call this when a user creates a paid rebalancer (e.g. after createPaidRebalancer).
access(all) fun addPaidRebalancer(uuid: UInt64) {
self.paidRebalancers[uuid] = true
emit AddedPaidRebalancer(uuid: uuid)
access(all) fun addPaidRebalancer(positionID: UInt64) {
self.paidRebalancers[positionID] = true
emit AddedPaidRebalancer(positionID: positionID)
}

/// Remove a paid rebalancer from the set. Call when the rebalancer is removed (e.g. user
/// deleted RebalancerPaid) so the Supervisor stops calling fixReschedule for it.
/// Returns the removed value if the uuid was present, nil otherwise.
access(all) fun removePaidRebalancer(uuid: UInt64): Bool? {
let removed = self.paidRebalancers.remove(key: uuid)
/// Returns the removed value if the positionID was present, nil otherwise.
access(all) fun removePaidRebalancer(positionID: UInt64): Bool? {
let removed = self.paidRebalancers.remove(key: positionID)
if removed != nil {
emit RemovedPaidRebalancer(uuid: uuid)
emit RemovedPaidRebalancer(positionID: positionID)
}
return removed
}
Expand All @@ -48,8 +48,8 @@ access(all) contract FlowALPSupervisorv1 {
/// recovering any that failed to schedule their next transaction.
access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
emit Executed(id: id)
for rebalancerUUID in self.paidRebalancers.keys {
FlowALPRebalancerPaidv1.fixReschedule(uuid: rebalancerUUID)
for positionID in self.paidRebalancers.keys {
FlowALPRebalancerPaidv1.fixReschedule(positionID: positionID)
}
}
}
Expand Down
Loading
Loading