Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions docs/exchange-router-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Users call `multicall` with a `Vec<RouterAction>` to execute one or more actions atomically. A single `caller.require_auth()` covers the entire batch. Any panic inside a sub-action reverts the whole transaction.

Direct single-action helpers (`create_order`, `cancel_order`, ) are also exposed; they are the same functions called by `multicall` internally.
Direct single-action helpers (`create_deposit`, `cancel_order`, `cancel_deposit`, `create_withdrawal`, `cancel_withdrawal`, `update_order`) are also exposed; they forward directly to the respective handlers. Note that `create_order` and `send_tokens` are dispatched via `multicall` (`RouterAction`), while `claim_funding_fees` is available as a dedicated standalone function.

---

Expand All @@ -21,6 +21,10 @@ Direct single-action helpers (`create_order`, `cancel_order`, …) are also expo
| 1 | `AlreadyInitialized` | `initialize` called after first-time setup |
| 2 | `NotInitialized` | Any function called before `initialize` |
| 3 | `Unauthorized` | `upgrade` or admin function called by non-admin |
| 4 | `Paused` | Protocol or market is paused during action execution |
| 5 | `BatchSizeLimitExceeded` | Multicall batch size exceeds maximum allowed limit |
| 6 | `TimelockNotExpired` | `execute_unpause` called before timelock window expired (issue #282) |
| 7 | `UnpauseNotScheduled` | `execute_unpause` called without a prior `schedule_unpause` (issue #282) |

---

Expand Down Expand Up @@ -100,7 +104,7 @@ create_order(env: Env, caller: Address, params: CreateOrderParams) -> BytesN<32>

**Storage written:** `OrderStorageKey::Order(key)` in `order_handler` persistent storage; order key added to global and per-account index sets in `data_store`.

**Events emitted:** `ord_crt` → `(key, caller, market)`
**Events emitted:** `ord_crt` → `(key, caller, market, size_delta_usd, collateral_delta_amount, order_type)`

**Errors** (from `order_handler`)
| Code | Condition |
Expand Down Expand Up @@ -193,7 +197,7 @@ create_deposit(env: Env, caller: Address, params: CreateDepositParams) -> BytesN

**Returns:** `BytesN<32>` — the new deposit key.

**Events emitted:** `dep_crt` → `(key, caller, market)`
**Events emitted:** `dep_crt` → `(key, caller, market, long_token_amount, short_token_amount)`

**Errors**
| Code | Condition |
Expand Down Expand Up @@ -253,7 +257,7 @@ create_withdrawal(env: Env, caller: Address, params: CreateWithdrawalParams) ->

**Returns:** `BytesN<32>` — the new withdrawal key.

**Events emitted:** `wth_crt` → `(key, caller, market)`
**Events emitted:** `wth_crt` → `(key, caller, market, market_token_amount)`

**Errors**
| Code | Condition |
Expand Down Expand Up @@ -312,7 +316,7 @@ claim_funding_fees(

**Side effects:** For each pair, transfers the claimable funding fee from the market pool to `caller`. Zeroes the claimable balance in `fee_handler`.

**Events emitted:** One `fee_clm` event per market/token pair (emitted by `fee_handler`).
**Events emitted:** One `fnd_clm` event (`FundingFeeClaimed`) per market/token pair (emitted by `fee_handler`).

**Errors**
| Code | Condition |
Expand Down
6 changes: 3 additions & 3 deletions docs/keeper-execution-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ sequenceDiagram

Note over Keeper: waits for new ledger

Keeper->>Oracle: set_primary_price(token, price)
Keeper->>Oracle: set_prices(prices)
Keeper->>OrderHandler: execute_order(nonce)
OrderHandler->>Oracle: get_price(token)
OrderHandler->>DataStore: update(position, pool)
Expand All @@ -41,7 +41,7 @@ sequenceDiagram
2. **ExchangeRouter → OrderVault**: transfers collateral from the user's wallet into the vault for increase/swap orders.
3. **ExchangeRouter → DataStore**: stores `OrderProps` (pending state) and records the key in the global and per-account order sets.
4. **Keeper**: detects the pending order event, fetches market prices from an external price source.
5. **Keeper → Oracle**: `set_primary_price(token, price)` — commits the price on-chain.
5. **Keeper → Oracle**: `set_prices(prices)` (or `set_prices_simple` in tests) — commits the price bundle on-chain.
6. **Keeper → OrderHandler**: `execute_order(nonce)` — triggers dispatch.
7. **OrderHandler → Oracle**: reads the committed price.
8. **OrderHandler → DataStore**: updates position size, pool amounts, open interest, and funding trackers.
Expand All @@ -59,7 +59,7 @@ sequenceDiagram

| Action | Required role |
|---|---|
| `set_primary_price` (Oracle) | `ORDER_KEEPER` |
| `set_prices` / `set_prices_simple` (Oracle) | `ORDER_KEEPER` |
| `execute_order` | `ORDER_KEEPER` |
| `freeze_order` | `ORDER_KEEPER` |
| `cancel_order` | account owner **or** `ORDER_KEEPER` |
Expand Down
32 changes: 15 additions & 17 deletions docs/liquidation-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,30 @@ A position becomes eligible for liquidation when its health factor drops below 1
A position is liquidatable when (in the normal, configured case):

```
net_collateral_usd < size_in_usd × min_collateral_factor
net_collateral_usd + pnl_usd < size_in_usd × min_collateral_factor
```

where `net_collateral_usd = collateral_usd - fees_usd` — current fees (borrowing +
funding + position fee, computed worst-case) are subtracted from collateral
**before** the comparison. Unrealised PnL is deliberately excluded from this
comparison: a profitable unrealised gain must not mask a genuine collateral
shortfall.
funding + position fee, computed worst-case) are subtracted from collateral, and
unrealised PnL (`pnl_usd`) is folded into `remaining` (`remaining = net_collateral + pnl_usd`)
before the comparison.

Where:
- `collateral_usd` — current mark-to-market value of the position's collateral, in USD at `FLOAT_PRECISION`
- `fees_usd` — all currently-accrued fees on the position, in USD
- `pnl_usd` — unrealised profit/loss of the position, in USD
- `size_in_usd` — total notional size of the position
- `min_collateral_factor` — per-market configuration stored under `min_collateral_factor_key(market)` in `data_store`

If `min_collateral_factor` is unset (0, the fallback case only), the check instead
uses `net_collateral_usd + pnl_usd < 0` — unrealised PnL only enters the
liquidation check in this fallback branch, never in the primary comparison above.
If `min_collateral_factor` is unset (0, the fallback case), the check evaluates
`remaining < 0` (`net_collateral_usd + pnl_usd < 0`). Both primary and fallback
branches evaluate `remaining` (including PnL).

The `is_liquidatable` helper in `libs/position_utils` encodes this check and is the sole gate called by `LiquidationHandler::check_liquidatable`. If the check passes (position is healthy) the liquidation reverts with `NotLiquidatable`.

### Why the factor matters

`min_collateral_factor` is typically 1% (`FLOAT_PRECISION / 100`). A $10,000 notional position needs at least $100 of net collateral (after fees). As fees accrue (and, in the fallback case only, as unrealised PnL moves against the position), available collateral erodes — once it falls below this threshold the position can be forcibly closed to prevent bad debt from accumulating in the pool.
`min_collateral_factor` is typically 1% (`FLOAT_PRECISION / 100`). A $10,000 notional position needs at least $100 of net collateral (after fees and PnL). As fees accrue or as unrealised PnL moves against the position, available collateral erodes — once it falls below this threshold the position can be forcibly closed to prevent bad debt from accumulating in the pool.

---

Expand Down Expand Up @@ -109,9 +109,8 @@ If `gross_collateral < keeper_fee + liquidation_fee` the fees are capped at the
**Setup:**
- Position size: $10,000 notional
- Collateral: 2 tokens of long_token @ $100 each = $200
- `min_collateral_factor` = 1%
- Liquidation fee factor = 5% of gross collateral
- Keeper fee factor = 20% of liquidation fee
- `min_collateral_factor` = 1% ($100 required)
- `liquidation_execution_fee_key` = $10.00 (flat keeper execution fee)

**Health check before price move:**
```
Expand All @@ -127,12 +126,11 @@ required_collateral = $10,000 × 0.01 = $100
$90 < $100 → position is liquidatable
```

**Collateral distribution:**
**Collateral distribution (current implementation):**
```
gross_collateral = $90.00
liquidation_fee = $90.00 × 5% = $4.50 → insurance fund
keeper_fee = $4.50 × 20% = $0.90 → keeper wallet
remainder = $90.00 - $4.50 = $85.50 → position owner
keeper_fee = min($10.00, $90.00) = $10.00 → keeper wallet
remainder = $90.00 - $10.00 = $80.00 → position owner
```

---
Expand All @@ -144,7 +142,7 @@ remainder = $90.00 - $4.50 = $85.50 → position owner
| **Trigger** | Individual position health factor < 1 | `total trader PnL / pool_value` (FLOAT_PRECISION-scaled ratio) exceeds the per-market-per-side threshold stored under `max_pnl_factor_for_adl_key(market, is_long)` |
| **Executor role** | `LIQUIDATION_KEEPER` | `ADL_KEEPER` |
| **Position selection** | Any single position below the health threshold | Highest-profit positions first (most impact on pool) |
| **Fee charged** | Keeper fee + insurance fee | None |
| **Fee charged** | Flat keeper execution fee (`liquidation_execution_fee_key`) | None |
| **Outcome** | Position fully closed | Position partially or fully reduced |
| **Primary purpose** | Prevent bad debt on under-collateralised positions | Rebalance pool PnL when profitable OI grows too large |

Expand Down
12 changes: 3 additions & 9 deletions docs/oracle.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,8 @@ Each submitted price is valid for **exactly one ledger sequence window** (at mos

Keepers submit both a `min_price` and a `max_price` for each token. The spread represents oracle uncertainty (bid/ask spread or aggregator confidence interval).

| Scenario | Price used | Rationale |
|---|---|---|
| Long increase (open long) | `max_price` | Worst case for the buyer |
| Long decrease (close long) | `min_price` | Worst case for the seller |
| Short increase (open short) | `min_price` | Worst case for the buyer |
| Short decrease (close short) | `max_price` | Worst case for the seller |

This ensures users always trade at the price least favourable to themselves, preventing oracle-based front-running.
- For market orders (`increase_position` / `decrease_position`), execution prices are derived from `mid_price()` (`(min_price + max_price) / 2`) and adjusted directionally by price impact (`get_execution_price`).
- The `min_price` and `max_price` bounds are used for limit/stop trigger evaluation (`order_handler::execute_order`) and conservative PnL calculations (`pick_price_for_pnl`) to protect against oracle latency and manipulation.

---

Expand Down Expand Up @@ -131,7 +125,7 @@ stellar contract invoke \
bash scripts/submit_prices.sh testnet my-keeper
```

`submit_prices.sh` signs a test price bundle for the configured token and calls `oracle.set_prices`. If the signature or key lookup fails the invocation will revert.
`submit_prices.sh` constructs a test price bundle for the configured token and calls `oracle.set_prices_simple` (the no-signature test path, gated by the `testutils` feature).

---

Expand Down
4 changes: 1 addition & 3 deletions libs/position_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,7 @@ pub fn is_liquidatable(
collateral_token_price,
TOKEN_PRECISION,
);
// net_collateral excludes PnL — used for the min_collateral_factor adequacy check.
// PnL should not mask a collateral shortfall: a profitable unrealised gain does
// not mean the deposited collateral is sufficient to absorb liquidation costs.
// Fold PnL into the comparison so adverse index-price moves cannot hide insolvency.
let net_collateral = collateral_usd - fees_usd;
let remaining = net_collateral + pnl_usd;

Expand Down