# QRE Stage 7 — Multi-Asset Position-Open Backing

**Status:** Design proposal, awaiting confirmation
**Author:** Claude (per user direction 2026-05-28 "Build Stage 7 first, then test")
**Related stages:** Stage 2 (withdraw gate), Stage 3 (liquidation trigger), Stage 5 (liquidation conversion)

## Problem

`BMV.allocateForPosition` (BalanceManagerVault.sol:184) enforces:
```solidity
if (freeBalance < amount) revert InsufficientBalance(amount, freeBalance);
```
where `freeBalance` = user's free USDC balance, `amount` = initial margin (IM).

This means a user with `$170 WBTC + $250 USDC = $419 QRE equity` can only open positions whose IM is `<= $250` (the USDC alone). Their WBTC backing is ignored on opens.

**Goal:** allow CROSS-mode positions to open when `QRE.equity18 >= total maintenance margin`, even if USDC alone is insufficient. ISOLATED positions stay USDC-only.

## Design

### Storage change

`IBalanceManagerVault.PositionAllocation`:
```diff
 struct PositionAllocation {
     uint128 amount;       // Position's claimed IM (UNCHANGED — used by PositionHandler for sizing)
     uint8 mode;
     bool exists;
     address market;
+    uint128 usdcLocked;   // NEW: actually locked USDC (≤ amount; rest backed by QRE equity)
 }
```
Appending to the struct is storage-safe because `positionAllocations` is a mapping (each entry sits in its own slots; growing the struct doesn't shift other layouts).

### `allocateForPosition` change

```solidity
function allocateForPosition(address user, bytes32 posKey, address market, uint256 amount, MarginMode mode) external {
    // ... existing setup ...
    Currency usdcCurrency = Currency.wrap($.usdc);
    uint256 freeBalance = IBalanceManager($.balanceManager).getBalance(user, usdcCurrency);

    uint256 toLock;
    if (mode == MarginMode.ISOLATED) {
        // ISOLATED: full USDC backing required per position (unchanged).
        if (freeBalance < amount) revert InsufficientBalance(amount, freeBalance);
        toLock = amount;
    } else {
        // CROSS: lock available USDC (up to amount), require QRE equity covers the rest.
        toLock = freeBalance >= amount ? amount : freeBalance;
        uint256 virtualBacking18 = (amount - toLock) * 10 ** (18 - $.collateralDecimals);
        if (virtualBacking18 > 0) {
            address qre = $.quantumRiskEngine;
            if (qre == address(0)) revert InsufficientBalance(amount, freeBalance); // fallback to legacy
            int256 equity18 = _qreEquity18(qre, user);
            uint256 mm18 = _qreMaintenanceMargin18(qre, user); // current MM
            // New position adds ~maintenanceMarginBps * sizeInUsd MM. For simplicity + conservatism,
            // require equity covers (currentMM + new IM). IM ≥ MM, so this is a strict upper bound.
            uint256 required18 = mm18 + (amount * 10 ** (18 - $.collateralDecimals));
            if (equity18 < int256(required18)) revert InsufficientEquity(required18, equity18);
        }
    }

    if (toLock > 0) IBalanceManager($.balanceManager).lock(user, usdcCurrency, toLock, address(this));

    // Track per-account allocation by the FULL claimed amount (preserves existing semantics).
    UserAccount storage account = $.accounts[user];
    account.totalAllocated += uint96(amount);
    if (mode == MarginMode.ISOLATED) account.isolatedAllocated += uint96(amount);
    else                              account.crossMarginAllocated += uint96(amount);

    PositionAllocation storage allocation = $.positionAllocations[posKey];
    if (allocation.exists) {
        allocation.amount += uint128(amount);
        allocation.usdcLocked += uint128(toLock);
    } else {
        allocation.amount = uint128(amount);
        allocation.usdcLocked = uint128(toLock);
        allocation.mode = uint8(mode);
        allocation.market = market;
        allocation.exists = true;
    }
    emit CollateralAllocated(user, posKey, market, amount, mode);
}
```

### `releaseFromPosition` change

Pass `usdcLocked` (not `amount`) to `_handleRelease` as the "collateral that's actually unlock-able":

```solidity
uint256 allocationAmount = allocation.amount;       // claimed IM
uint256 lockedAmount = allocation.usdcLocked;       // actually locked USDC

// PnL is applied to the FULL claimed amount (position's view).
int256 adjustedPnl = int256(collateralToRelease) + pnlInCollateral - int256(allocationAmount);

// But the lock-and-settle uses the ACTUAL locked USDC.
// The "virtual" piece (allocationAmount - lockedAmount) was never USDC — losses there
// reduce the user's portfolio equity by depleting non-USDC collateral via Stage 5.
_handleRelease($, user, market, lockedAmount, adjustedPnl, pnl, mode);
// ... rest unchanged, but subtract lockedAmount in the unlock-tracking line ...
```

### `_handleRelease` — no logic change

The function's `collateral` param semantically becomes "actually locked USDC". The existing branches still work:
- `finalCollateral > 0` → unlock min(finalCollateral, lockedAmount); credit excess from insurance — **correct**
- `finalCollateral <= 0 && lockedAmount > 0` → forfeit lockedAmount to insurance; deduct deficit from cross-account — **correct**
- `lockedAmount == 0` (fully virtual-backed position closed at loss) → no unlock needed; deduct full deficit from cross-account via `_settleCrossAccountDeficit` — **already handled by existing branch**

### Liquidation path (already wired)

When a CROSS position is liquidated:
1. `BMV.canLiquidateCrossMargin` → `QRE.isLiquidatable` returns true when `equity18 < MM`
2. Liquidator pays MM, gets `lockedAmount` of USDC
3. If `lockedAmount < MM owed` → Stage 5 conversion kicks in: spot-sell user's WBTC/WETH at oracle×liqFactor for USDC, transfer to liquidator
4. Insurance/backstop absorbs any final deficit

**No new liquidation code needed** — Stage 5 already covers this path. Only `_handleRelease` needs the lockedAmount switch.

### New error

```solidity
error InsufficientEquity(uint256 required18, int256 equity18);
```

### Test scenarios

1. **CROSS open with USDC-only** (regression): user has $500 USDC, opens $3000@10x IM=$300 → success, locks $300 USDC.
2. **CROSS open with mixed backing** (Stage 7): user has $250 USDC + $170 basket = $419 equity, opens $3500@10x IM=$350 → success, locks $250 USDC; allocation.usdcLocked = 250, allocation.amount = 350.
3. **CROSS open exceeds equity** (revert): user has $250 USDC + $170 basket = $419 equity, opens $5000@10x IM=$500 → revert `InsufficientEquity`.
4. **ISOLATED open requires USDC** (unchanged): user has $250 USDC + $170 basket, opens ISO $3000@10x IM=$300 → revert `InsufficientBalance` (basket doesn't back ISOLATED).
5. **Profitable close, virtual-backed position**: lockedAmount=$0, PnL=+$100 → user credited $100 from insurance, allocation deleted.
6. **Loss close, virtual-backed position**: lockedAmount=$0, PnL=-$50 → `_settleCrossAccountDeficit` deducts $50 from cross PnL; basket depletion happens later via natural liquidation flow.
7. **Partial-loss close, partially-backed position**: lockedAmount=$250, loss=$300 → unlock $0 (all forfeited), $50 deficit handled.
8. **Liquidation of virtual-backed position triggers Stage 5**: existing liquidation test extended.

### Audit surface

- `BMV.allocateForPosition` — new branching + QRE call
- `BMV.releaseFromPosition` / `partialReleaseFromPosition` — lockedAmount substitution
- `BMV.deallocateFromPosition` (cancel path) — same substitution
- `PositionAllocation` struct — new field
- `_handleRelease` — no logic change, just clearer semantics

### Out of scope

- Settlement-on-loss via auto-borrow USDC (Stage 6 — Model 3)
- Per-asset margin caps (already in QRE via CollateralConfig.maxNotionalUsd18)

## Open questions

1. **Conservatism of `required18 = mm18 + amount`** — using IM (not MM) as the new position's contribution overestimates the equity requirement. Pro: extra safety margin. Con: less efficient capital use. Alternative: compute MM directly via `mm_bps * sizeInUsd`. Recommend going with the conservative version first, refine if needed.

2. **Should ISOLATED also gain QRE-equity backing?** Hyperliquid keeps ISOLATED truly isolated (USDC-only per position). Recommend matching that — current spec keeps ISO USDC-only.

3. **Storage layout safety on upgrade** — the new `usdcLocked` field is appended to the struct, which lives in a mapping. Existing entries (without usdcLocked) will default to 0, which would WRONGLY indicate "fully virtual-backed". **Mitigation:** on first read of a pre-Stage-7 allocation, fall back to `usdcLocked = amount` (assume all locked) if `usdcLocked == 0 && amount > 0`. This is the safest backfill — no existing position loses its backing tracking.

## Plan

- [x] Read full lifecycle (allocateForPosition → releaseFromPosition → _handleRelease)
- [x] Write this design
- [ ] Implement in BMV + interface + storage struct
- [ ] Add `_qreEquity18` and `_qreMaintenanceMargin18` internal helpers (staticcall + fallback)
- [ ] Add 8 test scenarios in BMVStage7Test
- [ ] Run all tests (must stay green)
- [ ] Run Pashov audit on changed surface
- [ ] Run Nemesis audit
- [ ] Fix + loop until clean
- [ ] Redeploy BMV impl on Arc + beacon upgrade
- [ ] Run TestQREArcE2EStep2 — should now SUCCEED at opening $3500@10x with mixed backing
