FlowVaults: Integrate FlowALP scheduled liquidations#85
Open
Conversation
Contributor
Author
|
Companion FlowALP scheduler PR: onflow/FlowCreditMarket#58 |
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FlowALP Scheduled Liquidations – Architecture & PR Notes
This document summarizes the design and wiring of the automated, perpetual liquidation scheduling system for FlowALP, implemented on the
scheduled-liquidationsbranch.The goal is to mirror the proven FlowVaults Tides rebalancing scheduler architecture while targeting FlowALP positions and keeping the core FlowALP storage layout unchanged.
High-Level Architecture
Global Supervisor
FlowALPLiquidationScheduler.Supervisoris aFlowTransactionScheduler.TransactionHandler.FlowALPSchedulerRegistry.FlowALPLiquidationScheduler.isPositionLiquidatable.maxPositionsPerMarket).FlowALPSchedulerRegistry.datapayload of the scheduled transaction.Per-Market Liquidation Handler
FlowALPLiquidationScheduler.LiquidationHandleris aFlowTransactionScheduler.TransactionHandler.marketID: UInt64– logical market identifier for events/proofs.feesCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>– pays scheduler fees and receives seized collateral.debtVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>– pulls debt tokens (e.g. MOET) used to repay liquidations.debtType: Type– defaulted to@MOET.Vault.seizeType: Type– defaulted to@FlowToken.Vault.executeTransaction(id, data):marketID,positionID,isRecurring,recurringInterval,priority,executionEffort.FlowALP.Poolfrom its canonical storage path.requiredRepay <= 0.0.pool.quoteLiquidation.debtVaultCapto repay the position’s debt.pool.liquidateRepayForSeizeand:feesCap.FlowALPSchedulerProofs.markExecuted.FlowALPLiquidationScheduler.scheduleNextIfRecurring.Liquidation Manager (Schedule Metadata)
FlowALPLiquidationScheduler.LiquidationManageris a separate resource stored in the scheduler account.scheduleData: {UInt64: LiquidationScheduleData}keyed by scheduled transaction ID.scheduledByPosition: {UInt64: {UInt64: UInt64}}mapping(marketID -> (positionID -> scheduledTxID)).hasScheduled(marketID, positionID)performs cleanup on executed/canceled or missing schedules and returns whether there is an active schedule.scheduleLiquidationto enforce uniqueness and store metadata.isAlreadyScheduledhelper.scheduleNextIfRecurringto fetch recurrence config and create the next child job.Registry Contract
FlowALPSchedulerRegistrystores:registeredMarkets: {UInt64: Bool}.wrapperCaps: {UInt64: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>}– per-marketLiquidationHandlercaps.supervisorCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>?– global supervisor capability, used for self-rescheduling.positionsByMarket: {UInt64: {UInt64: Bool}}– optional position registry keyed by market.registerMarket(marketID, wrapperCap)/unregisterMarket(marketID).getRegisteredMarketIDs(): [UInt64].getWrapperCap(marketID): Capability<...>?.setSupervisorCap/getSupervisorCap.registerPosition(marketID, positionID)/unregisterPosition(marketID, positionID).getPositionIDsForMarket(marketID): [UInt64].Proofs Contract
FlowALPSchedulerProofsis a storage-only contract for executed liquidation proofs.LiquidationScheduled(marketID, positionID, scheduledTransactionID, timestamp)(defined, not currently relied upon in tests).LiquidationExecuted(marketID, positionID, scheduledTransactionID, timestamp)(defined, not currently relied upon in tests).executedByPosition: {UInt64: {UInt64: {UInt64: Bool}}}– mapping:marketID -> positionID -> scheduledTransactionID -> true.markExecuted(marketID, positionID, scheduledTransactionID)– called byLiquidationHandleron successful (or intentionally no-op) execution.wasExecuted(marketID, positionID, scheduledTransactionID): Bool.getExecutedIDs(marketID, positionID): [UInt64].Scheduler Contract – Public Surface
FlowALPLiquidationSchedulerexposes:Supervisor & Handlers
fun createSupervisor(): @SupervisorLiquidationManageris present in storage and publishes a capability for it.fun deriveSupervisorPath(): StoragePathfun createMarketWrapper(marketID: UInt64): @LiquidationHandlerLiquidationHandlerconfigured to repay with MOET and seize FlowToken.fun deriveMarketWrapperPath(marketID: UInt64): StoragePathScheduling Helpers
fun scheduleLiquidation(handlerCap, marketID, positionID, timestamp, priority, executionEffort, fees, isRecurring, recurringInterval?): UInt64FlowTransactionScheduler.schedule.LiquidationManager.LiquidationChildScheduled(scheduler-level event).fun estimateSchedulingCost(timestamp, priority, executionEffort): FlowTransactionScheduler.EstimatedScheduledTransactionFlowTransactionScheduler.estimate.fun scheduleNextIfRecurring(completedID, marketID, positionID)LiquidationScheduleDataforcompletedID.nextTimestamp = now + interval, re-estimates fees, and re-schedules a new child job via the appropriateLiquidationHandlercapability.fun isAlreadyScheduled(marketID, positionID): Boolfun getScheduledLiquidation(marketID, positionID): LiquidationScheduleInfo?Registration Utilities
fun registerMarket(marketID: UInt64)LiquidationHandleris stored underderiveMarketWrapperPath(marketID).TransactionHandlercapability and stores it inFlowALPSchedulerRegistry.registerMarket.fun unregisterMarket(marketID: UInt64)fun getRegisteredMarketIDs(): [UInt64]FlowALPSchedulerRegistry.getRegisteredMarketIDs.fun isPositionLiquidatable(positionID: UInt64): BoolFlowALP.Pooland callpool.isLiquidatable(pid: positionID).Integration with FlowALP (No Core Storage Changes)
The integration is deliberately isolated to helper contracts and test-only transactions, keeping the core
FlowALPstorage layout unchanged.Market Creation
lib/FlowALP/cadence/transactions/alp/create_market.cdcFlowALP.PoolFactoryto create the FlowALP Pool (idempotently).defaultTokenIdentifier: String– e.g.A.045a1763c93006ca.MOET.Vault.marketID: UInt64– logical identifier for the market.FlowALPLiquidationScheduler.registerMarket(marketID: marketID)Position Opening & Tracking
lib/FlowALP/cadence/transactions/alp/open_position_for_market.cdcFlowALP.Poolfrom the signer’s storage.amountof FlowToken from the signer’s vault.FungibleTokenConnectors.VaultSink.let pid = pool.createPosition(...).pool.rebalancePosition(pid: pid, force: true).FlowALPSchedulerRegistry.registerPosition(marketID: marketID, positionID: pid).FlowALPSchedulerRegistry.getPositionIDsForMarket(marketID)and then useisPositionLiquidatableto find underwater candidates.FlowALPSchedulerRegistry.unregisterPosition(marketID, positionID)is available for future integration with position close transactions but is not required for these tests.Underwater Discovery (Read-Only)
lib/FlowALP/cadence/scripts/alp/get_underwater_positions.cdcgetPositionIDsForMarket(marketID)from registry.FlowALPLiquidationScheduler.isPositionLiquidatable(pid).Transactions & Scripts
Scheduler Setup & Control
setup_liquidation_supervisor.cdcSupervisorresource atFlowALPLiquidationScheduler.deriveSupervisorPath()in the scheduler account (tidal).TransactionHandlercapability and saves it intoFlowALPSchedulerRegistry.setSupervisorCap.schedule_supervisor.cdcFlowTransactionScheduler.timestamp: first run time (usually now + a few seconds).priorityRaw: 0/1/2 → High/Medium/Low.executionEffort: computational effort hint.feeAmount: FlowToken to cover the scheduler fee.recurringInterval: seconds between Supervisor runs (0 to disable recurrence).maxPositionsPerMarket: per-run bound for positions per market.childRecurring: whether per-position liquidations should be recurring.childInterval: recurrence interval for child jobs.{String: AnyStruct}and passes it to the Supervisor handler.schedule_liquidation.cdcFlowALPSchedulerRegistry.getWrapperCap(marketID).FlowALPLiquidationScheduler.scheduleLiquidation(...).isRecurring/recurringInterval.Market & Position Helpers
create_market.cdcmarketIDinFlowALPLiquidationScheduler/FlowALPSchedulerRegistry.open_position_for_market.cdcFlowALPSchedulerRegistryfor supervisor discovery.Scripts
get_registered_market_ids.cdcget_scheduled_liquidation.cdcFlowALPLiquidationScheduler.getScheduledLiquidation(marketID, positionID).estimate_liquidation_cost.cdcFlowALPLiquidationScheduler.estimateSchedulingCost.flowFeeand add a small buffer to avoid underpayment.get_liquidation_proof.cdcFlowALPSchedulerProofs.wasExecuted(marketID, positionID, scheduledTransactionID).get_executed_liquidations_for_position.cdcget_underwater_positions.cdcFlowALPLiquidationScheduler.isPositionLiquidatable.E2E Test Setup & Runners
All E2E tests assume:
tidalaccount deployed with:FlowALPSchedulerRegistry,FlowALPSchedulerProofs,FlowALPLiquidationScheduler.Emulator Start Script
local/start_emulator_liquidations.shlocal/start_emulator_scheduled.sh.start_emulator_scheduled.shruns:flow emulator --scheduled-transactions --block-time 1swith the service key fromlocal/emulator-account.pkey../local/start_emulator_liquidations.sh.Single-Market Liquidation Test
run_single_market_liquidation_test.shlocal/setup_wallets.shandlocal/setup_emulator.sh(idempotent).tidal.setup_liquidation_supervisor.cdcto create and register the Supervisor.create_market.cdc(marketID=0).open_position_for_market.cdc(positionID=0).estimate_liquidation_cost.cdcand add a small buffer.schedule_liquidation.cdc.get_scheduled_liquidation.cdc.FlowTransactionSchedulerstatus viacadence/scripts/flow-vaults/get_scheduled_tx_status.cdc, with graceful handling of nil status.get_liquidation_proof.cdc.cadence/scripts/flow-alp/position_health.cdc.Executedevent exists in the block window, or an on-chain proof is present.1.0after liquidation.Multi-Market Supervisor Fan-Out Test
run_multi_market_supervisor_liquidations_test.sh0and1) viacreate_market.cdc.open_position_for_market.cdc.schedule_supervisor.cdc.FlowTransactionScheduler.Executedevents in the block window.get_executed_liquidations_for_position.cdcto ensure each has at least one executed ID.1.0.Auto-Register Market + Liquidation Test
run_auto_register_market_liquidation_test.shget_registered_market_ids.cdc.marketID = max(existing) + 1(or 0 if none).create_market.cdc(auto-registers with scheduler).get_registered_market_ids.cdc.open_position_for_market.cdc.get_underwater_positions.cdcto identify an underwater position.get_scheduled_liquidation.cdcfor the new market/position pair.schedule_liquidation.cdc.create_market.cdc.Emulator & Idempotency Notes
local/setup_emulator.sh:FlowActionssubmodule (if needed) and deploys all core contracts (FlowALP, MOET, FlowVaults, schedulers, etc.) to the emulator.|| truewhere safe to avoid flakiness if rerun.Known Limitations / Future Enhancements
FlowALPSchedulerRegistry.unregisterPosition, so the registry may include closed positions in long-lived environments.LiquidationHandlerboth checkisPositionLiquidatableand skip cleanly when not liquidatable.maxPositionsPerMarketbut does not yet implement chunked iteration over very large position sets (beyond tests’ needs).flowFee.FlowALPSchedulerProofsplus scheduler status and global FlowTransactionScheduler events.LiquidationScheduled/LiquidationExecutedevents inFlowALPSchedulerProofsare defined but not strictly required by the current tests.Work State & How to Re-Run
This section is intended to help future maintainers or tooling resume work quickly if interrupted.
tidal-sc):scheduled-liquidations(branched fromscheduled-rebalancing).lib/FlowALP):scheduled-liquidations.lib/FlowALP/cadence/contracts/FlowALPLiquidationScheduler.cdclib/FlowALP/cadence/contracts/FlowALPSchedulerRegistry.cdclib/FlowALP/cadence/contracts/FlowALPSchedulerProofs.cdclib/FlowALP/cadence/transactions/alp/setup_liquidation_supervisor.cdclib/FlowALP/cadence/transactions/alp/schedule_supervisor.cdclib/FlowALP/cadence/transactions/alp/schedule_liquidation.cdclib/FlowALP/cadence/transactions/alp/create_market.cdclib/FlowALP/cadence/transactions/alp/open_position_for_market.cdclib/FlowALP/cadence/scripts/alp/get_registered_market_ids.cdclib/FlowALP/cadence/scripts/alp/get_scheduled_liquidation.cdclib/FlowALP/cadence/scripts/alp/estimate_liquidation_cost.cdclib/FlowALP/cadence/scripts/alp/get_liquidation_proof.cdclib/FlowALP/cadence/scripts/alp/get_executed_liquidations_for_position.cdclib/FlowALP/cadence/scripts/alp/get_underwater_positions.cdclocal/start_emulator_liquidations.shrun_single_market_liquidation_test.shrun_multi_market_supervisor_liquidations_test.shrun_auto_register_market_liquidation_test.sh./local/start_emulator_liquidations.sh./run_single_market_liquidation_test.sh./run_multi_market_supervisor_liquidations_test.sh./run_auto_register_market_liquidation_test.shTest Results (emulator fresh-start)