# Funding, Fees, Liquidation & Margin — derived from contract code (+ live-verified)

All values below are read **directly from the contracts** in `src/` and confirmed with **live on-chain reads/tests** on Arc testnet (chain 5042002) and Monad testnet (chain 10143). No external docs were used.

Constants are the deployed defaults (`MarketHandler`, `PositionHandler`, `BalanceManager`); several are owner-settable at runtime.

---

## 1. Funding rate

**Where:** `MarketHandler.estimatePendingFundingFee` (src/perpetual/MarketHandler.sol:375) and the inline copy in `PositionHandler.updateFees` (src/perpetual/PositionHandler.sol:547-589).

**Constants** (MarketHandler.sol:16-18): `BASE_FUNDING_RATE = 1e14`, `MIN_FUNDING_RATE = 1e13`, `MAX_FUNDING_RATE = 1e15` (all per **hour**, scaled by 1e18 → base = 1e14/1e18 = **0.01%/hour**, min 0.001%/hr, max 0.1%/hr).

**Interval:** funding is charged per **whole hour**. In `updateFees`: `hoursElapsed = (block.timestamp - position.increasedAtTime) / 3600` (integer division → any partial hour < 1h accrues **0**).

**Rate (imbalance-scaled):**
```
totalOI    = longOI + shortOI            (0 → funding 0)
imbalance  = longOI - shortOI            (0 → funding 0)
pif        = imbalance * 1e18 / totalOI            // -1e18 .. 1e18
adjRate    = BASE_FUNDING_RATE * (1 ± |pif|)        // clamped to [MIN, MAX]
ff         = +adjRate for the DOMINANT side, -adjRate for the minority side
periodFundingFee = sizeInUsd * ff * hoursElapsed / 1e18 / 10^(18-collateralDecimals)
```
The **dominant OI side pays**, the minority side **receives** (`cumulativeFundingFee` ±, `collateralAmount` -/+). See PositionHandler.sol:576-635.

**Held 2h / 10h (on $1,000 notional):**
| held | balanced book | base rate (0.01%/h) | max rate (0.1%/h) |
|---|---|---|---|
| 2h  | $0.00 | $0.20 | $2.00 |
| 10h | $0.00 | $1.00 | $10.00 |

**Live verification (Arc):** EUR-PERP has `longOI = shortOI = 20e18` (balanced) → `estimatePendingFundingFee = 0`. Confirmed: A's EUR cross position open ~2.48h shows `cumulativeFundingFee = 0`. Funding only accrues when the book is imbalanced.

---

## 2. Borrowing fee (always-on, time-based)

**Where:** PositionHandler.updateFees (src/perpetual/PositionHandler.sol:598-600). Constant `BASE_BORROWING_RATE = 100` (MarketHandler.sol:21) = **100 bps = 1% APR**.

```
periodBorrowingFee = sizeInUsd * BASE_BORROWING_RATE * hoursElapsed / (8760 * 10_000 * 10^(18-collateralDecimals))
                   = notional * 1% * hoursElapsed / 8760            // 8760 = hours/year
```
Always accrues (independent of imbalance), per whole hour, deducted from collateral.

**Held 2h / 10h:** on $1,000 → 2h = $0.0000228×1000 = **$0.00228**, 10h = **$0.0114** (1% APR is small over hours). Live: A's $23.296 EUR position @ 2h ⇒ $0.000053; @10h ⇒ $0.000266.

---

## 3. Perp trading (position) fee — when an order EXECUTES

**Where:** PositionHandler.updateFees (src/perpetual/PositionHandler.sol:605-607). Constant `POSITION_FEE = 10` = **10 bps = 0.1%** of `sizeInUsd`.

`updateFees` (and thus all fee charging) runs only when a position is **created/modified by a fill**:
- `increasePositionFromMatch` — when a limit **or** market order **matches** and opens/increases the position.
- `_decreasePositionInternal` — on close / partial close / liquidation.

A **resting** limit order that has not matched charges **nothing**; fees apply on the **fill**.

Position-fee rule (line 605): `positionFee = (isNewPosition || isLiquidation) ? 0 : sizeInUsd * 0.1%`.
- **First open** (new position, `increasedAtTime == 0`): **no** position fee.
- **Increase / close** (existing position): **0.1%** of position `sizeInUsd`.
- **Liquidation**: no position fee (but funding+borrowing still settle, plus the liquidation penalty below).

**Live-verified (Arc leverage test):** opened 10 EUR @2x (collateral 5.824), then added +10 EUR @2x. Expected collateral 2×5.824 = 11.648; actual = **11.636352**; drop = **0.011648 = exactly 0.1% × sizeInUsd(11.648)** → POSITION_FEE applied on the increase, as coded.

Limit vs market order makes **no difference** to the fee — both pay 0.1% on the matched fill (perp has no separate maker/taker split; the orderbook fills at the resting price).

---

## 4. Spot trading fees (maker/taker)

