Skip to content

Commit 93229e7

Browse files
authored
Merge pull request #41 from onflow/feature/liquidation-mechanism
Liquidation Mechanism: Phases 1 & 2 (core + DEX)
2 parents dc98433 + 42c7c4a commit 93229e7

22 files changed

Lines changed: 1945 additions & 16 deletions

.github/workflows/cadence_tests.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: CI
22

33
on:
4+
workflow_dispatch:
45
push:
56
branches:
67
- main
@@ -28,11 +29,15 @@ jobs:
2829
restore-keys: |
2930
${{ runner.os }}-go-
3031
- name: Install Flow CLI
32+
env:
33+
FLOW_CLI_VERSION: v2.7.2
3134
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
32-
- name: Flow CLI Version
33-
run: flow version
34-
- name: Update PATH
35-
run: echo "/root/.local/bin" >> $GITHUB_PATH
35+
- name: Update PATH and show Flow version
36+
run: |
37+
echo "/root/.local/bin" >> $GITHUB_PATH
38+
echo "$HOME/.local/bin" >> $GITHUB_PATH
39+
which flow || true
40+
flow version
3641
- name: Install dependencies
3742
run: flow deps install --skip-alias --skip-deployments
3843
- name: Run tests

LIQUIDATION_MECHANISM_DESIGN.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
## Tidal Protocol — Production Liquidation Mechanism (DEX + Keeper + Auto)
2+
3+
### Objectives
4+
- **Safety**: Permissionless, incentive-aligned liquidations that reliably resolve undercollateralized positions.
5+
- **Coverage**: Dual paths — a direct repay-for-seize (keeper provides debt tokens) and a protocol-executed DEX path — plus an automatic DEX liquidation scheduler; no auction fallback.
6+
- **Determinism**: Predictable math, oracle guards, and invariant checks.
7+
- **Observability**: Rich events and view endpoints for keepers/frontends.
8+
9+
### Current foundations (in contract)
10+
- **Risk math**: `healthFactor`, `effectiveCollateral`, `effectiveDebt`, per-token factors; `RiskParams` includes `liquidationBonus`.
11+
- **Position config**: `InternalPosition` has `minHealth`, `targetHealth`, `maxHealth`, optional `topUpSource` and `drawDownSink`.
12+
- **Oracle connectors**: `DeFiActions.PriceOracle` (Band Oracle impl supports staleness checks).
13+
14+
### Out-of-scope for this phase
15+
- Multi-oracle medianization (can be added later).
16+
- Insurance module for bad debt (can be added later, we expose hooks).
17+
18+
## Architecture
19+
20+
### Liquidation paths
21+
- **Permissionless repay-for-seize (Keeper)**: A keeper repays the borrower’s debt and receives collateral with a bonus. Only the exact amount needed to reach the liquidation target health is used; any excess allowance is ignored.
22+
- **Protocol-executed DEX**: Protocol seizes collateral, swaps via allowlisted DEX connector into debt asset, repays debt, and returns any remainder appropriately.
23+
24+
### Routing policy (DEX-first with keeper override)
25+
1) Attempt `topUpSource` pull if present to restore health above the trigger (≥ 1.0).
26+
2) If still unhealthy and outside warm-up: compute the DEX route and quote the effective collateral-per-debt price.
27+
3) If a keeper presents an offer that is strictly better than the DEX route (i.e., requires less collateral per unit of debt repaid), route to keeper; otherwise execute via DEX.
28+
4) Additionally, run an automatic DEX liquidation on a timer (or keeper-triggered automation) so that positions are liquidated even without explicit keeper participation, subject to oracle/DEX deviation guards.
29+
30+
## Governance parameters
31+
- **Per-token (or global defaults)**
32+
- **collateralFactor** (exists)
33+
- **borrowFactor** (exists)
34+
- **liquidationBonus** (exists; percent added to seize quote)
35+
- **twapWindowSec**, **maxDeviationBps** (oracle guards)
36+
- **dustThresholdCredit**, **dustThresholdDebt**
37+
- **Global**
38+
- **liquidationTriggerHF** = 1.0e18 (health strictly below this triggers liquidation; constant)
39+
- **liquidationTargetHF** (e.g., 1.05e18 or 1.10e18; exact health to reach post-liquidation)
40+
- **dexMaxSlippageBps**, **dexMaxRouteHops**
41+
- **dexOracleDeviationBps** (max allowed deviation between oracle price and DEX mid/quote)
42+
- **fees**: `protocolLiquidationFeeBps`, `keeperBountyBps`, `feeSink`
43+
- **connector allowlists**: `allowedSwappers`, `allowedOracles` (by component ID)
44+
- **circuit breakers**: `liquidationsPaused`, `liquidationWarmupSec` (warm-up delay after unpause when liquidations remain disabled)
45+
- **position health spacing** (validation rule): enforce `1.0 < min < target < max` with minimum spacing (e.g., ≥ 5% between bounds) for user-configured thresholds; not used by liquidation.
46+
47+
### Storage additions
48+
- **`liquidationParams`** struct stored in pool.
49+
- **Connector allowlists** and **fee sink** capability path.
50+
- **`lastUnpausedAt: UInt64?`** to compute warm-up window (`liquidationWarmupSec`).
51+
52+
### Events
53+
- **LiquidationParamsUpdated(poolUUID, targetHF, warmupSec, protocolFeeBps)**
54+
- **LiquidationsPaused(by)** / **LiquidationsUnpaused(by, warmupEndsAt)**
55+
- **LiquidationExecuted**(pid, liquidator, debtType, repayAmount, seizeType, seizeAmount, bonusBps, newHF)
56+
- **LiquidationExecutedViaDex**(pid, seizeType, seized, debtType, repaid, slippageBps, newHF)
57+
- **AutoLiquidationExecuted**(pid, route, debtType, repaid, seizeType, seized, newHF)
58+
- **BadDebtWrittenOff**(pid, shortfall)
59+
60+
## Math and quoting (pure helpers)
61+
- **Eligibility**: `health(view) < liquidationThresholdHF`.
62+
- **Required repay to reach target**:
63+
- Compute the exact repay needed to move `health` to `liquidationTargetHF` given current snapshots.
64+
- If a keeper supplies more than required, only the required amount is used.
65+
- **Seize for repay** (single collateral type):
66+
- Let `R = repayTrueAmount` in debt token, `P_d = price(debt)`, `P_c = price(collateral)`, `BF = borrowFactor(debt)`, `CF = collateralFactor(collateral)`, `LB = (1 + liquidationBonus)`.
67+
- Debt value basis = `(R * P_d) / BF`.
68+
- Seize amount = `DebtValue * LB / (P_c * CF)`.
69+
- **Collateral selection**: choose collateral with highest effective value and available balance, unless a hint is provided.
70+
- **Quote**: `quoteLiquidation(pid, debtType, seizeType?) -> {requiredRepay, seizeType, seizeAmount, newHF, route=Keeper|DEX}` including DEX-vs-keeper price comparison when keeper input is provided.
71+
72+
## Entrypoints (new public API)
73+
- **View**
74+
- `isLiquidatable(pid) -> Bool`
75+
- `quoteLiquidation(pid, debtType, seizeType?) -> Quote`
76+
- `getLiquidationParams() -> LiquidationParams`
77+
- **Permissionless actions**
78+
- `liquidateRepayForSeize(pid, debtType, maxRepayAmount, seizeType, minSeizeAmount)` (requires `maxRepayAmount ≥ requiredRepay`)
79+
- `autoLiquidate(pid, debtType, seizeTypeHint?, routeParams?)` (DEX path; callable by any keeper/cron executor)
80+
- **Keeper/governance**
81+
- `liquidateViaDex(pid, debtType, seizeType, maxSeizeAmount, minRepayAmount, routeParams)`
82+
- `setLiquidationParams(params)`
83+
- `setConnectorAllowlists(swappers, oracles)`
84+
- `setFeeSink(sink)`
85+
- `pauseLiquidations(flag)`
86+
87+
## Execution flows
88+
89+
### Repay-for-seize (permissionless)
90+
- Preconditions: liquidations not paused; past warm-up if recently unpaused; fresh indices for involved tokens; oracle staleness/deviation checks incl. DEX-vs-oracle deviation; `health < 1.0e18`.
91+
- Steps:
92+
- Compute `requiredRepay` to reach `liquidationTargetHF`.
93+
- Require `maxRepayAmount ≥ requiredRepay`; transfer exactly `requiredRepay` from keeper; reduce position debt.
94+
- Compute `seizeAmount` with bonus; withdraw from position’s collateral; transfer to liquidator.
95+
- Apply fees/bounties from seized amount if configured; emit `LiquidationExecuted`.
96+
- Postconditions: health increases; no negative balances; dust rules applied.
97+
98+
### Via DEX (protocol executes swap)
99+
- Preconditions: same as above + swapper allowlisted + slippage bounds + DEX-vs-oracle deviation within `dexOracleDeviationBps`.
100+
- Steps:
101+
- Seize up to `maxSeizeAmount` collateral to an internal temporary vault.
102+
- Swap seized collateral → debt token via `SwapConnectors.Swapper` with `minOut` based on slippage.
103+
- Repay debt with swap output; handle any leftover collateral (return to position or fees/sink per params).
104+
- Emit `LiquidationExecutedViaDex`.
105+
- Security: snapshot → seize → external call → state mutate → emit; never pass borrower’s resources directly out.
106+
107+
### Auto liquidation (DEX timer)
108+
- A scheduled or keeper-triggered automation repeatedly scans for undercollateralized positions and calls `autoLiquidate` using the DEX path, subject to the same oracle/DEX deviation and warm-up rules.
109+
- Events: `AutoLiquidationExecuted` per position.
110+
111+
## Oracle safety and indices
112+
- Use oracle with `staleThreshold`, per-token TWAP window, and `maxDeviationBps` guard vs last snapshot.
113+
- Additionally enforce `dexOracleDeviationBps`: the DEX spot/TWAP price used for liquidation must be within this deviation vs oracle price, else revert.
114+
- Accrue interest indices for involved tokens on-demand before quoting/execution.
115+
116+
## Invariants and safety checks
117+
- Liquidation only when `health < threshold` and not paused.
118+
- Post-liquidation: `health ≥ pre.health`.
119+
- Balances never negative; dust thresholds applied.
120+
- Fees ≤ seized amount; fee sink receives expected amount.
121+
- Throttles: per-tx close factor, optional per-block liquidation cap, single active auction per pid.
122+
123+
## Scripts and transactions to add
124+
- Scripts: `quote_liquidation.cdc`, `get_liquidation_params.cdc`.
125+
- Transactions: `liquidate_repay_for_seize.cdc`, `liquidate_via_dex.cdc`, `auto_liquidate.cdc`.
126+
- Governance tx: `set_liquidation_params.cdc`, `allow_swapper.cdc`, `pause_liquidations.cdc`.
127+
128+
## Testing strategy
129+
- Unit (pure math): seize calculation vectors; HF monotonic increase on liquidation; close-factor clamping; rounding bias toward protocol.
130+
- Scenarios: price drop → repay-for-seize; DEX route vs keeper offer routing; stale oracle rejection; DEX-vs-oracle deviation rejection; warm-up blocking; top-up source prevents liquidation; bad-debt write-off path.
131+
- Fuzz/property: random portfolios and prices; repeated partial liquidations; assert invariants each step.
132+
133+
## Rollout phases
134+
- **Phase 1**: Params (incl. warm-up, target HF), view quotes, `liquidateRepayForSeize` + tests/events.
135+
- **Phase 2**: `liquidateViaDex` with an allowlisted swapper + slippage/deviation guards + tests.
136+
- **Phase 3**: Auto liquidation scheduler + keeper example + monitoring dashboards.
137+
- **Phase 4**: Security review, gas profiling, parameter calibration; optional insurance fund integration.
138+
139+
## Open questions
140+
- Single-asset seize per call vs multi-asset? (recommend single per call)
141+
- Default `liquidationTargetHF` (1.05 vs 1.10) and warm-up duration defaults
142+
- Keeper offer format standardization for price comparison (quote units)
143+
- Reserve/insurance module hookup for bad debt.
144+
145+
## References
146+
- High-level design (Notion): https://www.notion.so/Liquidation-Mechanism-in-Tidal-23a9c94cfb9c8087bee9d8e99045b3d9
147+
- Implementation doc (this branch): https://github.com/onflow/TidalProtocol/blob/feature/liquidation-mechanism/LIQUIDATION_MECHANISM_DESIGN.md
148+
149+
## Liquidation policy (Phase 1)
150+
- **Target health factor (HF):** `liquidationTargetHF = 1.05e24`.
151+
- **Trigger condition:** Liquidation is only allowed when current HF < 1.0e24.
152+
- **Quote behavior (`quoteLiquidation`):**
153+
- **If feasible:** Return the unique pair `(requiredRepay, seizeAmount)` that moves the position to HF ≈ `liquidationTargetHF` using the minimal necessary repayment and collateral seize.
154+
- **If infeasible (insolvency):** Return the pair that maximizes HF subject to `seizeAmount ≤ availableCollateral`. If reaching the target is not possible but solvency is, the quote should move HF to ≥ 1.0e24 (as close to target as allowed). If even solvency is not achievable, the quote must strictly improve HF while remaining < 1.0e24. Do not exceed available collateral.
155+
- **No over-reward:** The quote never recommends seizing more collateral than required by the target (or insolvency boundary). Extra repayment must not increase seized collateral.
156+
- **Monotonicity:** As price worsens, `requiredRepay` must not decrease. As price improves, `requiredRepay` must not increase (for the same state).
157+
- **Rounding:** Round conservatively so post-quote execution is not below target due to rounding; small “at or above target” tolerance is acceptable.
158+
- **Execution behavior (`liquidateRepayForSeize`):**
159+
- Uses the quote and takes **exactly** `requiredRepay` from the passed-in vault; if more is provided, the excess is returned/refunded to the caller.
160+
- Sends **exactly** `seizeAmount` collateral to the liquidator; never more.
161+
- Enforce slippage guards: `maxRepayAmount ≥ requiredRepay` and `minSeizeAmount ≤ seizeAmount`, else revert.
162+
- Multiple liquidations can occur over time, but each call performs a single exact-to-quote step. No “extra repay for extra seize.”
163+
164+
### Insolvency redemption (borrower path)
165+
- **Repay-all-and-redeem:** The borrower must always be able to repay all outstanding debt and fully redeem their collateral in one operation, regardless of HF (including when HF < 1.0e24). This closes the position and returns all collateral to the borrower.
166+
- **Partial borrower repayments:** Borrowers can partially repay debt via normal repay flows; collateral withdrawals remain gated by the health check. The effective collateral-to-debt exchange rate is determined by risk parameters and prices, not by discretionary ratios.
167+
168+
### Typical insolvency scenarios
169+
- **Missed/late liquidation:** Automation or keepers fail to liquidate promptly after HF dips below 1.0, allowing interest accrual or price drift to deepen undercollateralization.
170+
- **Sharp price gap:** A sudden oracle price drop (or market gap) pushes HF far below 1.0 faster than liquidation can be executed.
171+
- **Route guards:** DEX-vs-oracle deviation guard or slippage limits temporarily block the DEX route; HF may worsen until conditions normalize or a keeper route executes.
172+
173+
### Partial-to-above-one policy
174+
- When `liquidationTargetHF` cannot be reached due to constraints, but HF ≥ 1.0 is reachable, liquidations should proceed to bring HF above 1.0 immediately rather than waiting to hit 1.05 later. Subsequent liquidations can finish the move to target as conditions allow.
175+
176+
## Acceptance criteria
177+
- **Feasible cases:** After execution, `newHF` is ≥ `liquidationTargetHF - ε` (tiny tolerance for rounding) and ≈ target.
178+
- **Insolvent cases:** After execution, `newHF` is strictly improved compared to pre-liquidation HF. If the target is unreachable but solvency is, `newHF ≥ 1.0e24`. If even solvency is unreachable, `newHF < 1.0e24` but greater than pre-HF.
179+
- **No over-repay/over-seize:** Sending a larger vault must not increase `seizeAmount`; the contract only consumes `requiredRepay`.
180+
- **Slippage respected:** Transactions revert if `maxRepayAmount` < `requiredRepay` or `minSeizeAmount` > `seizeAmount`.
181+
182+
## What needs to be fixed/verified
183+
- **Config**
184+
- Verify `liquidationTargetHF` is 1.05e24 and exposed via `get_liquidation_params.cdc`.
185+
- **Contract**
186+
- Ensure `quoteLiquidation`:
187+
- Solves to target when feasible; otherwise returns boundary solution that maximizes HF under `seize ≤ availableCollateral`.
188+
- Rounds conservatively (post-exec HF not below target when feasible).
189+
- Respects monotonicity vs price.
190+
- Ensure `liquidateRepayForSeize`:
191+
- Consumes exactly `requiredRepay` and seizes exactly `seizeAmount`.
192+
- Refunds any excess funds passed in.
193+
- Enforces slippage guards and rejects partial repayments that do not meet the quoted requirement.
194+
- **Tests**
195+
- Update insolvency test:
196+
- Expect `requiredRepay > 0`, `seizeAmount > 0`.
197+
- Do not require “full seize” by default; instead require `newHF` > pre-HF and not above target; if the scenario is known infeasible, allow `newHF < 1.0e24`.
198+
- Update multi-liquidation test:
199+
- Ensure initial price produces HF < 1.0.
200+
- After one liquidation, assert `newHF` ≥ target (feasible case); if you want multi-step, drop price further and liquidate again.
201+
- Add an “overpay attempt” test:
202+
- Pass `maxRepayAmount` > `requiredRepay` and assert actual repay equals `requiredRepay` and `seizeAmount` unchanged.
203+
- Add a slippage failure test:
204+
- `maxRepayAmount < requiredRepay` → revert; `minSeizeAmount > seizeAmount` → revert.
205+
- Add rounding guard test:
206+
- Feasible case should not end below target due to rounding.
207+

0 commit comments

Comments
 (0)