diff --git a/FlowALP_SCHEDULED_LIQUIDATIONS_PR.md b/FlowALP_SCHEDULED_LIQUIDATIONS_PR.md new file mode 100644 index 00000000..7df81429 --- /dev/null +++ b/FlowALP_SCHEDULED_LIQUIDATIONS_PR.md @@ -0,0 +1,413 @@ +## 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-liquidations` branch. + +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.Supervisor` is a `FlowTransactionScheduler.TransactionHandler`. + - Runs as a single global job that fans out per-position liquidation children across all registered markets. + - Reads markets and positions from `FlowALPSchedulerRegistry`. + - For each registered market: + - Pulls registered position IDs for that market. + - Filters to currently liquidatable positions via `FlowALPLiquidationScheduler.isPositionLiquidatable`. + - Schedules child liquidation jobs via per-market wrapper capabilities, respecting a per-run bound (`maxPositionsPerMarket`). + - Supports optional recurrence: + - If configured, the supervisor self-reschedules using its own capability stored in `FlowALPSchedulerRegistry`. + - Recurrence is driven by configuration embedded in the `data` payload of the scheduled transaction. + +- **Per-Market Liquidation Handler** + - `FlowALPLiquidationScheduler.LiquidationHandler` is a `FlowTransactionScheduler.TransactionHandler`. + - One instance is created per (logical) FlowALP market. + - Fields: + - `marketID: UInt64` – logical market identifier for events/proofs. + - `feesCap: Capability` – pays scheduler fees and receives seized collateral. + - `debtVaultCap: Capability` – 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)`: + - Decodes a configuration map: + - `marketID`, `positionID`, `isRecurring`, `recurringInterval`, `priority`, `executionEffort`. + - Borrows the `FlowALP.Pool` from its canonical storage path. + - Skips gracefully (but still records proof) if the position is no longer liquidatable or if the quote indicates `requiredRepay <= 0.0`. + - Otherwise: + - Quotes liquidation via `pool.quoteLiquidation`. + - Withdraws debt tokens from `debtVaultCap` to repay the position’s debt. + - Executes `pool.liquidateRepayForSeize` and: + - Deposits seized collateral into the FlowToken vault referenced by `feesCap`. + - Returns unused debt tokens to the debt keeper vault. + - Records execution via `FlowALPSchedulerProofs.markExecuted`. + - Delegates recurrence bookkeeping to `FlowALPLiquidationScheduler.scheduleNextIfRecurring`. + +- **Liquidation Manager (Schedule Metadata)** + - `FlowALPLiquidationScheduler.LiquidationManager` is a separate resource stored in the scheduler account. + - Tracks: + - `scheduleData: {UInt64: LiquidationScheduleData}` keyed by scheduled transaction ID. + - `scheduledByPosition: {UInt64: {UInt64: UInt64}}` mapping `(marketID -> (positionID -> scheduledTxID))`. + - Responsibilities: + - Avoids duplicate scheduling: + - `hasScheduled(marketID, positionID)` performs cleanup on executed/canceled or missing schedules and returns whether there is an active schedule. + - Returns schedule metadata by ID or by (marketID, positionID). + - Used by: + - `scheduleLiquidation` to enforce uniqueness and store metadata. + - `isAlreadyScheduled` helper. + - `scheduleNextIfRecurring` to fetch recurrence config and create the next child job. + +- **Registry Contract** + - `FlowALPSchedulerRegistry` stores: + - `registeredMarkets: {UInt64: Bool}`. + - `wrapperCaps: {UInt64: Capability}` – per-market `LiquidationHandler` caps. + - `supervisorCap: Capability?` – global supervisor capability, used for self-rescheduling. + - `positionsByMarket: {UInt64: {UInt64: Bool}}` – optional position registry keyed by market. + - API: + - `registerMarket(marketID, wrapperCap)` / `unregisterMarket(marketID)`. + - `getRegisteredMarketIDs(): [UInt64]`. + - `getWrapperCap(marketID): Capability<...>?`. + - `setSupervisorCap` / `getSupervisorCap`. + - `registerPosition(marketID, positionID)` / `unregisterPosition(marketID, positionID)`. + - `getPositionIDsForMarket(marketID): [UInt64]`. + - Position registry is intentionally separate from FlowALP core: + - Populated via dedicated transactions (see integration points below). + - Allows the Supervisor to enumerate candidate positions without reading FlowALP internal storage. + +- **Proofs Contract** + - `FlowALPSchedulerProofs` is a storage-only contract for executed liquidation proofs. + - Events: + - `LiquidationScheduled(marketID, positionID, scheduledTransactionID, timestamp)` (defined, not currently relied upon in tests). + - `LiquidationExecuted(marketID, positionID, scheduledTransactionID, timestamp)` (defined, not currently relied upon in tests). + - Storage: + - `executedByPosition: {UInt64: {UInt64: {UInt64: Bool}}}` – mapping: + - `marketID -> positionID -> scheduledTransactionID -> true`. + - API: + - `markExecuted(marketID, positionID, scheduledTransactionID)` – called by `LiquidationHandler` on successful (or intentionally no-op) execution. + - `wasExecuted(marketID, positionID, scheduledTransactionID): Bool`. + - `getExecutedIDs(marketID, positionID): [UInt64]`. + - Tests and scripts read proofs via these helpers for deterministic verification. + +--- + +## Scheduler Contract – Public Surface + +`FlowALPLiquidationScheduler` exposes: + +- **Supervisor & Handlers** + - `fun createSupervisor(): @Supervisor` + - Ensures `LiquidationManager` is present in storage and publishes a capability for it. + - Issues a FlowToken fee vault capability for scheduler fees. + - `fun deriveSupervisorPath(): StoragePath` + - Deterministic storage path per scheduler account for the Supervisor resource. + - `fun createMarketWrapper(marketID: UInt64): @LiquidationHandler` + - Creates a per-market `LiquidationHandler` configured to repay with MOET and seize FlowToken. + - `fun deriveMarketWrapperPath(marketID: UInt64): StoragePath` + - Storage path for the handler resource per logical market. + +- **Scheduling Helpers** + - `fun scheduleLiquidation(handlerCap, marketID, positionID, timestamp, priority, executionEffort, fees, isRecurring, recurringInterval?): UInt64` + - Core primitive that: + - Prevents duplicates per (marketID, positionID). + - Calls `FlowTransactionScheduler.schedule`. + - Saves metadata into `LiquidationManager`. + - Emits `LiquidationChildScheduled` (scheduler-level event). + - `fun estimateSchedulingCost(timestamp, priority, executionEffort): FlowTransactionScheduler.EstimatedScheduledTransaction` + - Thin wrapper around `FlowTransactionScheduler.estimate`. + - `fun scheduleNextIfRecurring(completedID, marketID, positionID)` + - Looks up `LiquidationScheduleData` for `completedID`. + - If non-recurring, clears metadata and returns. + - If recurring, computes `nextTimestamp = now + interval`, re-estimates fees, and re-schedules a new child job via the appropriate `LiquidationHandler` capability. + - `fun isAlreadyScheduled(marketID, positionID): Bool` + - Convenience helper for scripts and tests. + - `fun getScheduledLiquidation(marketID, positionID): LiquidationScheduleInfo?` + - Structured view of current scheduled liquidation for a given (marketID, positionID), including scheduler status. + +- **Registration Utilities** + - `fun registerMarket(marketID: UInt64)` + - Idempotent: + - Ensures a per-market `LiquidationHandler` is stored under `deriveMarketWrapperPath(marketID)`. + - Issues its `TransactionHandler` capability and stores it in `FlowALPSchedulerRegistry.registerMarket`. + - `fun unregisterMarket(marketID: UInt64)` + - Deletes registry entries for the given market. + - `fun getRegisteredMarketIDs(): [UInt64]` + - Passthrough to `FlowALPSchedulerRegistry.getRegisteredMarketIDs`. + - `fun isPositionLiquidatable(positionID: UInt64): Bool` + - Borrow `FlowALP.Pool` and call `pool.isLiquidatable(pid: positionID)`. + - Used by Supervisor, scripts, and tests to identify underwater positions. + +--- + +## Integration with FlowALP (No Core Storage Changes) + +The integration is deliberately isolated to helper contracts and test-only transactions, keeping the core `FlowALP` storage layout unchanged. + +- **Market Creation** + - `lib/FlowALP/cadence/transactions/alp/create_market.cdc` + - Uses `FlowALP.PoolFactory` to create the FlowALP Pool (idempotently). + - Accepts: + - `defaultTokenIdentifier: String` – e.g. `A.045a1763c93006ca.MOET.Vault`. + - `marketID: UInt64` – logical identifier for the market. + - After ensuring the pool exists, calls: + - `FlowALPLiquidationScheduler.registerMarket(marketID: marketID)` + - This auto-registers the market with the scheduler registry; no extra manual step is required for new markets. + +- **Position Opening & Tracking** + - `lib/FlowALP/cadence/transactions/alp/open_position_for_market.cdc` + - Opens a FlowALP position and registers it for liquidation scheduling. + - Flow: + - Borrow `FlowALP.Pool` from the signer’s storage. + - Withdraw `amount` of FlowToken from the signer’s vault. + - Create a MOET vault sink using `FungibleTokenConnectors.VaultSink`. + - Call: + - `let pid = pool.createPosition(...)`. + - `pool.rebalancePosition(pid: pid, force: true)`. + - Register the new position in the scheduler registry: + - `FlowALPSchedulerRegistry.registerPosition(marketID: marketID, positionID: pid)`. + - Result: + - Supervisor can iterate over `FlowALPSchedulerRegistry.getPositionIDsForMarket(marketID)` and then use `isPositionLiquidatable` to find underwater candidates. + - Optional close hooks: + - `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.cdc` + - Uses the on-chain registry + FlowALP health to find underwater positions per market: + - `getPositionIDsForMarket(marketID)` from registry. + - Filters via `FlowALPLiquidationScheduler.isPositionLiquidatable(pid)`. + - Primarily used in E2E tests to: + - Validate that price changes cause positions to become underwater. + - Select candidate positions for targeted liquidation tests. + +--- + +## Transactions & Scripts + +### Scheduler Setup & Control + +- **`setup_liquidation_supervisor.cdc`** + - Creates and stores the global `Supervisor` resource at `FlowALPLiquidationScheduler.deriveSupervisorPath()` in the scheduler account (tidal). + - Issues the supervisor’s `TransactionHandler` capability and saves it into `FlowALPSchedulerRegistry.setSupervisorCap`. + - Idempotent: will not overwrite an existing Supervisor. + +- **`schedule_supervisor.cdc`** + - Schedules the Supervisor into `FlowTransactionScheduler`. + - Arguments: + - `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. + - Encodes config into a `{String: AnyStruct}` and passes it to the Supervisor handler. + +- **`schedule_liquidation.cdc`** + - Manual, per-position fallback scheduler. + - Fetches per-market handler capability from `FlowALPSchedulerRegistry.getWrapperCap(marketID)`. + - Withdraws FlowToken fees from the signer. + - Calls `FlowALPLiquidationScheduler.scheduleLiquidation(...)`. + - Supports both one-off and recurring jobs via `isRecurring` / `recurringInterval`. + +### Market & Position Helpers + +- **`create_market.cdc`** + - Creates the FlowALP Pool if not present and auto-registers the `marketID` in `FlowALPLiquidationScheduler` / `FlowALPSchedulerRegistry`. + +- **`open_position_for_market.cdc`** + - Opens a FlowALP position for a given market and registers it in `FlowALPSchedulerRegistry` for supervisor discovery. + +### Scripts + +- **`get_registered_market_ids.cdc`** + - Returns all scheduler-registered market IDs. + +- **`get_scheduled_liquidation.cdc`** + - Thin wrapper over `FlowALPLiquidationScheduler.getScheduledLiquidation(marketID, positionID)`. + - Used in tests to obtain the scheduled transaction ID for a (marketID, positionID) pair. + +- **`estimate_liquidation_cost.cdc`** + - Wraps `FlowALPLiquidationScheduler.estimateSchedulingCost`. + - Lets tests pre-estimate `flowFee` and add a small buffer to avoid underpayment. + +- **`get_liquidation_proof.cdc`** + - Calls `FlowALPSchedulerProofs.wasExecuted(marketID, positionID, scheduledTransactionID)`. + - Serves as an on-chain proof of execution for tests. + +- **`get_executed_liquidations_for_position.cdc`** + - Returns all executed scheduled transaction IDs for a given (marketID, positionID). + - Used in multi-market supervisor tests. + +- **`get_underwater_positions.cdc`** + - Read-only helper returning underwater positions for a given market ID, based on registry and `FlowALPLiquidationScheduler.isPositionLiquidatable`. + +--- + +## E2E Test Setup & Runners + +All E2E tests assume: + +- Flow emulator running with scheduled transactions enabled. +- The `tidal` account deployed with: + - FlowALP + MOET. + - `FlowALPSchedulerRegistry`, `FlowALPSchedulerProofs`, `FlowALPLiquidationScheduler`. + - FlowVaults contracts and their scheduler (already covered by previous work, reused for status polling helpers). + +### Emulator Start Script + +- **`local/start_emulator_liquidations.sh`** + - Convenience wrapper: + - Navigates to repo root. + - Executes `local/start_emulator_scheduled.sh`. + - The underlying `start_emulator_scheduled.sh` runs: + - `flow emulator --scheduled-transactions --block-time 1s` with the service key from `local/emulator-account.pkey`. + - Intended usage: + - Terminal 1: `./local/start_emulator_liquidations.sh`. + - Terminal 2: run one of the E2E test scripts below. + +### Single-Market Liquidation Test + +- **`run_single_market_liquidation_test.sh`** + - Flow: + 1. Wait for emulator on port 3569. + 2. Run `local/setup_wallets.sh` and `local/setup_emulator.sh` (idempotent). + 3. Ensure MOET vault exists for `tidal`. + 4. Run `setup_liquidation_supervisor.cdc` to create and register the Supervisor. + 5. Create a single market via `create_market.cdc` (`marketID=0`). + 6. Open one FlowALP position in that market via `open_position_for_market.cdc` (`positionID=0`). + 7. Drop FlowToken oracle price to make the position undercollateralised. + 8. Estimate scheduling cost via `estimate_liquidation_cost.cdc` and add a small buffer. + 9. Schedule a single liquidation via `schedule_liquidation.cdc`. + 10. Fetch the scheduled transaction ID using `get_scheduled_liquidation.cdc`. + 11. Poll `FlowTransactionScheduler` status via `cadence/scripts/flow-vaults/get_scheduled_tx_status.cdc`, with graceful handling of nil status. + 12. Read execution proof via `get_liquidation_proof.cdc`. + 13. Compare position health before/after via `cadence/scripts/flow-alp/position_health.cdc`. + - Assertions: + - Scheduler status transitions to Executed or disappears (nil) while an `Executed` event exists in the block window, or an on-chain proof is present. + - Position health improves and is at least `1.0` after liquidation. + +### Multi-Market Supervisor Fan-Out Test + +- **`run_multi_market_supervisor_liquidations_test.sh`** + - Flow: + 1. Wait for emulator, run wallet + emulator setup, ensure MOET vault and Supervisor exist. + 2. Create multiple markets (currently two: `0` and `1`) via `create_market.cdc`. + 3. Open positions in each market via `open_position_for_market.cdc`. + 4. Drop FlowToken oracle price to put positions underwater. + 5. Capture initial health for each position. + 6. Estimate Supervisor scheduling cost and schedule a single Supervisor run via `schedule_supervisor.cdc`. + 7. Sleep ~25 seconds to allow Supervisor and child jobs to execute. + 8. Check `FlowTransactionScheduler.Executed` events in the block window. + 9. For each (marketID, positionID), call `get_executed_liquidations_for_position.cdc` to ensure each has at least one executed ID. + 10. Re-check position health; assert it improved and is at least `1.0`. + - Validates: + - Global Supervisor fan-out across multiple registered markets. + - Per-market wrapper capabilities and LiquidationHandlers are used correctly. + - Observed health improvement and asset movement (via seized collateral). + +### Auto-Register Market + Liquidation Test + +- **`run_auto_register_market_liquidation_test.sh`** + - Flow: + 1. Wait for emulator, run wallet + emulator setup, ensure MOET vault and Supervisor exist. + 2. Fetch currently registered markets via `get_registered_market_ids.cdc`. + 3. Choose a new `marketID = max(existing) + 1` (or 0 if none). + 4. Create the new market via `create_market.cdc` (auto-registers with scheduler). + 5. Verify the new market ID shows up in `get_registered_market_ids.cdc`. + 6. Open a position in the new market via `open_position_for_market.cdc`. + 7. Drop FlowToken oracle price and call `get_underwater_positions.cdc` to identify an underwater position. + 8. Capture initial position health. + 9. Try to seed child liquidations via Supervisor: + - Up to two attempts: + - For each attempt: + - Estimate fee and schedule Supervisor with short lookahead and recurrence enabled. + - Sleep ~20 seconds. + - Query `get_scheduled_liquidation.cdc` for the new market/position pair. + 10. If no child job appears, fall back to manual `schedule_liquidation.cdc`. + 11. Once a scheduled ID exists, poll scheduler status and on-chain proofs similar to the single-market test. + 12. Verify health improvement as in previous tests. + - Validates: + - Market auto-registration via `create_market.cdc`. + - Supervisor-based seeding of child jobs for newly registered markets. + - Robustness via retries and a manual fallback path. + +--- + +## Emulator & Idempotency Notes + +- `local/setup_emulator.sh`: + - Updates the FlowALP `FlowActions` submodule (if needed) and deploys all core contracts (FlowALP, MOET, FlowVaults, schedulers, etc.) to the emulator. + - Configures: + - Mock oracle prices and liquidity sources. + - FlowALP pool and supported tokens. + - Intended to be idempotent; repeated calls should not break state. +- Test scripts: + - Guard critical setup commands with `|| true` where safe to avoid flakiness if rerun. + - Handle nil or missing scheduler statuses gracefully. + +--- + +## Known Limitations / Future Enhancements + +- Position registry: + - Positions are tracked per market in `FlowALPSchedulerRegistry`. + - Position closures are not yet wired to `unregisterPosition`, so the registry may include closed positions in long-lived environments. + - Mitigation: + - Supervisor and `LiquidationHandler` both check `isPositionLiquidatable` and skip cleanly when not liquidatable. +- Bounded enumeration: + - Supervisor currently enforces a per-market bound via `maxPositionsPerMarket` but does not yet implement chunked iteration over very large position sets (beyond tests’ needs). + - Recurring Supervisor runs can be used to cover large sets over time. +- Fees and buffers: + - Tests add a small fixed buffer on top of the estimated `flowFee`. + - Production environments may want more robust fee-buffering logic (e.g. multiplier or floor). +- Events vs proofs: + - The main verification channel is the proofs map in `FlowALPSchedulerProofs` plus scheduler status and global FlowTransactionScheduler events. + - `LiquidationScheduled` / `LiquidationExecuted` events in `FlowALPSchedulerProofs` are 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. + +- **Branches** + - Root repo (`tidal-sc`): `scheduled-liquidations` (branched from `scheduled-rebalancing`). + - FlowALP sub-repo (`lib/FlowALP`): `scheduled-liquidations`. +- **Key Contracts & Files** + - Scheduler contracts: + - `lib/FlowALP/cadence/contracts/FlowALPLiquidationScheduler.cdc` + - `lib/FlowALP/cadence/contracts/FlowALPSchedulerRegistry.cdc` + - `lib/FlowALP/cadence/contracts/FlowALPSchedulerProofs.cdc` + - Scheduler transactions: + - `lib/FlowALP/cadence/transactions/alp/setup_liquidation_supervisor.cdc` + - `lib/FlowALP/cadence/transactions/alp/schedule_supervisor.cdc` + - `lib/FlowALP/cadence/transactions/alp/schedule_liquidation.cdc` + - `lib/FlowALP/cadence/transactions/alp/create_market.cdc` + - `lib/FlowALP/cadence/transactions/alp/open_position_for_market.cdc` + - Scheduler scripts: + - `lib/FlowALP/cadence/scripts/alp/get_registered_market_ids.cdc` + - `lib/FlowALP/cadence/scripts/alp/get_scheduled_liquidation.cdc` + - `lib/FlowALP/cadence/scripts/alp/estimate_liquidation_cost.cdc` + - `lib/FlowALP/cadence/scripts/alp/get_liquidation_proof.cdc` + - `lib/FlowALP/cadence/scripts/alp/get_executed_liquidations_for_position.cdc` + - `lib/FlowALP/cadence/scripts/alp/get_underwater_positions.cdc` + - E2E harness: + - `local/start_emulator_liquidations.sh` + - `run_single_market_liquidation_test.sh` + - `run_multi_market_supervisor_liquidations_test.sh` + - `run_auto_register_market_liquidation_test.sh` +- **To (Re)Run Tests (from a fresh emulator)** + - Terminal 1: + - `./local/start_emulator_liquidations.sh` + - Terminal 2: + - Single market: `./run_single_market_liquidation_test.sh` + - Multi-market supervisor: `./run_multi_market_supervisor_liquidations_test.sh` + - Auto-register: `./run_auto_register_market_liquidation_test.sh` + +## Test Results (emulator fresh-start) + +- **Single-market scheduled liquidation**: PASS (position health improves from \<1.0 to \>1.0, proof recorded, fees paid via scheduler). +- **Multi-market supervisor fan-out**: PASS (Supervisor schedules child liquidations for all registered markets; proofs present and position health improves to \>1.0). For reproducibility, run on a fresh emulator to avoid residual positions from earlier runs. +- **Auto-register market liquidation**: PASS (newly created market auto-registers in the registry; Supervisor schedules a child job for its underwater position, with proof + health improvement asserted). Also recommended to run from a fresh emulator. + + diff --git a/cadence/contracts/FlowALPLiquidationScheduler.cdc b/cadence/contracts/FlowALPLiquidationScheduler.cdc new file mode 100644 index 00000000..76c47bf3 --- /dev/null +++ b/cadence/contracts/FlowALPLiquidationScheduler.cdc @@ -0,0 +1,779 @@ +import "FungibleToken" +import "FlowToken" +import "FlowTransactionScheduler" +import "FlowALP" +import "MOET" +import "FlowALPSchedulerRegistry" +import "FlowALPSchedulerProofs" + +/// FlowALPLiquidationScheduler +/// +/// Scheduler for automated, perpetual liquidations in FlowALP using FlowTransactionScheduler. +/// +/// Architecture +/// - Global Supervisor (`Supervisor` resource) fans out across all registered markets. +/// - Per-market wrapper handler (`LiquidationHandler` resource) executes liquidations for individual positions. +/// - `FlowALPSchedulerRegistry` stores market/handler registration and supervisor capability. +/// - `FlowALPSchedulerProofs` records on-chain proofs for scheduled and executed liquidations. +/// - `LiquidationManager` resource tracks schedule metadata and prevents duplicate scheduling. +access(all) contract FlowALPLiquidationScheduler { + + /* --- PATHS --- */ + + /// Storage path for the LiquidationManager resource + access(all) let LiquidationManagerStoragePath: StoragePath + /// Public path for the LiquidationManager (read-only helpers) + access(all) let LiquidationManagerPublicPath: PublicPath + + /* --- EVENTS --- */ + + /// Emitted when a child liquidation job is scheduled by the Supervisor or recurrence helper. + access(all) event LiquidationChildScheduled( + marketID: UInt64, + positionID: UInt64, + scheduledTransactionID: UInt64, + timestamp: UFix64 + ) + + /// Emitted when the Supervisor completes a fan-out tick. + access(all) event SupervisorSeeded( + timestamp: UFix64, + childCount: UInt64 + ) + + /* --- STRUCTS --- */ + + /// Public view of a scheduled liquidation, exposed via scripts. + access(all) struct LiquidationScheduleInfo { + access(all) let marketID: UInt64 + access(all) let positionID: UInt64 + access(all) let scheduledTransactionID: UInt64 + access(all) let timestamp: UFix64 + access(all) let priority: FlowTransactionScheduler.Priority + access(all) let isRecurring: Bool + access(all) let recurringInterval: UFix64? + access(all) let status: FlowTransactionScheduler.Status? + + init( + marketID: UInt64, + positionID: UInt64, + scheduledTransactionID: UInt64, + timestamp: UFix64, + priority: FlowTransactionScheduler.Priority, + isRecurring: Bool, + recurringInterval: UFix64?, + status: FlowTransactionScheduler.Status? + ) { + self.marketID = marketID + self.positionID = positionID + self.scheduledTransactionID = scheduledTransactionID + self.timestamp = timestamp + self.priority = priority + self.isRecurring = isRecurring + self.recurringInterval = recurringInterval + self.status = status + } + } + + /// Internal schedule metadata tracked by the LiquidationManager. + access(all) struct LiquidationScheduleData { + access(all) let marketID: UInt64 + access(all) let positionID: UInt64 + access(all) let isRecurring: Bool + access(all) let recurringInterval: UFix64? + access(all) let priority: FlowTransactionScheduler.Priority + access(all) let executionEffort: UInt64 + + init( + marketID: UInt64, + positionID: UInt64, + isRecurring: Bool, + recurringInterval: UFix64?, + priority: FlowTransactionScheduler.Priority, + executionEffort: UInt64 + ) { + self.marketID = marketID + self.positionID = positionID + self.isRecurring = isRecurring + self.recurringInterval = recurringInterval + self.priority = priority + self.executionEffort = executionEffort + } + } + + /* --- RESOURCES --- */ + + /// LiquidationManager tracks schedule metadata and prevents duplicate scheduling per (marketID, positionID). + access(all) resource LiquidationManager { + /// scheduledTransactionID -> schedule metadata + access(self) var scheduleData: {UInt64: LiquidationScheduleData} + + /// marketID -> (positionID -> scheduledTransactionID) + access(self) var scheduledByPosition: {UInt64: {UInt64: UInt64}} + + init() { + self.scheduleData = {} + self.scheduledByPosition = {} + } + + /// Returns schedule metadata by scheduled transaction ID, if present. + access(all) fun getScheduleData(id: UInt64): LiquidationScheduleData? { + return self.scheduleData[id] + } + + /// Returns the scheduledTransactionID for a given (marketID, positionID), if present. + access(all) fun getScheduledID(marketID: UInt64, positionID: UInt64): UInt64? { + let byMarket = self.scheduledByPosition[marketID] ?? {} as {UInt64: UInt64} + return byMarket[positionID] + } + + /// Returns true if a non-finalized schedule exists for the given (marketID, positionID). + /// Performs cleanup for finalized or missing transactions. + access(all) fun hasScheduled(marketID: UInt64, positionID: UInt64): Bool { + let byMarket = self.scheduledByPosition[marketID] ?? {} as {UInt64: UInt64} + let existingIDOpt = byMarket[positionID] + if existingIDOpt == nil { + return false + } + let existingID = existingIDOpt! + let status = FlowTransactionScheduler.getStatus(id: existingID) + if status == nil { + self.clearScheduleInternal(marketID: marketID, positionID: positionID, scheduledID: existingID) + return false + } + if status! == FlowTransactionScheduler.Status.Executed || + status! == FlowTransactionScheduler.Status.Canceled { + self.clearScheduleInternal(marketID: marketID, positionID: positionID, scheduledID: existingID) + return false + } + return true + } + + /// Clears schedule mappings for a given (marketID, positionID, scheduledID) if they match. + access(contract) fun clearScheduleInternal(marketID: UInt64, positionID: UInt64, scheduledID: UInt64) { + let byMarket = self.scheduledByPosition[marketID] ?? {} as {UInt64: UInt64} + let currentIDOpt = byMarket[positionID] + if currentIDOpt == nil || currentIDOpt! != scheduledID { + return + } + + var updatedByMarket = byMarket + let _removedPos = updatedByMarket.remove(key: positionID) + if updatedByMarket.keys.length == 0 { + let _removedMarket = self.scheduledByPosition.remove(key: marketID) + } else { + self.scheduledByPosition[marketID] = updatedByMarket + } + + let _removedSchedule = self.scheduleData.remove(key: scheduledID) + } + + /// Records new schedule metadata and indexes it by (marketID, positionID). + access(contract) fun setSchedule(id: UInt64, data: LiquidationScheduleData) { + self.scheduleData[id] = data + let byMarket = self.scheduledByPosition[data.marketID] ?? {} as {UInt64: UInt64} + var updatedByMarket = byMarket + updatedByMarket[data.positionID] = id + self.scheduledByPosition[data.marketID] = updatedByMarket + } + } + + /// Per-market handler that executes a liquidation for a specific position + /// and optionally schedules the next child if recurring. + access(all) resource LiquidationHandler: FlowTransactionScheduler.TransactionHandler { + + /// Market identifier this handler is associated with (for events & proofs). + access(self) let marketID: UInt64 + + /// Capability to withdraw FlowToken for scheduling fees or seized collateral. + access(self) let feesCap: Capability + + /// Capability to withdraw debt tokens (MOET) used to repay liquidations. + access(self) let debtVaultCap: Capability + + /// Debt token type used when repaying liquidations (e.g. MOET). + access(self) let debtType: Type + + /// Collateral token type to seize (e.g. FlowToken). + access(self) let seizeType: Type + + init( + marketID: UInt64, + feesCap: Capability, + debtVaultCap: Capability, + debtType: Type, + seizeType: Type + ) { + self.marketID = marketID + self.feesCap = feesCap + self.debtVaultCap = debtVaultCap + self.debtType = debtType + self.seizeType = seizeType + } + + /// Executes liquidation for a given position. + /// + /// `data` is expected to be: + /// { + /// "marketID": UInt64, + /// "positionID": UInt64, + /// "isRecurring": Bool, + /// "recurringInterval": UFix64, + /// "priority": UInt8, + /// "executionEffort": UInt64 + /// } + access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) { + let cfg = data as? {String: AnyStruct} ?? {} + let positionID = cfg["positionID"] as! UInt64 + + // Borrow FlowALP pool + let poolAccount = Type<@FlowALP.Pool>().address! + let pool = getAccount(poolAccount).capabilities + .borrow<&FlowALP.Pool>(FlowALP.PoolPublicPath) + ?? panic("LiquidationHandler: Could not borrow FlowALP.Pool at public path") + + // If position is no longer liquidatable, we still treat the scheduled tx as executed + // but skip actual liquidation logic to avoid aborting the scheduler callback. + if !FlowALPLiquidationScheduler.isPositionLiquidatable(positionID: positionID) { + FlowALPSchedulerProofs.markExecuted( + marketID: self.marketID, + positionID: positionID, + scheduledTransactionID: id + ) + FlowALPLiquidationScheduler.scheduleNextIfRecurring( + completedID: id, + marketID: self.marketID, + positionID: positionID + ) + return + } + + // Quote liquidation parameters + let quote = pool.quoteLiquidation( + pid: positionID, + debtType: self.debtType, + seizeType: self.seizeType + ) + if quote.requiredRepay <= 0.0 { + // Nothing to liquidate; record execution and bail out gracefully + FlowALPSchedulerProofs.markExecuted( + marketID: self.marketID, + positionID: positionID, + scheduledTransactionID: id + ) + FlowALPLiquidationScheduler.scheduleNextIfRecurring( + completedID: id, + marketID: self.marketID, + positionID: positionID + ) + return + } + + let repayAmount: UFix64 = quote.requiredRepay + + // Withdraw debt tokens (MOET) used to repay the borrower's debt. + let debtVaultRef = self.debtVaultCap.borrow() + ?? panic("LiquidationHandler: cannot borrow debt vault") + assert( + debtVaultRef.balance >= repayAmount, + message: "LiquidationHandler: insufficient debt balance in keeper vault" + ) + let repay <- debtVaultRef.withdraw(amount: repayAmount) + + // Execute liquidation via FlowALP pool + let result <- pool.liquidateRepayForSeize( + pid: positionID, + debtType: self.debtType, + maxRepayAmount: repayAmount, + seizeType: self.seizeType, + minSeizeAmount: 0.0, + from: <-repay + ) + + let seized <- result.takeSeized() + let remainder <- result.takeRemainder() + destroy result + + // Deposit seized collateral into the FlowToken vault owned by the scheduler account. + // This keeps accounting simple for tests while still providing observable asset movement. + let flowVaultRef = self.feesCap.borrow() + ?? panic("LiquidationHandler: cannot borrow FlowToken vault for seized collateral") + flowVaultRef.deposit(from: <-seized) + + // Any unused debt tokens are returned to the keeper's debt vault. + let debtVaultRef2 = self.debtVaultCap.borrow() + ?? panic("LiquidationHandler: cannot borrow debt vault for remainder") + debtVaultRef2.deposit(from: <-remainder) + + // Record proof that this scheduled transaction executed successfully. + FlowALPSchedulerProofs.markExecuted( + marketID: self.marketID, + positionID: positionID, + scheduledTransactionID: id + ) + + // If this schedule is recurring, schedule the next child + FlowALPLiquidationScheduler.scheduleNextIfRecurring( + completedID: id, + marketID: self.marketID, + positionID: positionID + ) + } + } + + /// Global Supervisor that fans out liquidation jobs across registered markets. + access(all) resource Supervisor: FlowTransactionScheduler.TransactionHandler { + + /// Capability to the LiquidationManager for schedule bookkeeping. + access(self) let managerCap: Capability<&FlowALPLiquidationScheduler.LiquidationManager> + + /// Capability to withdraw FlowToken used to pay scheduling fees. + access(self) let feesCap: Capability + + init( + managerCap: Capability<&FlowALPLiquidationScheduler.LiquidationManager>, + feesCap: Capability + ) { + self.managerCap = managerCap + self.feesCap = feesCap + } + + /// Supervisor configuration is passed via `data`: + /// { + /// "priority": UInt8 (0=High,1=Medium,2=Low), + /// "executionEffort": UInt64, + /// "lookaheadSecs": UFix64, + /// "maxPositionsPerMarket": UInt64, + /// "childRecurring": Bool, + /// "childInterval": UFix64, + /// "isRecurring": Bool, + /// "recurringInterval": UFix64 + /// } + access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) { + let cfg = data as? {String: AnyStruct} ?? {} + + let priorityRaw = cfg["priority"] as? UInt8 ?? 1 + let executionEffort = cfg["executionEffort"] as? UInt64 ?? 800 + let lookaheadSecs = cfg["lookaheadSecs"] as? UFix64 ?? 5.0 + let maxPositionsPerMarket = cfg["maxPositionsPerMarket"] as? UInt64 ?? 32 + let childRecurring = cfg["childRecurring"] as? Bool ?? true + let childInterval = cfg["childInterval"] as? UFix64 ?? 60.0 + let isRecurring = cfg["isRecurring"] as? Bool ?? true + let recurringInterval = cfg["recurringInterval"] as? UFix64 ?? 60.0 + + let priority: FlowTransactionScheduler.Priority = + priorityRaw == 0 + ? FlowTransactionScheduler.Priority.High + : (priorityRaw == 1 + ? FlowTransactionScheduler.Priority.Medium + : FlowTransactionScheduler.Priority.Low) + + let manager = self.managerCap.borrow() + ?? panic("Supervisor: missing LiquidationManager") + + var totalChildren: UInt64 = 0 + + // Iterate through registered markets and schedule liquidations for underwater positions. + for marketID in FlowALPSchedulerRegistry.getRegisteredMarketIDs() { + let wrapperCap = FlowALPSchedulerRegistry.getWrapperCap(marketID: marketID) + ?? panic("Supervisor: no wrapper capability for market ".concat(marketID.toString())) + + let positionIDs = FlowALPSchedulerRegistry.getPositionIDsForMarket(marketID: marketID) + + var processed: UInt64 = 0 + + for positionID in positionIDs { + if processed >= maxPositionsPerMarket { + break + } + if manager.hasScheduled(marketID: marketID, positionID: positionID) { + continue + } + if !FlowALPLiquidationScheduler.isPositionLiquidatable(positionID: positionID) { + continue + } + + let ts = getCurrentBlock().timestamp + lookaheadSecs + let est = FlowALPLiquidationScheduler.estimateSchedulingCost( + timestamp: ts, + priority: priority, + executionEffort: executionEffort + ) + + // Add a small safety buffer above the estimated fee to avoid "Insufficient fees" + // assertions if the on-chain estimate rounds slightly higher at schedule time. + if est.flowFee == nil || est.timestamp == nil { + continue + } + let baseFee: UFix64 = est.flowFee! + let required: UFix64 = baseFee + 0.00002 + + let vaultRef = self.feesCap.borrow() + ?? panic("Supervisor: cannot borrow FlowToken Vault for child fees") + let pay <- vaultRef.withdraw(amount: required) as! @FlowToken.Vault + + let _scheduledID = FlowALPLiquidationScheduler.scheduleLiquidation( + handlerCap: wrapperCap, + marketID: marketID, + positionID: positionID, + timestamp: ts, + priority: priority, + executionEffort: executionEffort, + fees: <-pay, + isRecurring: childRecurring, + recurringInterval: childRecurring ? childInterval : nil + ) + + totalChildren = totalChildren + 1 + processed = processed + 1 + } + } + + emit SupervisorSeeded( + timestamp: getCurrentBlock().timestamp, + childCount: totalChildren + ) + + // Self-reschedule Supervisor for perpetual operation, if configured. + if isRecurring { + let nextTimestamp = getCurrentBlock().timestamp + recurringInterval + let est = FlowALPLiquidationScheduler.estimateSchedulingCost( + timestamp: nextTimestamp, + priority: priority, + executionEffort: executionEffort + ) + if est.flowFee == nil || est.timestamp == nil { + return + } + let baseFee: UFix64 = est.flowFee! + let required: UFix64 = baseFee + 0.00002 + + let vaultRef = self.feesCap.borrow() + ?? panic("Supervisor: cannot borrow FlowToken Vault for self-reschedule") + let pay <- vaultRef.withdraw(amount: required) as! @FlowToken.Vault + + let supCap = FlowALPSchedulerRegistry.getSupervisorCap() + ?? panic("Supervisor: missing supervisor capability in registry") + + let _scheduled <- FlowTransactionScheduler.schedule( + handlerCap: supCap, + data: cfg, + timestamp: nextTimestamp, + priority: priority, + executionEffort: executionEffort, + fees: <-pay + ) + destroy _scheduled + } + } + } + + /* --- HELPER FUNCTIONS --- */ + + /// Returns true if the given position is currently liquidatable according to FlowALP. + access(all) fun isPositionLiquidatable(positionID: UInt64): Bool { + let poolAccount = Type<@FlowALP.Pool>().address! + let pool = getAccount(poolAccount).capabilities + .borrow<&FlowALP.Pool>(FlowALP.PoolPublicPath) + ?? panic("isPositionLiquidatable: Could not borrow FlowALP.Pool") + return pool.isLiquidatable(pid: positionID) + } + + /// Schedules the next liquidation for a position if the completed scheduled transaction + /// was marked as recurring. + access(all) fun scheduleNextIfRecurring(completedID: UInt64, marketID: UInt64, positionID: UInt64) { + let manager = self.account.storage + .borrow<&FlowALPLiquidationScheduler.LiquidationManager>(from: self.LiquidationManagerStoragePath) + ?? panic("scheduleNextIfRecurring: missing LiquidationManager") + + let dataOpt = manager.getScheduleData(id: completedID) + if dataOpt == nil { + manager.clearScheduleInternal(marketID: marketID, positionID: positionID, scheduledID: completedID) + return + } + let data = dataOpt! + if !data.isRecurring { + manager.clearScheduleInternal(marketID: marketID, positionID: positionID, scheduledID: completedID) + return + } + + let interval = data.recurringInterval ?? 60.0 + let priority = data.priority + let executionEffort = data.executionEffort + let ts = getCurrentBlock().timestamp + interval + + let wrapperCap = FlowALPSchedulerRegistry.getWrapperCap(marketID: marketID) + ?? panic("scheduleNextIfRecurring: missing wrapper capability for market ".concat(marketID.toString())) + + let est = FlowALPLiquidationScheduler.estimateSchedulingCost( + timestamp: ts, + priority: priority, + executionEffort: executionEffort + ) + if est.flowFee == nil || est.timestamp == nil { + manager.clearScheduleInternal(marketID: marketID, positionID: positionID, scheduledID: completedID) + return + } + let baseFee: UFix64 = est.flowFee! + let required: UFix64 = baseFee + 0.00002 + let vaultRef = self.account.storage + .borrow(from: /storage/flowTokenVault) + ?? panic("scheduleNextIfRecurring: cannot borrow FlowToken Vault for recurrence fees") + let pay <- vaultRef.withdraw(amount: required) as! @FlowToken.Vault + + // Replace old schedule entry with the new recurring one. + manager.clearScheduleInternal(marketID: marketID, positionID: positionID, scheduledID: completedID) + + let _scheduledID = FlowALPLiquidationScheduler.scheduleLiquidation( + handlerCap: wrapperCap, + marketID: marketID, + positionID: positionID, + timestamp: ts, + priority: priority, + executionEffort: executionEffort, + fees: <-pay, + isRecurring: true, + recurringInterval: interval + ) + } + + /// Convenience helper to check if a schedule already exists for a given (marketID, positionID). + access(all) fun isAlreadyScheduled(marketID: UInt64, positionID: UInt64): Bool { + let manager = self.account.storage + .borrow<&FlowALPLiquidationScheduler.LiquidationManager>(from: self.LiquidationManagerStoragePath) + if manager == nil { + return false + } + return manager!.hasScheduled(marketID: marketID, positionID: positionID) + } + + /// Returns schedule info for a given (marketID, positionID), if present. + access(all) fun getScheduledLiquidation(marketID: UInt64, positionID: UInt64): LiquidationScheduleInfo? { + let manager = self.account.storage + .borrow<&FlowALPLiquidationScheduler.LiquidationManager>(from: self.LiquidationManagerStoragePath) + if manager == nil { + return nil + } + let scheduledIDOpt = manager!.getScheduledID(marketID: marketID, positionID: positionID) + if scheduledIDOpt == nil { + return nil + } + let scheduledID = scheduledIDOpt! + let dataOpt = manager!.getScheduleData(id: scheduledID) + if dataOpt == nil { + return nil + } + let data = dataOpt! + + let txData = FlowTransactionScheduler.getTransactionData(id: scheduledID) + var ts: UFix64 = 0.0 + var prio: FlowTransactionScheduler.Priority = data.priority + var status: FlowTransactionScheduler.Status? = nil + if txData != nil { + ts = txData!.scheduledTimestamp + prio = txData!.priority + status = txData!.status + } + + return LiquidationScheduleInfo( + marketID: data.marketID, + positionID: data.positionID, + scheduledTransactionID: scheduledID, + timestamp: ts, + priority: prio, + isRecurring: data.isRecurring, + recurringInterval: data.recurringInterval, + status: status + ) + } + + /* --- PUBLIC FUNCTIONS --- */ + + /// Creates a global Supervisor handler resource. + /// This function also ensures that a LiquidationManager is present in storage + /// and that its public capability is published. + access(all) fun createSupervisor(): @Supervisor { + if self.account.storage.borrow<&FlowALPLiquidationScheduler.LiquidationManager>( + from: self.LiquidationManagerStoragePath + ) == nil { + let mgr <- self.createLiquidationManager() + self.account.storage.save(<-mgr, to: self.LiquidationManagerStoragePath) + + let cap = self.account.capabilities.storage + .issue<&FlowALPLiquidationScheduler.LiquidationManager>(self.LiquidationManagerStoragePath) + self.account.capabilities.unpublish(self.LiquidationManagerPublicPath) + self.account.capabilities.publish(cap, at: self.LiquidationManagerPublicPath) + } + + let managerCap = self.account.capabilities.storage + .issue<&FlowALPLiquidationScheduler.LiquidationManager>(self.LiquidationManagerStoragePath) + let feesCap = self.account.capabilities.storage + .issue(/storage/flowTokenVault) + + return <- create Supervisor(managerCap: managerCap, feesCap: feesCap) + } + + /// Derives a storage path for the global Supervisor. + access(all) fun deriveSupervisorPath(): StoragePath { + let identifier = "FlowALPLiquidationScheduler_Supervisor_".concat(self.account.address.toString()) + return StoragePath(identifier: identifier)! + } + + /// Creates a per-market LiquidationHandler wrapper. + /// For now, this uses MOET as debt token and FlowToken as seized collateral token. + access(all) fun createMarketWrapper(marketID: UInt64): @LiquidationHandler { + let feesCap = self.account.capabilities.storage + .issue(/storage/flowTokenVault) + let debtVaultCap = self.account.capabilities.storage + .issue(MOET.VaultStoragePath) + + let debtType = Type<@MOET.Vault>() + let seizeType = Type<@FlowToken.Vault>() + + return <- create LiquidationHandler( + marketID: marketID, + feesCap: feesCap, + debtVaultCap: debtVaultCap, + debtType: debtType, + seizeType: seizeType + ) + } + + /// Derives a storage path for a per-market LiquidationHandler wrapper. + access(all) fun deriveMarketWrapperPath(marketID: UInt64): StoragePath { + let identifier = "FlowALPLiquidationScheduler_LiquidationHandler_".concat(marketID.toString()) + return StoragePath(identifier: identifier)! + } + + /// Creates a new LiquidationManager resource. + access(all) fun createLiquidationManager(): @LiquidationManager { + return <- create LiquidationManager() + } + + /// Schedules a liquidation for a specific (marketID, positionID). + /// Handles duplicate prevention, schedule metadata bookkeeping, and proof/events. + /// Returns the scheduled transaction ID. + access(all) fun scheduleLiquidation( + handlerCap: Capability, + marketID: UInt64, + positionID: UInt64, + timestamp: UFix64, + priority: FlowTransactionScheduler.Priority, + executionEffort: UInt64, + fees: @FlowToken.Vault, + isRecurring: Bool, + recurringInterval: UFix64? + ): UInt64 { + let managerOpt = self.account.storage + .borrow<&FlowALPLiquidationScheduler.LiquidationManager>(from: self.LiquidationManagerStoragePath) + if managerOpt == nil { + panic("scheduleLiquidation: missing LiquidationManager - create Supervisor first") + } + let manager = managerOpt! + + if manager.hasScheduled(marketID: marketID, positionID: positionID) { + panic("scheduleLiquidation: liquidation already scheduled for this market and position") + } + if isRecurring { + if recurringInterval == nil || recurringInterval! <= 0.0 { + panic("scheduleLiquidation: recurringInterval must be > 0 when isRecurring is true") + } + } + if !handlerCap.check() { + panic("scheduleLiquidation: invalid handler capability") + } + + let priorityRaw = priority.rawValue + let data: {String: AnyStruct} = { + "marketID": marketID, + "positionID": positionID, + "isRecurring": isRecurring, + "recurringInterval": recurringInterval ?? 0.0, + "priority": priorityRaw, + "executionEffort": executionEffort + } + + let scheduled <- FlowTransactionScheduler.schedule( + handlerCap: handlerCap, + data: data, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort, + fees: <-fees + ) + + let scheduleData = LiquidationScheduleData( + marketID: marketID, + positionID: positionID, + isRecurring: isRecurring, + recurringInterval: recurringInterval, + priority: priority, + executionEffort: executionEffort + ) + manager.setSchedule(id: scheduled.id, data: scheduleData) + + emit LiquidationChildScheduled( + marketID: marketID, + positionID: positionID, + scheduledTransactionID: scheduled.id, + timestamp: timestamp + ) + + let scheduledID = scheduled.id + destroy scheduled + return scheduledID + } + + /// Registers a market with the scheduler (idempotent). + /// - Ensures a per-market LiquidationHandler exists in storage. + /// - Issues its TransactionHandler capability. + /// - Stores the capability in FlowALPSchedulerRegistry. + access(all) fun registerMarket(marketID: UInt64) { + let wrapperPath = self.deriveMarketWrapperPath(marketID: marketID) + + if self.account.storage.borrow<&FlowALPLiquidationScheduler.LiquidationHandler>(from: wrapperPath) == nil { + let wrapper <- self.createMarketWrapper(marketID: marketID) + self.account.storage.save(<-wrapper, to: wrapperPath) + } + + let wrapperCap = self.account.capabilities.storage + .issue(wrapperPath) + + FlowALPSchedulerRegistry.registerMarket( + marketID: marketID, + wrapperCap: wrapperCap + ) + } + + /// Unregisters a market (idempotent). + access(all) fun unregisterMarket(marketID: UInt64) { + FlowALPSchedulerRegistry.unregisterMarket(marketID: marketID) + } + + /// Lists registered market IDs (proxy to registry). + access(all) fun getRegisteredMarketIDs(): [UInt64] { + return FlowALPSchedulerRegistry.getRegisteredMarketIDs() + } + + /// Estimates the cost of scheduling a liquidation. + access(all) fun estimateSchedulingCost( + timestamp: UFix64, + priority: FlowTransactionScheduler.Priority, + executionEffort: UInt64 + ): FlowTransactionScheduler.EstimatedScheduledTransaction { + return FlowTransactionScheduler.estimate( + data: nil, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort + ) + } + + init() { + let identifier = "FlowALPLiquidationScheduler_".concat(self.account.address.toString()) + self.LiquidationManagerStoragePath = StoragePath(identifier: identifier.concat("_Manager"))! + self.LiquidationManagerPublicPath = PublicPath(identifier: identifier.concat("_Manager"))! + } +} + + diff --git a/cadence/contracts/FlowALPSchedulerProofs.cdc b/cadence/contracts/FlowALPSchedulerProofs.cdc new file mode 100644 index 00000000..18911f15 --- /dev/null +++ b/cadence/contracts/FlowALPSchedulerProofs.cdc @@ -0,0 +1,64 @@ +/// FlowALPSchedulerProofs +/// +/// Stores on-chain proofs for scheduled liquidations in FlowALP. +/// This contract is intentionally storage-only so that the main scheduler +/// logic in `FlowALPLiquidationScheduler` can be upgraded independently. +access(all) contract FlowALPSchedulerProofs { + + /// Emitted when a liquidation child job is scheduled for a specific (marketID, positionID). + access(all) event LiquidationScheduled( + marketID: UInt64, + positionID: UInt64, + scheduledTransactionID: UInt64, + timestamp: UFix64 + ) + + /// Emitted when a scheduled liquidation executes successfully. + access(all) event LiquidationExecuted( + marketID: UInt64, + positionID: UInt64, + scheduledTransactionID: UInt64, + timestamp: UFix64 + ) + + /// Proof map: + /// marketID -> positionID -> scheduledTransactionID -> true + access(self) var executedByPosition: {UInt64: {UInt64: {UInt64: Bool}}} + + /// Records that a scheduled liquidation transaction was executed. + access(all) fun markExecuted(marketID: UInt64, positionID: UInt64, scheduledTransactionID: UInt64) { + let byMarket = self.executedByPosition[marketID] ?? {} as {UInt64: {UInt64: Bool}} + let byPosition = byMarket[positionID] ?? {} as {UInt64: Bool} + + var updatedByPosition = byPosition + updatedByPosition[scheduledTransactionID] = true + + var updatedByMarket = byMarket + updatedByMarket[positionID] = updatedByPosition + self.executedByPosition[marketID] = updatedByMarket + } + + /// Returns true if the given scheduled transaction was executed for (marketID, positionID). + access(all) fun wasExecuted( + marketID: UInt64, + positionID: UInt64, + scheduledTransactionID: UInt64 + ): Bool { + let byMarket = self.executedByPosition[marketID] ?? {} as {UInt64: {UInt64: Bool}} + let byPosition = byMarket[positionID] ?? {} as {UInt64: Bool} + return byPosition[scheduledTransactionID] ?? false + } + + /// Returns all executed scheduled transaction IDs for a given (marketID, positionID). + access(all) fun getExecutedIDs(marketID: UInt64, positionID: UInt64): [UInt64] { + let byMarket = self.executedByPosition[marketID] ?? {} as {UInt64: {UInt64: Bool}} + let byPosition = byMarket[positionID] ?? {} as {UInt64: Bool} + return byPosition.keys + } + + init() { + self.executedByPosition = {} + } +} + + diff --git a/cadence/contracts/FlowALPSchedulerRegistry.cdc b/cadence/contracts/FlowALPSchedulerRegistry.cdc new file mode 100644 index 00000000..8c58b7d1 --- /dev/null +++ b/cadence/contracts/FlowALPSchedulerRegistry.cdc @@ -0,0 +1,115 @@ +import "FlowTransactionScheduler" + +/// FlowALPSchedulerRegistry +/// +/// Lightweight on-chain registry for FlowALP liquidation scheduling. +/// - Tracks which markets are registered with the global Supervisor +/// - Stores a per-market wrapper capability for scheduling per-position liquidations +/// - Stores the Supervisor capability for self-rescheduling +/// - Optionally tracks positions per market to support bounded supervisor fan-out +access(all) contract FlowALPSchedulerRegistry { + + /// Set of registered market IDs + access(self) var registeredMarkets: {UInt64: Bool} + + /// Per-market wrapper capabilities for `FlowTransactionScheduler.TransactionHandler` + access(self) var wrapperCaps: { + UInt64: Capability + } + + /// Global Supervisor capability (used for self-rescheduling) + access(self) var supervisorCap: Capability< + auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler} + >? + + /// Optional: positions registered per market. + /// This enables the Supervisor to enumerate candidate positions in a gas-safe way + /// without requiring FlowALP to expose its internal storage layout. + /// + /// Shape: marketID -> (positionID -> true) + access(self) var positionsByMarket: {UInt64: {UInt64: Bool}} + + /// Registers a market and stores its wrapper capability (idempotent). + access(all) fun registerMarket( + marketID: UInt64, + wrapperCap: Capability + ) { + self.registeredMarkets[marketID] = true + self.wrapperCaps[marketID] = wrapperCap + } + + /// Unregisters a market (idempotent). + /// Any existing wrapper capability entry is removed and the positions set is cleared. + access(all) fun unregisterMarket(marketID: UInt64) { + self.registeredMarkets.remove(key: marketID) + self.wrapperCaps.remove(key: marketID) + self.positionsByMarket.remove(key: marketID) + } + + /// Returns all registered market IDs. + access(all) fun getRegisteredMarketIDs(): [UInt64] { + return self.registeredMarkets.keys + } + + /// Returns the wrapper capability for a given market, if present. + access(all) fun getWrapperCap( + marketID: UInt64 + ): Capability? { + return self.wrapperCaps[marketID] + } + + /// Sets the global Supervisor capability (used for Supervisor self-rescheduling). + access(all) fun setSupervisorCap( + cap: Capability + ) { + self.supervisorCap = cap + } + + /// Returns the global Supervisor capability, if configured. + access(all) fun getSupervisorCap(): Capability< + auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler} + >? { + return self.supervisorCap + } + + /// Registers a position under a given market (idempotent). + /// This is the primary hook used by transactions when a position is opened. + access(all) fun registerPosition(marketID: UInt64, positionID: UInt64) { + let current = self.positionsByMarket[marketID] ?? {} as {UInt64: Bool} + var updated = current + updated[positionID] = true + self.positionsByMarket[marketID] = updated + } + + /// Unregisters a position from a given market (idempotent). + /// This hook can be called when a position is permanently closed. + access(all) fun unregisterPosition(marketID: UInt64, positionID: UInt64) { + let current = self.positionsByMarket[marketID] ?? {} as {UInt64: Bool} + if current[positionID] == nil { + return + } + var updated = current + let _ = updated.remove(key: positionID) + if updated.keys.length == 0 { + let _ = self.positionsByMarket.remove(key: marketID) + } else { + self.positionsByMarket[marketID] = updated + } + } + + /// Returns the registered position IDs for a given market. + /// The Supervisor is responsible for applying any per-tick bounds on iteration. + access(all) fun getPositionIDsForMarket(marketID: UInt64): [UInt64] { + let byMarket = self.positionsByMarket[marketID] ?? {} as {UInt64: Bool} + return byMarket.keys + } + + init() { + self.registeredMarkets = {} + self.wrapperCaps = {} + self.supervisorCap = nil + self.positionsByMarket = {} + } +} + + diff --git a/cadence/scripts/alp/estimate_liquidation_cost.cdc b/cadence/scripts/alp/estimate_liquidation_cost.cdc new file mode 100644 index 00000000..da8beb9e --- /dev/null +++ b/cadence/scripts/alp/estimate_liquidation_cost.cdc @@ -0,0 +1,28 @@ +import "FlowTransactionScheduler" +import "FlowALPLiquidationScheduler" + +/// Estimates the cost of scheduling a liquidation transaction via FlowALPLiquidationScheduler. +/// +/// - `timestamp`: desired execution timestamp +/// - `priorityRaw`: 0=High,1=Medium,2=Low +/// - `executionEffort`: expected execution effort +access(all) fun main( + timestamp: UFix64, + priorityRaw: UInt8, + executionEffort: UInt64 +): FlowTransactionScheduler.EstimatedScheduledTransaction { + let priority: FlowTransactionScheduler.Priority = + priorityRaw == 0 + ? FlowTransactionScheduler.Priority.High + : (priorityRaw == 1 + ? FlowTransactionScheduler.Priority.Medium + : FlowTransactionScheduler.Priority.Low) + + return FlowALPLiquidationScheduler.estimateSchedulingCost( + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort + ) +} + + diff --git a/cadence/scripts/alp/get_executed_liquidations_for_position.cdc b/cadence/scripts/alp/get_executed_liquidations_for_position.cdc new file mode 100644 index 00000000..fcd65910 --- /dev/null +++ b/cadence/scripts/alp/get_executed_liquidations_for_position.cdc @@ -0,0 +1,11 @@ +import "FlowALPSchedulerProofs" + +/// Returns the executed scheduled transaction IDs for a given (marketID, positionID). +access(all) fun main(marketID: UInt64, positionID: UInt64): [UInt64] { + return FlowALPSchedulerProofs.getExecutedIDs( + marketID: marketID, + positionID: positionID + ) +} + + diff --git a/cadence/scripts/alp/get_liquidation_proof.cdc b/cadence/scripts/alp/get_liquidation_proof.cdc new file mode 100644 index 00000000..7761a6f3 --- /dev/null +++ b/cadence/scripts/alp/get_liquidation_proof.cdc @@ -0,0 +1,17 @@ +import "FlowALPSchedulerProofs" + +/// Returns true if the given scheduled transaction ID has been marked executed +/// for the specified (marketID, positionID) pair. +access(all) fun main( + marketID: UInt64, + positionID: UInt64, + scheduledTransactionID: UInt64 +): Bool { + return FlowALPSchedulerProofs.wasExecuted( + marketID: marketID, + positionID: positionID, + scheduledTransactionID: scheduledTransactionID + ) +} + + diff --git a/cadence/scripts/alp/get_registered_market_ids.cdc b/cadence/scripts/alp/get_registered_market_ids.cdc new file mode 100644 index 00000000..bc677a1f --- /dev/null +++ b/cadence/scripts/alp/get_registered_market_ids.cdc @@ -0,0 +1,8 @@ +import "FlowALPSchedulerRegistry" + +/// Returns all market IDs registered with the FlowALP liquidation scheduler. +access(all) fun main(): [UInt64] { + return FlowALPSchedulerRegistry.getRegisteredMarketIDs() +} + + diff --git a/cadence/scripts/alp/get_scheduled_liquidation.cdc b/cadence/scripts/alp/get_scheduled_liquidation.cdc new file mode 100644 index 00000000..60afc0d8 --- /dev/null +++ b/cadence/scripts/alp/get_scheduled_liquidation.cdc @@ -0,0 +1,12 @@ +import "FlowALPLiquidationScheduler" + +/// Returns schedule info for a liquidation associated with (marketID, positionID), +/// if a schedule currently exists. +access(all) fun main(marketID: UInt64, positionID: UInt64): FlowALPLiquidationScheduler.LiquidationScheduleInfo? { + return FlowALPLiquidationScheduler.getScheduledLiquidation( + marketID: marketID, + positionID: positionID + ) +} + + diff --git a/cadence/scripts/alp/get_underwater_positions.cdc b/cadence/scripts/alp/get_underwater_positions.cdc new file mode 100644 index 00000000..abcda977 --- /dev/null +++ b/cadence/scripts/alp/get_underwater_positions.cdc @@ -0,0 +1,19 @@ +import "FlowALP" +import "FlowALPSchedulerRegistry" +import "FlowALPLiquidationScheduler" + +/// Helper script for tests: returns all registered position IDs for a market +/// that are currently liquidatable according to FlowALP. +access(all) fun main(marketID: UInt64): [UInt64] { + let allPositions = FlowALPSchedulerRegistry.getPositionIDsForMarket(marketID: marketID) + let results: [UInt64] = [] + + for pid in allPositions { + if FlowALPLiquidationScheduler.isPositionLiquidatable(positionID: pid) { + results.append(pid) + } + } + return results +} + + diff --git a/cadence/transactions/alp/create_market.cdc b/cadence/transactions/alp/create_market.cdc new file mode 100644 index 00000000..8938f7ce --- /dev/null +++ b/cadence/transactions/alp/create_market.cdc @@ -0,0 +1,47 @@ +import "FungibleToken" + +import "DeFiActions" +import "FlowALP" +import "MockOracle" +import "FlowALPLiquidationScheduler" + +/// Creates the FlowALP Pool (if not already created) and auto-registers a logical +/// market with the liquidation scheduler. +/// +/// This transaction is intended as the single entrypoint for setting up a new +/// liquidation-enabled market in FlowALP environments. +/// +/// - `defaultTokenIdentifier`: Type identifier of the Pool's default token, +/// e.g. `Type<@MOET.Vault>().identifier`. +/// - `marketID`: logical market identifier to register with the scheduler. +transaction(defaultTokenIdentifier: String, marketID: UInt64) { + + let factory: &FlowALP.PoolFactory + let defaultToken: Type + let oracle: {DeFiActions.PriceOracle} + var shouldCreatePool: Bool + + prepare(signer: auth(BorrowValue) &Account) { + self.factory = signer.storage.borrow<&FlowALP.PoolFactory>(from: FlowALP.PoolFactoryPath) + ?? panic("create_market: Could not find FlowALP.PoolFactory in signer's account") + + self.defaultToken = CompositeType(defaultTokenIdentifier) + ?? panic("create_market: Invalid defaultTokenIdentifier ".concat(defaultTokenIdentifier)) + + self.oracle = MockOracle.PriceOracle() + + // Idempotent pool creation: only create if no Pool is currently stored. + self.shouldCreatePool = signer.storage.type(at: FlowALP.PoolStoragePath) == nil + } + + execute { + if self.shouldCreatePool { + self.factory.createPool(defaultToken: self.defaultToken, priceOracle: self.oracle) + } + + // Auto-register market with scheduler (idempotent at scheduler level). + FlowALPLiquidationScheduler.registerMarket(marketID: marketID) + } +} + + diff --git a/cadence/transactions/alp/open_position_for_market.cdc b/cadence/transactions/alp/open_position_for_market.cdc new file mode 100644 index 00000000..965340b3 --- /dev/null +++ b/cadence/transactions/alp/open_position_for_market.cdc @@ -0,0 +1,59 @@ +import "FungibleToken" +import "FlowToken" + +import "DeFiActions" +import "DeFiActionsUtils" +import "FlowALP" +import "MOET" +import "FungibleTokenConnectors" +import "FlowALPSchedulerRegistry" + +/// Opens a FlowALP position for a given market and registers the position +/// with the liquidation scheduler registry. +/// +/// This is a convenience transaction used primarily in E2E tests to ensure +/// positions are discoverable by the Supervisor without modifying core +/// FlowALP storage. +/// +/// - `marketID`: logical market identifier already registered via `create_market`. +/// - `amount`: amount of FLOW to deposit as initial collateral. +transaction(marketID: UInt64, amount: UFix64) { + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability) &Account) { + + let pool = signer.storage.borrow( + from: FlowALP.PoolStoragePath + ) ?? panic("open_position_for_market: could not borrow FlowALP.Pool from storage") + + let vaultRef = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("open_position_for_market: could not borrow FlowToken Vault from signer") + + let flowFunds <- vaultRef.withdraw(amount: amount) + + let depositVaultCap = signer.capabilities.get<&{FungibleToken.Vault}>(MOET.VaultPublicPath) + assert( + depositVaultCap.check(), + message: "open_position_for_market: invalid MOET Vault public capability; ensure Vault is configured" + ) + + let depositSink = FungibleTokenConnectors.VaultSink( + max: UFix64.max, + depositVault: depositVaultCap, + uniqueID: nil + ) + + // Create the FlowALP position and immediately rebalance for the provided collateral. + let pid = pool.createPosition( + funds: <-flowFunds, + issuanceSink: depositSink, + repaymentSource: nil, + pushToDrawDownSink: false + ) + pool.rebalancePosition(pid: pid, force: true) + + // Register the new position with the scheduler registry under the given market. + FlowALPSchedulerRegistry.registerPosition(marketID: marketID, positionID: pid) + } +} + + diff --git a/cadence/transactions/alp/schedule_liquidation.cdc b/cadence/transactions/alp/schedule_liquidation.cdc new file mode 100644 index 00000000..93c859a0 --- /dev/null +++ b/cadence/transactions/alp/schedule_liquidation.cdc @@ -0,0 +1,68 @@ +import "FlowALPLiquidationScheduler" +import "FlowALPSchedulerRegistry" +import "FlowTransactionScheduler" +import "FlowToken" +import "FungibleToken" + +/// Manually schedules a liquidation job for a specific (marketID, positionID). +/// Useful for tests, backfills, or as a fallback when Supervisor-based fan-out +/// is not yet configured. +/// +/// Arguments: +/// - `marketID`: logical market identifier registered in FlowALPSchedulerRegistry +/// - `positionID`: FlowALP position ID to liquidate +/// - `timestamp`: desired execution time +/// - `priorityRaw`: 0=High,1=Medium,2=Low +/// - `executionEffort`: execution effort hint for the scheduler +/// - `feeAmount`: FLOW to cover scheduling +/// - `isRecurring`: whether this liquidation should self-reschedule +/// - `recurringInterval`: interval in seconds between recurring executions +transaction( + marketID: UInt64, + positionID: UInt64, + timestamp: UFix64, + priorityRaw: UInt8, + executionEffort: UInt64, + feeAmount: UFix64, + isRecurring: Bool, + recurringInterval: UFix64 +) { + let handlerCap: Capability + let payment: @FlowToken.Vault + + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + let wrapperCapOpt = FlowALPSchedulerRegistry.getWrapperCap(marketID: marketID) + assert(wrapperCapOpt != nil, message: "schedule_liquidation: market is not registered") + self.handlerCap = wrapperCapOpt! + + let vaultRef = signer.storage + .borrow(from: /storage/flowTokenVault) + ?? panic("schedule_liquidation: could not borrow FlowToken Vault") + self.payment <- vaultRef.withdraw(amount: feeAmount) as! @FlowToken.Vault + } + + execute { + let priority: FlowTransactionScheduler.Priority = + priorityRaw == 0 + ? FlowTransactionScheduler.Priority.High + : (priorityRaw == 1 + ? FlowTransactionScheduler.Priority.Medium + : FlowTransactionScheduler.Priority.Low) + + let intervalOpt: UFix64? = isRecurring ? recurringInterval : nil + + let _scheduledID = FlowALPLiquidationScheduler.scheduleLiquidation( + handlerCap: self.handlerCap, + marketID: marketID, + positionID: positionID, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort, + fees: <-self.payment, + isRecurring: isRecurring, + recurringInterval: intervalOpt + ) + } +} + + diff --git a/cadence/transactions/alp/schedule_supervisor.cdc b/cadence/transactions/alp/schedule_supervisor.cdc new file mode 100644 index 00000000..5e61a2de --- /dev/null +++ b/cadence/transactions/alp/schedule_supervisor.cdc @@ -0,0 +1,79 @@ +import "FlowALPLiquidationScheduler" +import "FlowTransactionScheduler" +import "FlowToken" +import "FungibleToken" + +/// Schedules the global liquidation Supervisor for FlowALP. +/// +/// Arguments: +/// - `timestamp`: first run timestamp (typically now + a small delta) +/// - `priorityRaw`: 0=High, 1=Medium, 2=Low +/// - `executionEffort`: typical 800 +/// - `feeAmount`: FLOW to cover the scheduling fee +/// - `recurringInterval`: seconds between Supervisor runs (0 to disable recurrence) +/// - `maxPositionsPerMarket`: per-tick bound for positions processed per market +/// - `childRecurring`: whether per-position liquidations should be recurring +/// - `childInterval`: interval between recurring child liquidations +transaction( + timestamp: UFix64, + priorityRaw: UInt8, + executionEffort: UInt64, + feeAmount: UFix64, + recurringInterval: UFix64, + maxPositionsPerMarket: UInt64, + childRecurring: Bool, + childInterval: UFix64 +) { + let handlerCap: Capability + let payment: @FlowToken.Vault + + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + let supPath = FlowALPLiquidationScheduler.deriveSupervisorPath() + assert( + signer.storage.borrow<&FlowALPLiquidationScheduler.Supervisor>(from: supPath) != nil, + message: "Liquidation Supervisor not set up; run setup_liquidation_supervisor first" + ) + + self.handlerCap = signer.capabilities.storage + .issue(supPath) + + let vaultRef = signer.storage + .borrow(from: /storage/flowTokenVault) + ?? panic("Could not borrow FlowToken Vault for supervisor scheduling fee") + self.payment <- vaultRef.withdraw(amount: feeAmount) as! @FlowToken.Vault + } + + execute { + let priority: FlowTransactionScheduler.Priority = + priorityRaw == 0 + ? FlowTransactionScheduler.Priority.High + : (priorityRaw == 1 + ? FlowTransactionScheduler.Priority.Medium + : FlowTransactionScheduler.Priority.Low) + + let isRecurring: Bool = recurringInterval > 0.0 + + let cfg: {String: AnyStruct} = { + "priority": priorityRaw, + "executionEffort": executionEffort, + "lookaheadSecs": 5.0, + "maxPositionsPerMarket": maxPositionsPerMarket, + "childRecurring": childRecurring, + "childInterval": childInterval, + "isRecurring": isRecurring, + "recurringInterval": recurringInterval + } + + let _scheduled <- FlowTransactionScheduler.schedule( + handlerCap: self.handlerCap, + data: cfg, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort, + fees: <-self.payment + ) + destroy _scheduled + } +} + + diff --git a/cadence/transactions/alp/setup_liquidation_supervisor.cdc b/cadence/transactions/alp/setup_liquidation_supervisor.cdc new file mode 100644 index 00000000..e783ed9c --- /dev/null +++ b/cadence/transactions/alp/setup_liquidation_supervisor.cdc @@ -0,0 +1,24 @@ +import "FlowALPLiquidationScheduler" +import "FlowALPSchedulerRegistry" +import "FlowTransactionScheduler" + +/// Creates and stores the global liquidation Supervisor handler in the FlowALP account, +/// and publishes its TransactionHandler capability into FlowALPSchedulerRegistry so that +/// the Supervisor can self-reschedule. +transaction() { + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability) &Account) { + let path = FlowALPLiquidationScheduler.deriveSupervisorPath() + + if signer.storage.borrow<&FlowALPLiquidationScheduler.Supervisor>(from: path) == nil { + let sup <- FlowALPLiquidationScheduler.createSupervisor() + signer.storage.save(<-sup, to: path) + } + + let supCap = signer.capabilities.storage + .issue(path) + + FlowALPSchedulerRegistry.setSupervisorCap(cap: supCap) + } +} + + diff --git a/flow.json b/flow.json index bf04ead2..259df3f7 100644 --- a/flow.json +++ b/flow.json @@ -69,6 +69,24 @@ "aliases": { "testing": "0000000000000007" } + }, + "FlowALPSchedulerRegistry": { + "source": "./cadence/contracts/FlowALPSchedulerRegistry.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowALPSchedulerProofs": { + "source": "./cadence/contracts/FlowALPSchedulerProofs.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowALPLiquidationScheduler": { + "source": "./cadence/contracts/FlowALPLiquidationScheduler.cdc", + "aliases": { + "testing": "0000000000000007" + } } }, "dependencies": { @@ -134,6 +152,15 @@ "mainnet": "1d7e57aa55817448", "testnet": "631e88ae7f1d7c20" } + }, + "FlowTransactionScheduler": { + "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", + "hash": "312885f5fa3bc70327dfb59edc5da6d30b826002c322db8c566ddf17099310ac", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } } }, "networks": { @@ -182,7 +209,10 @@ } ] }, - "FlowALP" + "FlowALP", + "FlowALPSchedulerRegistry", + "FlowALPSchedulerProofs", + "FlowALPLiquidationScheduler" ] }, "testnet": {