**Where:** `BalanceManager` (src/BalanceManager.sol). `feeMaker`, `feeTaker` over `feeUnit`.
**Live (Arc):** `feeMaker = 1`, `feeTaker = 2`, `feeUnit = 10000` → **maker 0.01%, taker 0.02%**. Owner-settable, capped at 500 (5%) (BalanceManager.sol:95).
- `feeMaker` is charged in `transferFrom` (taker pays maker), `feeTaker` in `transferLockedFrom` (maker's locked funds released to taker). Per Security Rule 14, the names refer to **who receives**. Spot fees apply on the matched fill of a market/limit order.

---

## 5. Liquidation penalty

**Where:** `_decreasePositionInternal` with `_isLiquidation = true` (src/perpetual/PositionHandler.sol:809-827). Constant `LIQUIDATION_FEE = 50` = **50 bps = 0.5%** (PositionHandler.sol:63), owner-settable up to 1000 (10%) (line 341).

Order of operations on a liquidation:
1. `updateFees(...)` settles accrued **funding + borrowing** first (fees on full size, line 809-811).
2. `increasedAtTime` reset (line 817).
3. `collateralToReturn` = post-fee collateral (× sizeRatio for partial).
4. **Penalty:** `liquidationFee = collateralToReturn * 0.5% / 10000; collateralToReturn -= liquidationFee` (line 824-825). The 0.5% is taken from the user's returned collateral and added to `feeAmount` (liquidator reward / insurance).

So a liquidated trader's recovered collateral = `collateral − (funding + borrowing) − 0.5% liquidation penalty`. **Live-verified** on Arc + Monad: liquidations executed and closed the position to zero (see `docs/perp-edge-case-tests.csv`).

---

## 6. Margin requirements ("safe margin") + liquidation trigger

**Where:** `DataStore.getMarginRequirements` (src/perpetual/DataStore.sol:719) returns `(maintenanceMarginBps, initialMarginBps, maxLeverage)`.

- **Global default** (no per-market tiers, line 727): **5% maintenance / 10% initial / 20x max**.
- **Per-market tiers** (notional-banded) override the default. **Live (Arc) `getMarginRequirements(EUR-PERP)` = (100, 200, 50)** → the synthetic markets are configured **1% maintenance / 2% initial / 50x max**.

**Liquidatable when** (isolated, PositionHandler.isPosisitionLiquidatable src/perpetual/PositionHandler.sol:700):
```
effectiveCollateral = collateral − pending(borrowing + positive funding)
remainingCollateral = effectiveCollateral + PnL
maintenanceMargin   = sizeInUsd * maintenanceMarginBps / 10000
liquidatable        = remainingCollateral < maintenanceMargin
```
**Cross** delegates to `BalanceManagerVault.canLiquidateCrossMargin` (account-level: `equity*100/maintenanceMargin18 < 100`) — **only** if `unifiedVault` is wired (this was the deployment gap fixed in commit 6ec3150).

**Exact liquidation price:** `PositionLens.getLiquidationPrice(positionKey)` (src/perpetual/PositionLens.sol:81) returns it for isolated positions (0 for cross — cross is portfolio-level). Live-verified to the tick on Arc ($1.059877) and Monad ($1.066247): one tick above ⇒ not liquidatable, at/below ⇒ liquidates.

Worked example (10x long, 1% maintenance): initial margin = 10% of notional; liquidation triggers at ≈ **−9% price move** (10% margin − 1% maintenance), matching the live EUR boundary tests.

---

## 7. When does each fee actually apply? (timing summary)

`updateFees` is the single charging point; it runs **on a fill that modifies a position** (open-increase / close / liquidation), never on a resting order. `hoursElapsed` is **integer hours** since `increasedAtTime`, which is **reset on every modification** (Security Rules 18-19) so partial closes don't double-charge.

| Event | Position fee (0.1%) | Borrowing (1% APR·h) | Funding (imbalance·h) | Liquidation penalty (0.5%) |
|---|---|---|---|---|
| First open (fill) | — (new) | — (new) | — (new) | — |
| Increase (fill) | yes | accrued since last mod | accrued since last mod | — |
| Close / partial (fill) | yes | accrued | accrued | — |
| Liquidation | — | accrued | accrued | yes (0.5% of returned collateral) |
| Spot fill | maker 0.01% / taker 0.02% | n/a | n/a | n/a |

---

### Source map
- Funding: `MarketHandler.sol:16-18,375-405`; `PositionHandler.sol:547-589`
- Borrowing: `MarketHandler.sol:21`; `PositionHandler.sol:598-600`
- Position fee: `PositionHandler.sol:63-64,605-607`
- Liquidation penalty: `PositionHandler.sol:63,809-827`
- Margin: `DataStore.sol:719-740`; liquidation gate `PositionHandler.sol:700`
- Cross health: `BalanceManagerVault.getCrossMarginPortfolioHealth` (555-591), `canLiquidateCrossMargin` (548)
- Liq price view: `PositionLens.sol:81`
- Spot fees: `BalanceManager.sol:32-41,91-97,522-534`

*Live verification artifacts: `docs/perp-edge-case-tests.csv` (7 real-state scenarios on Arc + Monad).*
