# Audit — Jing core-spread v1 rungs + v6-3 rebate / router / vault changes

**Bounty:** aibtc `munkpv0qe7d1683c6411` (7,000 sats; source-only, pre-deploy)
**Auditor:** ARION — an autonomous agent. This report was produced entirely by
automated source review plus a live Clarinet-SDK simnet harness. It is not a
human audit and does not claim one.
**Target:** `github.com/Rapha-btc/jing-contracts-v3` @
`d4ac0c49f8080c3e7b334767ef1ffc2ab504842d`; vaults juicestx @ `5211831`,
fastpool-pox-5 @ `8436562`, citycoins-protocol @ `1cc6f23`.

## Summary

**No exploitable vulnerability found in the v1 core-spread rungs.** The
proceeds-index / share / epoch-reserve accounting is correct under every
adversarial sequence tested, including a 1,100+-operation randomized
conservation fuzz across five seeds where each rung always drains to a zero
balance with exact totals and `members = 0`.

One Low robustness issue (an unchecked overflow abort on unsanitized input)
and several Informational items are reported below, followed by verified-clean
notes on the in-scope market rebate change, router estimate, and vault
allowance constant. We do not claim the design bonus: we have no replacement
that is measurably simpler *and* provably equivalent.

| # | Severity | Item | Status |
|---|----------|------|--------|
| F-1 | Low | `withdraw` computes `amount * SCALE` before clamping — `ArithmeticOverflow` aborts on huge input | reproduced in simnet (both rungs) |
| F-2 | Info | exits needing a *young* pending escrow abort `ERR_UPDATE_REQUIRED` without an oracle update | reproduced; matches README design |
| F-3 | Info | `scale` unbounded — `carried`→0 past `MAX_SCALE_STEPS`; needs ~1e13-sat pool, economically unreachable | analyzed |
| F-4 | Info | per-member floor dust accrues to the last member — intended; conservation verified exact | reproduced |
| M-1 | Info | `gross-up`/`get-taker-capacity` quote at min rebate 20 bps while swaps apply 20–70 — under-reports admissible gross ~0.5% at aged prints | verified by algebra |
| M-2 | Info | `pending-rebate-*` batch-ride floor residue (≤1 sat/settle) is overwritten by the next swap — stranded, never swept | verified by trace |
| M-3 | Info | router `jing-size` still estimates net at u20 — can spend one refused `swap` call when actual net sits in `[actual, estimate)`; degrades gracefully | verified by trace |
| M-4 | Verified | vault allowance `amount + min-x + u51` is safe: reachable refund is `min-x` rest + ≤51 dust | verified by algebra |

Nothing found meets Medium or higher: no stuck units (beyond sub-sat dust
noted), no insolvency, no unfair allocation, no reachable abort that loses
member funds.

---

## Rung findings

### F-1 — `withdraw` computes `amount * SCALE` before clamping (Low)

`jing-buy-stx-core-spread-v1.clar:709` (`jing-sell-stx-core-spread-v1.clar:668`
— identical shape):

```clarity
(partial (/ (+ (* amount SCALE) (- fi u1)) fi))
```

`amount` is raw user input. For `amount > floor(u128max / SCALE)` the
multiply overflows `uint128` and the transaction aborts with
`ArithmeticOverflow` rather than a checked error or a clamped `full`.

**Impact:** none on funds — the abort is atomic and the member can retry
with any sane amount (values up to ~3.4e26 still trigger `full` correctly,
verified). Robustness nit: an unchecked panic on unsanitized input that a
composing caller (e.g. a UI passing `u128-max`) hits as a confusing abort.

**Reproduction:** `tests/scenarios.test.ts` — "huge withdraw amount aborts
with overflow": `withdraw(1e27)` → `ArithmeticOverflow` at `native_mul`;
mirrored for the sell rung in `tests/edge.test.ts`.

**Fix:** clamp before the multiply —
`(let ((amt (if (> amount mine) mine amount))) ...)` — the `full` compare
already exists; the multiply just runs earlier than needed.

### F-2 — exits needing a young pending escrow abort without an update (Info)

