FlowCreditMarket: Automated liquidation scheduler #58
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.
FlowCreditMarket Scheduled Liquidations – Architecture & PR Notes
This document summarizes the design and wiring of the automated, perpetual liquidation scheduling system for FlowCreditMarket, implemented on the
scheduled-liquidationsbranch.The goal is to mirror the proven FlowVaults Tides rebalancing scheduler architecture while targeting FlowCreditMarket positions and keeping the core FlowCreditMarket storage layout unchanged.
High-Level Architecture
Global Supervisor
FlowCreditMarketLiquidationScheduler.Supervisoris aFlowTransactionScheduler.TransactionHandler.FlowCreditMarketSchedulerRegistry.FlowCreditMarketLiquidationScheduler.isPositionLiquidatable.maxPositionsPerMarket).FlowCreditMarketSchedulerRegistry.datapayload of the scheduled transaction.Per-Market Liquidation Handler
FlowCreditMarketLiquidationScheduler.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.FlowCreditMarket.Poolfrom its canonical storage path.requiredRepay <= 0.0.pool.quoteLiquidation.debtVaultCapto repay the position’s debt.pool.liquidateRepayForSeizeand:feesCap.FlowCreditMarketSchedulerProofs.markExecuted.FlowCreditMarketLiquidationScheduler.scheduleNextIfRecurring.Liquidation Manager (Schedule Metadata)
FlowCreditMarketLiquidationScheduler.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
FlowCreditMarketSchedulerRegistrystores: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
FlowCreditMarketSchedulerProofsis 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
FlowCreditMarketLiquidationSchedulerexposes: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 inFlowCreditMarketSchedulerRegistry.registerMarket.fun unregisterMarket(marketID: UInt64)fun getRegisteredMarketIDs(): [UInt64]FlowCreditMarketSchedulerRegistry.getRegisteredMarketIDs.fun isPositionLiquidatable(positionID: UInt64): BoolFlowCreditMarket.Pooland callpool.isLiquidatable(pid: positionID).Integration with FlowCreditMarket (No Core Storage Changes)
The integration is deliberately isolated to helper contracts and test-only transactions, keeping the core
FlowCreditMarketstorage layout unchanged.Market Creation
lib/FlowCreditMarket/cadence/transactions/alp/create_market.cdcFlowCreditMarket.PoolFactoryto create the FlowCreditMarket Pool (idempotently).defaultTokenIdentifier: String– e.g.A.045a1763c93006ca.MOET.Vault.marketID: UInt64– logical identifier for the market.FlowCreditMarketLiquidationScheduler.registerMarket(marketID: marketID)Position Opening & Tracking
lib/FlowCreditMarket/cadence/transactions/alp/open_position_for_market.cdcFlowCreditMarket.Poolfrom the signer’s storage.amountof FlowToken from the signer’s vault.FungibleTokenConnectors.VaultSink.let pid = pool.createPosition(...).pool.rebalancePosition(pid: pid, force: true).FlowCreditMarketSchedulerRegistry.registerPosition(marketID: marketID, positionID: pid).FlowCreditMarketSchedulerRegistry.getPositionIDsForMarket(marketID)and then useisPositionLiquidatableto find underwater candidates.FlowCreditMarketSchedulerRegistry.unregisterPosition(marketID, positionID)is available for future integration with position close transactions but is not required for these tests.Underwater Discovery (Read-Only)
lib/FlowCreditMarket/cadence/scripts/alp/get_underwater_positions.cdcgetPositionIDsForMarket(marketID)from registry.FlowCreditMarketLiquidationScheduler.isPositionLiquidatable(pid).Transactions & Scripts
Scheduler Setup & Control
setup_liquidation_supervisor.cdcSupervisorresource atFlowCreditMarketLiquidationScheduler.deriveSupervisorPath()in the scheduler account (tidal).TransactionHandlercapability and saves it intoFlowCreditMarketSchedulerRegistry.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.cdcFlowCreditMarketSchedulerRegistry.getWrapperCap(marketID).FlowCreditMarketLiquidationScheduler.scheduleLiquidation(...).isRecurring/recurringInterval.Market & Position Helpers
create_market.cdcmarketIDinFlowCreditMarketLiquidationScheduler/FlowCreditMarketSchedulerRegistry.open_position_for_market.cdcFlowCreditMarketSchedulerRegistryfor supervisor discovery.Scripts
get_registered_market_ids.cdcget_scheduled_liquidation.cdcFlowCreditMarketLiquidationScheduler.getScheduledLiquidation(marketID, positionID).estimate_liquidation_cost.cdcFlowCreditMarketLiquidationScheduler.estimateSchedulingCost.flowFeeand add a small buffer to avoid underpayment.get_liquidation_proof.cdcFlowCreditMarketSchedulerProofs.wasExecuted(marketID, positionID, scheduledTransactionID).get_executed_liquidations_for_position.cdcget_underwater_positions.cdcFlowCreditMarketLiquidationScheduler.isPositionLiquidatable.E2E Test Setup & Runners
All E2E tests assume:
tidalaccount deployed with:FlowCreditMarketSchedulerRegistry,FlowCreditMarketSchedulerProofs,FlowCreditMarketLiquidationScheduler.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 (FlowCreditMarket, MOET, FlowVaults, schedulers, etc.) to the emulator.|| truewhere safe to avoid flakiness if rerun.Known Limitations / Future Enhancements
FlowCreditMarketSchedulerRegistry.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.FlowCreditMarketSchedulerProofsplus scheduler status and global FlowTransactionScheduler events.LiquidationScheduled/LiquidationExecutedevents inFlowCreditMarketSchedulerProofsare 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/FlowCreditMarket):scheduled-liquidations.lib/FlowCreditMarket/cadence/contracts/FlowCreditMarketLiquidationScheduler.cdclib/FlowCreditMarket/cadence/contracts/FlowCreditMarketSchedulerRegistry.cdclib/FlowCreditMarket/cadence/contracts/FlowCreditMarketSchedulerProofs.cdclib/FlowCreditMarket/cadence/transactions/alp/setup_liquidation_supervisor.cdclib/FlowCreditMarket/cadence/transactions/alp/schedule_supervisor.cdclib/FlowCreditMarket/cadence/transactions/alp/schedule_liquidation.cdclib/FlowCreditMarket/cadence/transactions/alp/create_market.cdclib/FlowCreditMarket/cadence/transactions/alp/open_position_for_market.cdclib/FlowCreditMarket/cadence/scripts/alp/get_registered_market_ids.cdclib/FlowCreditMarket/cadence/scripts/alp/get_scheduled_liquidation.cdclib/FlowCreditMarket/cadence/scripts/alp/estimate_liquidation_cost.cdclib/FlowCreditMarket/cadence/scripts/alp/get_liquidation_proof.cdclib/FlowCreditMarket/cadence/scripts/alp/get_executed_liquidations_for_position.cdclib/FlowCreditMarket/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)