# CCIP-028 review — the vault's sale path has zero oracle-fee headroom

**Target:** `Rapha-btc/citycoins-protocol`, branch `review/ccip028-direct-activation`,
commit `46885083b55362e06ab3386cc13d275ed6040bd0` (PR citycoins/protocol#92).
**Source identity verified against the bounty brief:**

| File | sha256 | Brief |
|---|---|---|
| `contracts/extensions/ccd015-redemption-book-mia-stx.clar` | `b3526d4ce89e615108c1b6781bf67b6f8354e3880adb404883a30fbdd36ec65f` | `b3526d4c…` ✔ |
| `contracts/extensions/ccd016-swap-vault-mia-v2.clar` | `ac0c305f8bfcfd9f210fb37cba6e1f4d61a26ad40ed7ca4792beca948778502e` | `ac0c305f…` ✔ |

---

## Finding — Low: `set-pyth-fee-budget u0` leaves no allowance for the one call every permissionless sale path makes

CCIP-028 line 37 sets the vault's Pyth fee subsidy to zero:

```
ccip028-activate-mia-fair-redemption.clar:37
  (try! (contract-call? .ccd016-swap-vault-mia-v2 set-pyth-fee-budget u0))
```

with the comment at line 28: *"Pyth is free at activation: no fee subsidy is withdrawn from the treasury."*

That is true today — the fee is governed, not constant. What the activation does not account for is that the
same variable is an **allowance, not a budget hint**:

```
ccd016-swap-vault-mia-v2.clar:1056
(define-private (vault-mid (update (buff 8192)))
  (as-contract? ((with-stx (var-get pyth-fee-budget)))
    (try! (current-mid update))
  )
)
```

`current-mid` → `JING_MARKET.refresh-mid` → `lazer-feeds` → `LAZER_ORACLE.verify-price-feeds`, and the oracle
charges its fee to `tx-sender` — which inside that `as-contract?` is the vault:

```
pyth-lazer-oracle (SPMV5HDZ4EMB8XY7HAYT3XW0DF7DZ4E8XEG2J1T8), charge-fee:
  (stx-transfer? fee-amount tx-sender (var-get fee-recipient))   ;; only when fee-amount > 0
(define-data-var fee uint u0)
```

With `pyth-fee-budget = u0` the STX allowance on the body is exactly zero. Any non-zero fee therefore fails
the allowance check and reverts **the whole entry**, not just the fee.

**Every permissionless sale entry goes through `vault-mid`:**

| Line | Entry | Caller |
|---|---|---|
| 499 | `jing-place` | permissionless |
| 532 | `jing-refloor` | permissionless |
| 563 | `jing-take` | permissionless |
| 613 | `router-swap` | permissionless |
| 673 | `router-swap-split` | permissionless |

The only sale path that does **not** touch the Pyth verifier is `router-swap-split-dia` (line 733), and it is
`is-dao-or-extension`-gated (line 740), so no stranger can drive it and it is unavailable while the DAO is
inactive.

### Measured, on mainnet, 2026-11-06

```
POST https://api.hiro.so/v2/contracts/call-read/SPMV5HDZ4EMB8XY7HAYT3XW0DF7DZ4E8XEG2J1T8/pyth-lazer-oracle/get-fee

  {"okay":true,"result":"0x0100000000000000000000000000000000"}   ;; (ok u0)
```

So activation is **correct today** and no funds are at risk. The defect is that the launch configuration has no
headroom for the value it depends on:

* `fee` is a governance variable on the oracle (role `ROLE_FEE`), changeable without the DAO's involvement.
* If it is ever set above zero, all five permissionless entries revert on the allowance, the vault stops
  converting sBTC, `fuel-fair-book` receives no STX, and the book gets no budget — the feature CCIP-028 exists
  to start is off, with no permissionless workaround.
* Recovery needs another full proposal cycle (propose + 3 of 5 approver signals). `dao-recall-sbtc` still
  returns the vault's sBTC to `ccd002-treasury-mia-rewards-v3`, so this is an availability defect, not a loss
  of funds.

### Fix

Set a small non-zero subsidy at activation — one STX is enough for ~100k verifies at a 10 µSTX fee and is
7 orders of magnitude below `MAX_PYTH_FEE_BUDGET` (u10000000 = 10 STX):

```
(set-pyth-fee-budget u1000000)   ;; 1 STX of headroom
```

The vault then degrades (pays the raise) instead of stopping.

---

## What I verified and did not find

Recorded so the next reviewer does not re-derive it.

* **The book is a faithful fork of the production book, in the three places the header says.** Diffed against
  the live `SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22.mia-fair-faktory-v2` source (278 lines) fetched from the
  Hiro API: `place-offer`, `cancel-offer`, the sorted insert and the frontier partial fill are the same
  arithmetic. The three deviations are all intentional and all safe: the payer moves from `tx-sender` to the
  contract (so the upstream self-offer skip at `settle-step` is correctly dropped), the par comparison becomes
  cross-multiplied exact (`at-or-below-par?`, line 302) instead of division-truncated, and MIA is burned rather
  than delivered to a settler.
* **The partial-fill remainder cannot invert the sort or exceed par.** In `settle-step` (line 462)
  `taken = floor(amount * remaining / ask)` and the kept record becomes
  `(amount - taken, ask - remaining)`. For `taken > 0`, `amount * remaining >= ask * taken`
  ⇒ `(ask - remaining) / (amount - taken) <= ask / amount`, so the leftover's implied price never rises above
  its original, and both it and the queue order stay at or below par. `taken = 0` is only reachable when
  `remaining = 0`, in which case no later entry can be paid either (`remaining` stays 0), so a pricier offer
  cannot jump a cheaper one.
* **MIA custody matches the book.** Every record's `amount` is transferred in at `place-offer` (line 179) or
  `change-offer` (line 222), refunded at `cancel-offer` / eviction (line 424), and only `acquired` is burned
  (line 278), inside `as-contract?` guarded by a `with-ft` allowance of exactly `acquired`. One offer per owner
  is enforced by `ERR_HAS_OFFER` (line 178), which is what makes the single-record refund in `cancel-offer`
  correct.
* **`cross-book` cannot overpay.** The budget is `stx-get-balance(current-contract)` (line 257) and the fold
  runs inside `as-contract? ((with-stx budget))` (line 265); `settle-step` decrements `remaining` by exactly
  what it transfers, and every payout is additionally gated by `at-or-below-par?` at cross time (line 477).
* **Par is units-correct against live state.** `get-treasury-balance` sums `stx-account` of
  `ccd002-treasury-mia-mining-v3` and `ccd014-pox5-staking-mia`; live reads today give mining-v3
  `balance 0 / locked 0` and pox5 `locked 0x95088cb6d2a` = 10,241,497.066794 STX — no double count, since the
  balance sits only in the staking contract. Combined supply 5,014,205,754.491373 MIA gives
  `par_scaled = 10241497066794 * 1e8 / 5014205754491373 = 204249`, i.e. 2,042.49 STX per 1M MIA, matching the
  brief's rehearsal figure.
* **All nine vault setters in CCIP-028 are within their caps, and `set-extension` / `set-allowed` run as
  base-dao** (tx-sender = base-dao inside the `as-contract` that `base-dao.execute` opens, so the proposal's
  `(and (is-eq contract-caller DAO) (is-eq tx-sender DAO))` guard at line 15 holds and the treasury's
  `is-dao-or-extension` passes).
* **Re-execution is blocked twice**: `ExecutedProposals` map-insert in `base-dao.execute` (line 44) and the
  proposal's own `get-par-scaled == u0` guard (line 16).

## What I did not cover

The Jing market and router sources beyond `refresh-mid`/`lazer-feeds` (live, unmodified, and out of scope);
the vault's router split maths under adversarial pool state; and the book's behaviour at exactly 50 offers —
I read that path (`place-offer` eviction, lines 181-192) but did not execute it, so it is unverified rather
than cleared.