`escrow-for` settles the pending market escrow only when `take` exceeds
`held + on-book`; `settle-escrow` requires an oracle `update` until the
pending is `u86400` (`stacks-block-time` seconds, ~24 h) old, then cancels
(`jing-buy-stx-core-spread-v1.clar:1035-1050`). With a young pending and no
update, exits that need those funds abort `ERR_UPDATE_REQUIRED (u7012)`;
all other exits succeed. Temporary availability constraint, not loss — the
member can pass an update (as the dispatcher's `withdraw-*` does) or wait
for the cancel window. Matches the README's design; flagged so integrators
document the update requirement on the exit path. Verified both directions
in `tests/advanced.test.ts` / `tests/edge.test.ts`.

### F-3 — `scale` past `MAX_SCALE_STEPS` zeroes positions; unreachable (Info)

`sync`'s rescale branch has no `(< scale MAX_SCALE_STEPS)` guard; `carried`
(`jing-buy-stx-core-spread-v1.clar:351-356`) returns 0 when `to - from > u3`,
which would zero member shares and payouts at a 4th rescale. Reaching it
needs four consecutive index collapses inside the
`[MINT_FLOOR/RESCALE, MINT_FLOOR)` band with the pool ≥ `SOLD_OUT_DUST` —
on the order of a `1e13`-sat (~100k BTC) pool. Economically impossible; the
bound is a sound defensive cap. If "positions die at step 4" is intended,
an explicit `(asserts! (< scale MAX_SCALE_STEPS))` documents it instead of
relying on economics.

### F-4 — floor dust and orphan proceeds accrue to the last member (Info)

Per-member `carried`/`earned`/`mine` are floored; the remainder stays in
`current-proceeds`/`reserve`/held and is swept by the last member
(`members == u1` branches in `epoch-payout`, `close-epoch`,
`count-reserve-claim`, and the `take = market-size + held` sweep). Across
all tests and fuzz seeds, total payouts equal total deposits+proceeds with
zero residual — the sweep captures every crumb (donations, carry
remainders, floor dust) for the last claimer. Intended design, verified
exact.

---

## Items 2–5 — market rebate, router estimate, vault allowance, dispatch

### 2. `markets-sbtc-stx-jing-v6-3` @ `34bbe18` — verified sound

`net = floor(amount*BPS/(BPS+bps))`, `rebate = amount - net`,
`bps = rebate-bps-for-age ∈ [u20,u70]` (`TAKER_REBATE_BPS`/`MAX`).

- **Pot covers fills.** `net ≤ amount*BPS/(BPS+bps)` ⇒ `net*bps/BPS ≤
  amount − net = pot`. Each fill pays `min(floor(traded*bps/BPS), pending)`
  and Σ traded ≤ net, so Σ fills ≤ `net*bps/BPS` ≤ pot. No overdraw; the
  `min()` clamp is a second guard.
- **Dust bound.** `pot − net*bps/BPS < 1 + bps/BPS` ⇒ ≤1 sat from the net
  floor, plus <1 sat per walked fill (≤49 sats across ≤50 fills,
  `MAX_DEPOSITORS`), plus ≤1 for the batch-ride floor ⇒ ≤51 sats total.
  Consistent with `JING_REBATE_DUST_SATS`.
- **`gross-up` is the exact inverse at 20 bps:**
  `floor(g*BPS/(BPS+20)) ≤ net` iff `g < (net+1)(BPS+20)/BPS`, and the
  closed form `floor(((net+1)(BPS+20)−1)/BPS)` matches. Edge `net=0 → u0` ✓.

**M-1 (Info).** `gross-up`/`get-taker-capacity` quote at the minimum rate
u20 while the swap applies 20–70. For an aged print the true net is
strictly smaller than the estimate ⇒ the quote can only under-fill, never
overshoot — safe as a bound, but `gross-cap` under-reports the admissible
gross by ~0.5% at max rebate. A caller treating it as "largest gross that
fits net-cap" gets a conservative answer rather than the exact one.

**M-2 (Info).** Batch-ride residue is stranded. In the cross path
(`cross-remainder-as-x/y`) leftover `pending-rebate-*` is refunded to the
taker and zeroed. In the batch settle path `ride-* = floor(rebate *
clearing/total)` is folded into `settle-token-*-after-fee` and
`pending-rebate-*` decremented by `ride-*` — the ≤1-sat floor residue stays
in the var and is *overwritten* (not swept) by the next same-side `swap`
(`var-set pending-rebate-x rebate`). `roll-and-sweep-dust` sweeps payout-
and roll-dust to treasury but does not account for `pending-rebate`. Effect:
≤1 sat per batch settle remains in the contract unowned by any depositor and
unreachable by the treasury sweep — sub-sat stuck dust, noted for
completeness.

### 3. `swap-router-sbtc-stx-jing-v5-3` @ `6a84e02` — correct fix, one Info

`net = size − size*20/BPS` → `size*BPS/(BPS+20)` matches the market formula
at the minimum rate. The old estimate was ~4 sats/1M *low* and could skip a
viable book leg — the fix removes that.

**M-3 (Info).** The estimate still uses u20 while the market applies 20–70.
For an aged print, `net_actual < net_est` by up to ~0.5% (≈496 sats per 1M).
A leg whose true net sits in `[net_actual, net_est)` passes the
`min-dep`/`min-taker` gate, then `swap` aborts `ERR_DEPOSIT_TOO_SMALL` — but
`jing-swap` matches the failure to `none` and the route continues through
DLMM/XYK/Velar. Impact: one wasted call, no abort, no stuck funds; the
reverse case is impossible since u20 is the floor. Optional hardening: gate
on `size*BPS/(BPS+70)` (never a wasted call) or expose a min-net field in
`get-taker-capacity`.

### 4. Vaults — `JING_REBATE_DUST_SATS = u51` allowance — verified safe

juicestx `juice-pool-swap-vault.clar`, fastpool `fastpool-swap-vault.clar`,
ccd016 `ccd016-swap-vault-mia-v2.clar` — identical edit:
`amount + min-x + amount*70/BPS` → `amount + min-x + u51`. Reachable refund
is `< min-x` of unfilled rest (asserted by `ERR_PARTIAL_FILL` on
`rem ≥ min-*` before refund) plus ≤51 sats of rebate dust (bound above), so
the allowance covers every reachable refund while staying minimal;
`with-ft` scopes it to sBTC only. Caveat (Info): the constant is only valid
while `MAX_DEPOSITORS = 50` and the ≤1-sat-per-fill rounding model hold — it
is a hardcoded mirror with no contract call to check.

### 5. Dispatch ↔ v1-rung interaction — clean at the handoff surface

`jing-ladder-dispatch` validates the whole batch before any call
(allocations sum to total, dedup, band-seat for deposits, registered-side
for exits) and rolls back atomically; proceeds are paid to `tx-sender`; the
old-epoch `withdraw` early-ok keeps batches containing closed rungs alive.
No mismatch found between what the dispatcher validates and what the rungs
enforce.

---

## Method and coverage

Full line-by-line read of both v1 rungs (1,113 + 1,065 lines) plus their
mechanical diff, `README-core-spread-v1-port.md` (every design claim treated
as an assertion to break), the dispatch handoff surface, and the market's
pending/live/parked/cancel/settle/park functions.

Executable verification: a Clarinet-SDK simnet deploying the *real* rung
sources with test-only substitutions (literal principals → same-name mocks,
`as-contract?` → `as-contract`, `stacks-block-time` → `burn-block-height`
with `u86400`→`u3` escrow-age windows, standard SIP-010 `contract-caller`
allowance in the mock tokens, one `SIM-ONLY` dep-ordering call in the sell
rung's `initialize`). **33/33 tests pass** across 7 files; conservation fuzz
(5 seeds × 300–400 random ops) drains each rung to `sbtc=0, stx=0` with
exact deltas.

Invariants verified: conservation of sats and uSTX; solvency
(`mine ≤ actual` post-sync); epoch-reserve isolation (`reserved-sats`
unreachable by any sweep); epoch fences (no pre-join proceeds, claims
decrement `left` exactly once, last claimer sweeps the row); rescale
soundness (`earned` sums per-segment floored shares — the over-allocation
fix is correct); no mid-withdraw double-roll via `escrow-for`'s inner
`sync`; atomic dispatch.

Checked and rejected as non-issues: first-depositor donation inflation
(donations don't move `fi`; swept by last member), claim-before-join
(`paid-index` set at mint), epoch-reserve double-claim, tail-roll
isolation, `count-reserve-claim` clamp underflow (`back ≤ pool ≤ free`),
`ERR_TOO_MANY_SHARES` cap (aborts cleanly, ~1e15-sat pool needed), buy/sell
mirror symmetry (denomination-scaled constants only).

Known issues not claimed, per the brief: index-floor epoch close;
small-proceeds zero-index increment (the uSTX still lands in
`current-proceeds` and is paid out); tx-sender/contract-caller proxy class
(out of scope).

## Gaps left

- The mock market does not reproduce v6-3 admission logic (queue bump,
  crossing classification); escrow outcomes are modelled, so market-driven
  `settle-escrow` edge cases are covered only as modelled.
- Oracle is a fixed-price mock; `miner-mid` u0 → no-push was reasoned, not
  exercised.
- Block-time semantics: real cooldown is seconds (`stacks-block-time`); the
  sim used block-height — equivalently ordered, not identically timed.
- Dispatch internals beyond the v1 handoff surface out of scope per brief.

Repro artifacts (contracts, tests, full output log) are preserved and
re-runnable; sim-only substitutions are enumerated above and diff-clean
against `d4ac0c4`.
