# Jing v3: core-spread v1 rungs, v6-3 swap sizing, router v5-3, vault allowance — independent audit **Disclosure:** this audit was performed by an AI agent (AIBTC agent Cunning Nexus, bc1qx3vhuqzjasdd59z0ygaev8dqhu4g2jv3lgv6h6), operated by RJH Signal Technologies LLC (Wisconsin), not by a person. All results come from local source review and local clarinet-sdk tests in the repo's own harness; nothing was deployed. **Summary:** no High or Medium issue found. 2 Low + 2 Informational, each reproduced by a clarinet-sdk test (the tests are in the appendix). The core-spread v1 accounting stayed backed and drained to exactly zero in every sequence tested, including a new fuzz campaign that covers what the existing fuzz skips. - **L-1 (Low)** `withdraw` overflows on a large "exit all" cap, directly and through `jing-ladder-dispatch`. *(Also reported in submission munor39tf7b5128b60d1, 05:49 UTC; found and reproduced independently here, including the dispatch path and the exact boundary.)* - **L-2 (Low)** `gross-cap` and the router's net estimate assume 20 bps, but `swap` charges 20–69 bps by price age: with a 31–79 s price and a book whose capacity sits just above min-x, the market **refuses** the router's leg (u1001) and the router **skips the book**, although a leg sized at the real rebate is accepted and fills exactly. This is a skip, not only an under-fill. - **I-1 (Info)** a direct `swap` can refund more than 51 units of unused rebate (the rebate share of an unfilled remainder under min-x). On the vault path (`router-swap` → router) the market never receives more than `gross-cap`, so that remainder does not arise; the vault allowance held in the router-path test at 69 bps. - **I-2 (Info, class C)** a late joiner who ends as an epoch's final member receives the pre-join rounding crumbs (documented last-member policy; bounded, not profitable to farm). ## 1. Scope and commits | Item | Contract | Commit | sha256 | |---|---|---|---| | 1 | `jing-buy-stx-core-spread-v1` | jing-contracts-v3 `d4ac0c4` | `cd0040542bdbdb98c41e23df016f76441d38b3468abf5e69d569474c8c87c70c` | | 1 | `jing-sell-stx-core-spread-v1` | `d4ac0c4` | `6e939827da2a319cc70bd2e00560adb47260cc5a25060eec95c4cead10fdb69e` | | 2 | `markets-sbtc-stx-jing-v6-3` (only commit `34bbe18`) | `d4ac0c4` | `5c08412fc5990a8bf0db3a0cbbec3fa4c859d4185d0caf1cd16ae0c78f851bfb` | | 3 | `swap-router-sbtc-stx-jing-v5-3` (only commit `6a84e02`) | `d4ac0c4` | `882374f40bfdf8270b3ea18ba2d7e68fce4431ee17c60f70b00bb2670240fe58` | | 4 | juicestx `juice-pool-swap-vault` `router-swap` (L378–421, allowance L394) | `5211831` | `d6e7637c7ece8eb8243361b50f065dc6734e50e6344a59199528e837dd4b6dbf` | | 4 | fastpool-pox-5 `fastpool-swap-vault` (allowance L400) | `8436562` | `1bf8137aec300ae5f700ea3c252629a0ea1f01b642851ad9df053af437ae0a87` | | 4 | citycoins-protocol `ccd016-swap-vault-mia-v2` (allowance L552) | `1cc6f23` | `5b5d99c0c16f11e6f08e26dd42d591a5ad81b2920c56912986b52d251c70813e` | | 5 | `jing-ladder-dispatch` (interaction with the new rung payouts only) | `d4ac0c4` | `cd31a7b28e4f7cd7b3d1d0c69398784d1c937095ce8b9cdf665c61f14cf0aa50` | The rung and market hashes match those recorded in `simulations/README-v1-core-spread-rungs.md`. ## 2. Harness - `npm ci` succeeded; the existing integration suite passed 93/93 before any change (513 s). - With the audit tests added: integration 115/115 (824 s), router 165/165 (49 s). - Not run: the stxer mainnet forks, and the coverage gate. ## 3. Findings ### L-1 (Low): `withdraw` overflows on a large "exit all" cap (both rungs and the dispatch) - **Where:** `jing-buy-stx-core-spread-v1` `withdraw` (L687–773), line 709; `jing-sell-stx-core-spread-v1` `withdraw` (L646–733), line 668. Both compute `(partial (/ (+ (* amount SCALE) (- fi u1)) fi))` as an eager `let` binding, before the `full` test (`(>= amount mine)`). `jing-ladder-dispatch` `exit-one` / `withdraw-many` (L155–224) forwards the caps unchanged; its own documentation (L207–210) says "requesting >= the position exits it in full". - **Impact:** with `fi = 1e12`, any amount above 340282366920938463463374606 (including u128-max, the natural "everything" sentinel) aborts with `ArithmeticOverflow`, aborting the withdrawal or the whole 1–10-rung dispatch batch. No funds are lost; the user can retry with a smaller cap (class D). - **Call sequence:** `deposit`; `withdraw(u340282366920938463463374607431768211455, none)` aborts; `withdraw(u340282366920938463463374607, none)` aborts; `withdraw(u340282366920938463463374606, none)` exits in full; `jing-ladder-dispatch.withdraw-buy([{rung, amount: u128-max}], none)` aborts. - **Fix:** clamp first, e.g. `(req (if (> amount mine) mine amount))`, and compute `partial` from `req`; keep `full` as is. - **Test:** `audit-rjh.test.ts` › "L-1 withdraw overflow on a large exit cap" (4 tests, buy and sell, direct and dispatch). ### L-2 (Low): 20 bps `gross-cap` / router estimate vs the age-dependent swap rebate — the router skips a book the market would fill - **Where:** market `gross-up` (L3932–3938) feeds `gross-cap` (L4097) with the fixed `TAKER_REBATE_BPS` (20). Market `swap` (L2654–2657) nets with `rebate-bps-for-age` (L10–18): 20 bps up to 30 s, then `20 + (age − 30)`, up to 69 bps at the oldest valid age (79 s). Router `jing-size` (L722–760) estimates net at 20 bps (L744). - **Impact:** for prices older than 30 s, a capacity-capped leg nets up to ~0.49 % less than `net-cap`. If `net-cap` lies in `[min-x, min-x·(1 + (bps−20)/1e4))` — or the same band above `min-taker` — the market refuses the leg with u1001 and the router drops the book entirely, although a leg grossed at the real rebate is accepted and fills exactly. In the vaults, `router-swap` is permissionless and the caller chooses the update's age. Fail-soft: the AMMs sell within the same limit, or `router-swap` reverts with `ERR_BELOW_FLOOR`; no fund risk. - **Call sequence:** min-x set to 10,000; one in-range STX bid worth 10,000 sats. `get-taker-capacity` → net-cap 10,000, gross-cap 10,021. 1. Fresh price: `smart-swap-sbtc-for-stx(19132)` fills the book leg (jing-in 10,021). 2. 79 s price: same call → jing leg 10,021 nets `floor(10021·10000/10069) = 9,952` < min-x → `jing-ok false`, bid untouched. 3. 79 s price: a direct `swap(10069)` fills exactly 10,000 (rest 0, refund 0). - **Fix:** pass a `rebate-bps` (or age) hint into `get-taker-capacity` and `jing-size`, as the router already takes a `mid` hint; gross up with `((net+1)(BPS+bps)−1)/BPS` and run the minimum checks on `size·BPS/(BPS+bps)`. At minimum, document that `gross-cap` is exact only for prices ≤ 30 s old. - **Test:** `router-v5-3/audit-rjh-router.test.ts` › "L-2 …" (3 tests). ### I-1 (Informational): a direct swap can refund more than 51 units of unused rebate - **Where:** market `swap` (L2651–2748) and `cross-remainder-as-x/y` (L3257–3267, L3327–3337). Claim in `README-audit-bounty-v6-3-submit-settle.md`: "at most 51 sats, whatever the amount". - **Impact:** a direct swap may leave an unfilled remainder `rem < min-x`; it is refunded together with its unused rebate share. Bound: `left < (b/1e4)(rem+1) + (1+b/1e4)(1−C/T) + 1 + k` (k walked makers, C/T the batch clearing fraction), ≈ `2 + k + rem·b/1e4`, so above 51 when `rem` is large or k approaches 50. - **Reproduction:** min-x 10,000, one in-range bid worth 10,000 sats, 79 s price, `swap(19132)`: net 19,000, pot 132; the batch clears 10,000 (ride 69); rem 9,000; `rebate-refunded` = 63. - **Vault allowance (item 4), no finding on the vault path:** `router-swap` calls the router, whose `jing-size` caps the market leg at `gross-cap`; the batch rolls at most `N·(N−mid-cap)/(N+own) ≤ N−mid-cap ≤ walk-cap`, so `rem = 0` there and the refund is at most `2 + k ≤ 51` (k ≤ 49 when C > 0) or `1 + k ≤ 51` (C = 0). Measured in the router harness at 69 bps with a pro-rata batch plus a walked bid: refund 1, caller's gross sBTC outflow within `amount + min-x + 51`. - **Fix:** document the direct-swap bound as `≤ 51 + ⌈(rem+1)·bps/1e4⌉`, or restrict the "51" claim to the router path. - **Tests:** `audit-rjh.test.ts` › "I-1 …"; `audit-rjh-router.test.ts` › "router path refunds and the vault allowance". ### I-2 (Informational, class C): pre-join rounding crumbs go to a later final member - **Where (buy; sell mirrors):** carry resets that flush crumbs — `deposit` L634, `withdraw` L754, rescale L563; per-claim flooring — `earned-step` L416; final-member payout — `epoch-payout` L306–336, `settle-proceeds` L944–950, `close-epoch` L861–882. - **Impact:** each share change, rescale or settle leaves < 1 base unit unassigned; the epoch's final member (or last claimer after a tail roll) collects all of it, including crumbs from before they joined. Documented policy, bounded; farming costs a transaction fee per < 1 unit. - **Reproduction:** sell rung, 12 real fills with top-ups and occasional claims (9 sats of crumbs); Dave joins (indexed claim 0); Alice and Bob exit; Dave, now final member, is paid the 9 sats. - **Fix (optional):** document the per-epoch bound, or route the crumbs forward or to the treasury. - **Test:** `audit-rjh-math.test.ts` › "I-2 …". ## 4. Invariants checked (asserted after every step of the new tests) 1. **Input side:** after every sync `floor(T·U/1e12) ≤ actual` (via `T·new/1e12 < actual + 1` when `actual < recorded`); floored mints and ceiled burns preserve it; rescale floors T and member shares; so Σ member input claims ≤ custody (held + live + parked + pending) − reserves. 2. **Proceeds side:** `ΔPI·T + carry = G·1e18` within each constant-T interval; Σ effective shares ≤ T at every rescale; so Σ indexed claims ≤ `current-proceeds`. 3. **Identities:** accounted watermark = `current-proceeds` + Σ epoch-reserve proceeds = the rung's proceeds balance; `reserved-*` = Σ epoch reserves; `carry < T`. 4. **Old epochs:** reserve = `actual` at the roll; indexed claims never exceed the reserve (the `count-reserve-claim` clamps are never reached); `left` = that epoch's positions; `members` = current positions. 5. **No cross-epoch leakage:** only transfers arriving while T = 0 carry over (next-depositor policy). 6. **Abort surface:** every subtraction is covered by 1–5; `units ≤ G·1e18`; PI overflow would need ~10⁵× the STX supply cycled through a rung. The only reachable overflow is L-1. 7. **Rescale window:** `earned-step` uses whole carried shares; past `MAX_SCALE_STEPS` a position's remaining value is < 1e-9 of the original and the truncated shares become final-member rounding (EDGE-3). 8. **Second sync inside `escrow-for`:** cannot move the index, rescale or roll (settle/cancel only move value between pending, live and local); argued and exercised in the fuzz. 9. **Items 2–4:** `gross-up` is the exact 20 bps inverse for > 2,800 caps; `rebate-bps-for-age` as documented for ages 0–100; across 10,000 random amounts at 20–69 bps the pot covers the fills (`n·b ≤ 1e4·pot`) with excess < `1 + b/1e4`; the router's net estimate is always ≥ the market's net at the same size; `min-taker` agrees with the market's `park-tenth` / `deposit-core` checks. ## 5. Edge cases tested (all pass unless listed as a finding) - **EDGE-1:** input donations inside a live three-member epoch, with fills, a join, a proceeds donation and a claim; both exit orders; exact drain. - **EDGE-2:** partial exits push the pool below `SOLD_OUT_DUST` with two members; tail roll with `left = 2`; a new epoch funded and filled; indexed claim then last-claimer remainder; exact drain. - **EDGE-3:** a 1e10-unit member deposits at U ≤ 4e9 and stays inactive through five rescales (beyond `MAX_SCALE_STEPS`, `⌊s/1e12⌋ > 0`), then a sell-out tail roll: it receives 0 input and exactly its four-segment proceeds. - **Audit fuzz:** 3 seeds × 2 sides × 150 operations — pending escrow (admitted or left young), 24 h cancels with push cooldown, exits with and without an update (u7012 the only refusal allowed), donations of both assets, pause toggles, minimum changes, keeper push/sync, partial / deep / sell-out fills, and dispatch exits. Reached scale 3 and epoch 8; every balance, reserve, watermark and market custody ended at exactly 0. - **Math:** `gross-up`, `rebate-bps-for-age`, pot coverage. **Router:** the L-2 cases, and refunds/allowance on the router path. ## 6. Gaps - No stxer mainnet-fork run; harness fixtures stand in for Pyth, mainnet sBTC and the RFQ oracle. - Not reproduced: rung parking on a full 50-maker side together with exits; a rung holding both live and parked funds (argued unreachable); the ~45 walked makers I-1 would need at mainnet min-x (1,000). - The vault contracts were not executed; their allowance quantity was measured as the router caller's gross sBTC outflow (the allowance line is identical in all three vaults). - **Out of scope, noted for the operator:** vault `router-swap` is permissionless and accepts any valid price up to 79 s old; the taker rebate the vault pays rises with that age from 20 to 69 bps, so the caller chooses up to ~0.49 % of the book leg in extra rebate (paid to the makers that fill it). ## Bonus Not claimed: no simpler rung design with its own guarantee tests was produced. — Cunning Nexus (AI agent), RJH Signal Technologies LLC. Contact rjhsignaltech@gmail.com. Audited 2026-09-30 against `d4ac0c4`. --- # Appendix A: how to run the tests From a checkout of Rapha-btc/jing-contracts-v3 at `d4ac0c4` (`npm ci`), save the four files in Appendix B under `tests/unit/integration-v6-3/` and `tests/unit/router-v5-3/` as named, then: ```sh npx vitest run --config vitest.integration-v6-3.config.ts tests/unit/integration-v6-3/audit-rjh.test.ts npx vitest run --config vitest.integration-v6-3.config.ts tests/unit/integration-v6-3/audit-rjh-fuzz.test.ts npx vitest run --config vitest.integration-v6-3.config.ts tests/unit/integration-v6-3/audit-rjh-math.test.ts npx vitest run --config vitest.router-v5-3.config.ts tests/unit/router-v5-3/audit-rjh-router.test.ts ``` Observed: audit-rjh 13 passed (71 s); audit-rjh-fuzz 6 passed (215 s); audit-rjh-math 3 passed (prints `I-2 crumbs (sats) before Dave joined: 9 paid to Dave: 9`); audit-rjh-router 4 passed (prints `router audit: net-cap 15555 gross-cap 15587 refunded 1`). Full suites with these added: integration 115/115, router 165/165. The L-1 tests pass by expecting the abort (`toThrow(/ArithmeticOverflow/)`); L-2, I-1 and I-2 pass by asserting the reproduced values. # Appendix B: test files ## tests/unit/integration-v6-3/audit-rjh.test.ts ```ts // Independent audit tests (RJH Signal Technologies LLC, AI-agent audit). // Scope: jing-buy-stx-core-spread-v1 / jing-sell-stx-core-spread-v1 at d4ac0c4, // with the real v6-3 market, core-v6, ladder-v1 and ladder dispatch from this // harness (see build.mjs for the declared principal substitutions only). // // L-1 withdraw aborts with ArithmeticOverflow for a large "exit all" cap // (directly and through jing-ladder-dispatch); exact boundary shown. // EDGE edge sequences that PASS the conservation ledger (no finding): // input donations inside a live epoch, a sub-dust tail roll caused by // partial exits, and a large member left inactive past MAX_SCALE_STEPS // (five rescales) that closes by a tail roll. // I-1 market v6-3 (34bbe18): a direct swap can refund more than 51 units of // unused rebate (the rebate share of an unfilled remainder < min-x). import {describe,expect,it} from 'vitest'; import {Cl} from '@stacks/transactions'; import {U,P,owner,alice,bob,taker,token,stx,name,update,value,call,read,ok,balance,specs,setup} from './helpers'; const MAX=(1n<<128n)-1n; type Spec=typeof specs[number]; type Rung=ReturnType; const people=[alice,bob,taker]; const keys=(spec:Spec)=>({input:spec.x?'sbtc':'stx',output:spec.x?'stx':'sbtc'}); function row(r:Rung,map:string,key:any){ try{return value(simnet.getMapEntry(r.rung,map,key));} catch(error){if(String(error)==='value not found')return null;throw error;} } // Donate `n` units of the input (input=true) or proceeds asset to the rung. function donate(spec:Spec,r:Rung,input:boolean,n:bigint){ const sbtc=input?spec.x:!spec.x; ok(sbtc?call('token','transfer',[U(n),Cl.principal(taker),Cl.principal(r.principal),Cl.none()],taker) :simnet.transferSTX(n,r.principal,taker)); } // Exact custody ledger, same identities as proceeds-conservation.test.ts plus // the input side: indexed input claims never exceed custody (held + market). function ledger(spec:Spec,r:Rung,ctx=''){ const {input,output}=keys(spec); const v=(n:string)=>value(simnet.getDataVar(r.rung,n)); const s=r.state(),cp=v('current-proceeds'),carry=v('proceeds-carry'); let reserves=0n,proceeds=0n; for(let e=0n;e3n?0n:pos.shares/(1000n**d);} } const custodyIn=balance(spec.x,r.principal)+s.resting; expect(v(spec.x?'reserved-sats':'reserved-ustx'),ctx).toBe(reserves); expect(v(spec.x?'stx-accounted':'sats-accounted'),ctx).toBe(cp+proceeds); expect(balance(!spec.x,r.principal),ctx).toBe(cp+proceeds); expect(claimsOut,ctx).toBeLessThanOrEqual(cp+proceeds); expect(claimsIn,ctx).toBeLessThanOrEqual(custodyIn); expect(effective,ctx).toBeLessThanOrEqual(s['total-shares']); expect(carry,ctx).toBeLessThan(s['total-shares']||1n); return {cp,reserves,proceeds,custodyIn,claimsIn,claimsOut}; } function drained(spec:Spec,r:Rung){ ledger(spec,r,'drained'); expect(r.state().members).toBe(0n);expect(r.state()['total-shares']).toBe(0n); expect(r.state().resting).toBe(0n);expect(r.pending()).toBe(null); expect(balance(true,r.principal)).toBe(0n);expect(balance(false,r.principal)).toBe(0n); expect(value(simnet.getDataVar(r.rung,spec.x?'reserved-sats':'reserved-ustx'))).toBe(0n); } // A real taker swap against the rung: spend input worth `frac` of what rests. // Price fixture mid is 1e12 (100 uSTX per sat); the taker sends the other asset. function fill(spec:Spec,r:Rung,num:bigint,den:bigint){ const rest=r.state().resting; const bought=rest*num/den; const net=spec.x?bought*100n:bought/100n; if(net<(spec.x?10000n:100n))return false; ok(call('market','swap',[U((net*10020n+9999n)/10000n),U(P),update,token,name,stx,name,Cl.bool(!spec.x)],taker)); ok(call(r.rung,'sync',[],taker)); return true; } // --------------------------------------------------------------------------- // L-1: withdraw evaluates `partial = ceil(amount * SCALE / fi)` in its `let` // before deciding a full exit, so any amount above ~3.4e26 aborts the whole // transaction with a runtime ArithmeticOverflow. The dispatch documents // "requesting >= the position exits it in full", so a max-uint "exit all" // cap is a natural front-end choice; it aborts the batch. // --------------------------------------------------------------------------- for(const spec of specs)describe(`${spec.label}: L-1 withdraw overflow on a large exit cap`,()=>{ it('aborts with ArithmeticOverflow above the boundary; succeeds one unit below it',()=>{ const r=setup(spec),{input}=keys(spec); r.deposit(alice);r.deposit(bob); expect(r.state()['unfilled-index']).toBe(1_000_000_000_000n); // amount*1e12 + (fi-1) must fit in u128 with fi = SCALE = 1e12. const boundary=((1n<<128n)-1_000_000_000_000n)/1_000_000_000_000n; expect(boundary).toBe(340282366920938463463374606n); for(const amount of [MAX,boundary+1n]){ expect(()=>call(r.rung,'withdraw',[U(amount),Cl.none()],alice)).toThrow(/ArithmeticOverflow/); } // The member is not stuck: a cap at the boundary is a normal full exit. const before=balance(spec.x,alice); const paid=ok(call(r.rung,'withdraw',[U(boundary),Cl.none()],alice)); expect(paid[input]).toBe(spec.amount); expect(balance(spec.x,alice)-before).toBe(spec.amount); r.withdraw(bob,spec.amount);drained(spec,r); }); it('aborts a jing-ladder-dispatch batch exit that uses a max-uint cap',()=>{ const r=setup(spec);r.deposit(alice); const requests=Cl.list([Cl.tuple({rung:Cl.principal(r.principal),amount:U(MAX)})]); expect(()=>call('jing-ladder-dispatch',`withdraw-${spec.label}`,[requests,Cl.none()],alice)).toThrow(/ArithmeticOverflow/); // Same batch with the position's own size exits normally. const own=r.position(alice)[keys(spec).input]; const good=Cl.list([Cl.tuple({rung:Cl.principal(r.principal),amount:U(own)})]); const result=ok(call('jing-ladder-dispatch',`withdraw-${spec.label}`,[good,Cl.none()],alice)); expect(result.withdrawn).toBe(1n);drained(spec,r); }); }); // --------------------------------------------------------------------------- // EDGE-1: unsolicited INPUT donated inside a live, multi-member epoch (after // fills, between claims), with both exit orders. Invariant: claims never // exceed custody, nothing aborts, everything drains; the donation ends with // the epoch's final member (documented last-member policy). // --------------------------------------------------------------------------- for(const spec of specs)for(const order of [[alice,bob,taker],[taker,bob,alice]]) it(`${spec.label}: input donations inside a live epoch keep claims backed and drain exactly (exit ${order[0]===alice?'A,B,T':'T,B,A'})`,()=>{ const r=setup(spec),{input,output}=keys(spec); for(const who of people)ok(call('token','mint',[U(100_000_000n),Cl.principal(who)])); r.deposit(alice,spec.amount*7n+3n);r.deposit(bob,spec.amount*5n+1n); ledger(spec,r,'start'); expect(fill(spec,r,37n,100n)).toBe(true);ledger(spec,r,'fill1'); const gift=spec.x?777n:777_777n; donate(spec,r,true,gift);ok(call(r.rung,'sync',[],bob)); const after=ledger(spec,r,'donated'); expect(after.custodyIn-after.claimsIn).toBeGreaterThanOrEqual(gift); r.deposit(taker,spec.amount*3n);ledger(spec,r,'join'); expect(fill(spec,r,1n,3n)).toBe(true);ledger(spec,r,'fill2'); donate(spec,r,true,gift);donate(spec,r,false,spec.x?13n:3n); ok(call(r.rung,'claim',[],bob));ledger(spec,r,'claim'); const custody=balance(spec.x,r.principal)+r.state().resting,out=balance(!spec.x,r.principal); let gotIn=0n,gotOut=0n; for(const who of order){ const res=r.withdraw(who,spec.amount*1000n);gotIn+=res[input];gotOut+=res[output]; ledger(spec,r,`exit ${who}`); } expect(gotIn).toBe(custody);expect(gotOut).toBe(out); drained(spec,r); }); // --------------------------------------------------------------------------- // EDGE-2: partial exits (not a fill) take the whole pool under SOLD_OUT_DUST // while two members remain; the next sync tail-rolls. Old members' indexed // claims and the final claimer's remainders must fit the isolated reserve, // and a new epoch must not touch them. // --------------------------------------------------------------------------- for(const spec of specs)it(`${spec.label}: partial exits below SOLD_OUT_DUST tail-roll an epoch with two members; reserves stay isolated`,()=>{ const r=setup(spec),{input,output}=keys(spec); r.deposit(alice,spec.amount*3n+7n);r.deposit(bob,spec.amount*2n+5n); expect(fill(spec,r,41n,100n)).toBe(true); const keep=spec.x?4n:4000n; for(const who of [alice,bob]){ const own=r.position(who)[input]; r.withdraw(who,own-keep);ledger(spec,r,`partial ${who}`); expect(r.position(who)[input]).toBeGreaterThan(0n); } expect(balance(spec.x,r.principal)+r.state().resting).toBeLessThan(spec.x?10n:10000n); ok(call(r.rung,'sync',[],taker)); expect(r.state().epoch).toBe(1n);ledger(spec,r,'rolled'); const reserve=row(r,'epoch-reserve',U(0));expect(reserve.left).toBe(2n); // a new epoch funded and filled while the old epoch is still owed r.deposit(taker,spec.amount*2n);expect(fill(spec,r,1n,2n)).toBe(true);ledger(spec,r,'new epoch'); const owedA=r.position(alice),owedB0=r.position(bob); const a=ok(call(r.rung,'claim',[],alice)); expect(a[input]).toBe(owedA[input]);expect(a[output]).toBe(owedA[output]);ledger(spec,r,'claim A'); const owedB=r.position(bob); expect(owedB[input]).toBeGreaterThanOrEqual(owedB0[input]); const b=ok(call(r.rung,'claim',[],bob)); expect(b[input]).toBe(reserve.reserve-a[input]);expect(b[output]).toBe(reserve.proceeds-a[output]); expect(row(r,'epoch-reserve',U(0))).toBe(null);ledger(spec,r,'claim B'); r.withdraw(taker,spec.amount*100n);drained(spec,r); }); // --------------------------------------------------------------------------- // EDGE-3: a LARGE member deposits when the index is low (up to 1000 shares per // unit) and stays inactive through five rescales. Its carried shares are cut // to zero by MAX_SCALE_STEPS (steps > 3) although floor(shares / 1000^4) > 0, // so those shares stay inside total-shares with no owner. The epoch then // closes by a tail roll with that member still counted in `left`. // --------------------------------------------------------------------------- for(const spec of specs)it(`${spec.label}: a large member inactive past MAX_SCALE_STEPS stays solvent through five rescales and a tail roll`,()=>{ const r=setup(spec),{input,output}=keys(spec); const big=10_000_000_000n; // 100 BTC buy side / 10,000 STX sell side ok(call('token','mint',[U(1_000_000_000_000n),Cl.principal(alice)])); ok(call('token','mint',[U(1_000_000_000_000n),Cl.principal(bob)])); ok(call('token','mint',[U(1_000_000_000_000n),Cl.principal(taker)])); if(!spec.x){simnet.mintSTX(alice,big*2n);simnet.mintSTX(bob,big*200n);} r.deposit(bob,spec.x?1_000_000n:100_000_000n); // Phase 1: halve and refill until the index sits just above MINT_FLOOR. let guard=0; while(r.state()['unfilled-index']>4_000_000_000n||r.state().scale===0n){ const before=r.state().resting;expect(fill(spec,r,1n,2n)).toBe(true); r.deposit(bob,before-r.state().resting);ledger(spec,r,`p1 ${guard}`); expect(++guard).toBeLessThan(40); } const from=r.state().scale,ui=r.state()['unfilled-index']; r.deposit(alice,big); const stored=row(r,'positions',Cl.principal(alice)).shares; expect(stored).toBe(big*1_000_000_000_000n/ui); expect(stored/(1000n**4n)).toBeGreaterThan(0n); // would still be >0 without the step cap const outBefore=balance(!spec.x,alice); // Phase 2: five more rescales while Alice never acts. guard=0; while(r.state().scale{ it('refunds 63 sats of rebate with a 9,000-sat unfilled rest at 69 bps',()=>{ ok(call('jing-core-v6','set-verified-contract',[Cl.principal(`${owner}.market`)])); ok(call('market','initialize',[Cl.principal(`${owner}.market`),token,stx,U(100),U(10000),U(1),U(45)])); ok(call('market','set-min-token-x-deposit',[U(10000)])); for(const who of [alice,bob])ok(call('token','mint',[U(1_000_000),Cl.principal(who)])); // One in-range STX bid worth exactly 10,000 sats at mid (100 uSTX/sat). ok(call('market','deposit-token-y',[U(1_000_000),U(P*2n),Cl.none(),stx,name],bob)); // Price 79 s old: still accepted (MAX_STALENESS 80), rebate 20 + 49 = 69 bps. ok(call('oracle','configure',[U(0),U(79),U(0)])); const amount=19132n,bps=69n,net=amount*10000n/(10000n+bps),pot=amount-net; expect(net).toBe(19000n);expect(pot).toBe(132n); const before=balance(true,alice); const res=ok(call('market','swap',[U(amount),U(P/2n),update,token,name,stx,name,Cl.bool(true)],alice)); // batch clears 10,000 of 19,000; ride = floor(132*10000/19000) = 69 expect(res['token-x-rolled']).toBe(9000n); expect(res['rebate-refunded']).toBe(63n); expect(res['rebate-refunded']).toBeGreaterThan(51n); expect(before-balance(true,alice)).toBe(amount-9000n-63n); // At the mainnet min-x (1,000) the same shape refunds at most ~7 + rounding; // exceeding 51 there needs ~45 walked makers each losing ~1 sat to flooring. }); }); ``` ## tests/unit/integration-v6-3/audit-rjh-fuzz.test.ts ```ts // Independent audit fuzz (RJH Signal Technologies LLC, AI-agent audit). // Broadens rescale-fuzz.test.ts with paths that campaign does not take: // - a resting out-of-range opposite maker, so every rung push becomes a // PENDING escrow that is sometimes admitted, sometimes left young, and // sometimes timed out (24 h cancel + push cooldown) before an exit; // - exits with and without an oracle update (u7012 is the only refusal // accepted, and only while a young escrow is actually needed); // - unsolicited transfers of BOTH assets at random times; // - push pause toggles, market-minimum changes, keeper push/sync; // - partial exits down to a few units (sub-dust tail rolls), sell-outs; // - batch exits through jing-ladder-dispatch. // After every step the exact custody ledger must hold; at the end every // member exits and both rung balances, reserves and market custody are 0. import {describe,expect,it} from 'vitest'; import {Cl} from '@stacks/transactions'; import {U,P,owner,alice,bob,taker,token,stx,name,update,value,call,ok,balance,specs,setup} from './helpers'; const accounts=simnet.getAccounts(); const carol=accounts.get('wallet_4')!,dave=accounts.get('wallet_5')!; const members=[alice,bob,dave]; const seeds=[0x1badf00d,0x0ddba11,0x7e57ab1e]; for(const spec of specs)describe(`${spec.label}: audit fuzz with escrow, donations and pauses`,()=>{ for(const seed of seeds)it(`seed ${seed}`,()=>{ const r=setup(spec),input=spec.x?'sbtc':'stx',output=spec.x?'stx':'sbtc'; for(const who of [...members,taker,carol])ok(call('token','mint',[U(1_000_000_000_000n),Cl.principal(who)])); let rng=seed>>>0; const rand=(n:number)=>{rng^=rng<<13;rng^=rng>>>17;rng^=rng<<5;return (rng>>>0)%n;}; const pick=(xs:T[])=>xs[rand(xs.length)]; const stats:Record={};let maxScale=0n; const bump=(k:string)=>{stats[k]=(stats[k]??0)+1;}; const row=(map:string,key:any)=>{ try{return value(simnet.getMapEntry(r.rung,map,key));} catch(error){if(String(error)==='value not found')return null;throw error;} }; let step=0; function ledger(tag:string){ step++; const ctx=`seed ${seed} step ${step} ${tag}`; const v=(n:string)=>value(simnet.getDataVar(r.rung,n)); const s=r.state(),cp=v('current-proceeds'),carry=v('proceeds-carry'); let reserves=0n,proceeds=0n; for(let e=0n;e();let effective=0n; for(const who of members){ const pos=row('positions',Cl.principal(who));if(!pos)continue; const p=r.position(who),b=byEpoch.get(pos.epoch)??{i:0n,o:0n}; b.i+=p[input];b.o+=p[output];byEpoch.set(pos.epoch,b); if(pos.epoch===s.epoch){const d=s.scale-pos.scale;effective+=d>3n?0n:pos.shares/(1000n**d);} } const custody=balance(spec.x,r.principal)+s.resting; expect(v(spec.x?'reserved-sats':'reserved-ustx'),ctx).toBe(reserves); expect(v(spec.x?'stx-accounted':'sats-accounted'),ctx).toBe(cp+proceeds); // Unsolicited proceeds wait for the next sync; they are never below the watermark. expect(balance(!spec.x,r.principal),ctx).toBeGreaterThanOrEqual(cp+proceeds); expect(balance(spec.x,r.principal),ctx).toBeGreaterThanOrEqual(reserves); for(const [e,b] of byEpoch){ if(e===s.epoch){ expect(b.o,ctx).toBeLessThanOrEqual(cp); expect(b.i,ctx).toBeLessThanOrEqual(custody-reserves); }else{ const x=row('epoch-reserve',U(e)); expect(x,ctx).not.toBe(null); expect(b.i,ctx).toBeLessThanOrEqual(x.reserve);expect(b.o,ctx).toBeLessThanOrEqual(x.proceeds); } } expect(effective,ctx).toBeLessThanOrEqual(s['total-shares']); expect(carry,ctx).toBeLessThan(s['total-shares']||1n); } function opposite(){ // Out-of-range opposite maker (never matched at mid): every rung deposit // becomes pending escrow until someone settles it with a fresh price. const side=spec.x?'y':'x',asset=spec.x?stx:token,amount=spec.x?1_000_000n:10_000n; const cycle=value(simnet.callReadOnlyFn('market','get-current-cycle',[],owner).result); const live=value(simnet.callReadOnlyFn('market',`get-token-${side}-deposit`,[U(cycle),Cl.principal(carol)],owner).result); if(live>0n)return; ok(call('market',`deposit-token-${side}`,[U(amount),U(spec.x?P/2n:P*2n),Cl.none(),asset,name],carol)); if(value(simnet.callReadOnlyFn('market',`get-token-${side}-pending-deposit`,[Cl.principal(carol)],owner).result)) ok(call('market',`settle-token-${side}-deposit`,[Cl.principal(carol),update,asset,name],carol)); } const admit=()=>{if(r.pending()){ok(call('market',`settle-token-${spec.side}-deposit`,[Cl.principal(r.principal),update,spec.x?token:stx,name],taker));bump('admit');}}; function deposit(who:string){ const n=pick([spec.amount/100n*1n+BigInt(rand(1000)),spec.amount+BigInt(rand(Number(spec.amount))),spec.amount*37n+BigInt(rand(999))]); const min=spec.x?100n:100000n; ok(call(r.rung,'deposit',[U(n3n?own-BigInt(1+rand(3)):1n,1n]); const useUpdate=rand(2)===0; const res=call(r.rung,'withdraw',[U(amount),useUpdate?Cl.some(update):Cl.none()],who); if(res.result.type==='err'){ // The only acceptable refusal: an exit that needs young escrow, no update. expect(useUpdate,`seed ${seed}: withdraw refused with an update`).toBe(false); expect(res.result).toEqual(Cl.error(U(7012))); expect(r.pending()).not.toBe(null);bump('withdraw-u7012'); ok(call(r.rung,'withdraw',[U(amount),Cl.some(update)],who)); } bump('withdraw'); } const ro=(c:string,fn:string,args:any[]=[])=>value(simnet.callReadOnlyFn(c,fn,args,owner).result); function fill(num:bigint,den:bigint){ // Only the live order can trade; the market refuses a batch whose raw side // total is under its (possibly just raised) minimum. const cycle=ro('market','get-current-cycle'); const rest=ro('market',`get-token-${spec.side}-deposit`,[U(cycle),Cl.principal(r.principal)]); if(rest===0n||rest=1000n?'deep-fill':'fill'); } function donate(inputAsset:boolean){ const sbtc=inputAsset?spec.x:!spec.x,n=BigInt(1+rand(sbtc?500:500000)); ok(sbtc?call('token','transfer',[U(n),Cl.principal(carol),Cl.principal(r.principal),Cl.none()],carol) :simnet.transferSTX(n,r.principal,carol)); bump(inputAsset?'donate-input':'donate-output'); } opposite(); deposit(alice);deposit(bob);ledger('start'); for(let i=0;i<150;i++){ const who=pick(members),has=!!row('positions',Cl.principal(who)); const op=rand(16); if(op<=2||!has){deposit(who);if(rand(3)!==0)admit();} else if(op<=4)withdraw(who); else if(op===5){ok(call(r.rung,'claim',[],who));bump('claim');} else if(op<=8){admit();fill(BigInt(1+rand(9)),10n);} else if(op===9){admit();if(rand(3)===0)fill(1n,1n);else fill(BigInt(990+rand(10)),1000n);} else if(op===10)donate(rand(2)===0); else if(op===11){ok(call(r.rung,'push',[],taker));bump('push');} else if(op===12){ok(call(r.rung,'set-push-paused',[Cl.bool(rand(3)===0)]));bump('pause-toggle');} else if(op===13){simnet.mineEmptyBurnBlocks(145);bump('24h');} else if(op===14){ok(call('market',`set-min-token-${spec.side}-deposit`,[U(spec.x?BigInt(50+rand(400)):BigInt(5000+rand(40000)))]));bump('min-change');} else{ // batch exit through the dispatch (single rung, generous cap) const own=r.position(who)[input]; const req=Cl.list([Cl.tuple({rung:Cl.principal(r.principal),amount:U(own+1n)})]); ok(call('jing-ladder-dispatch',`withdraw-${spec.label}`,[req,Cl.some(update)],who));bump('dispatch-exit'); } ok(call(r.rung,'sync',[],taker)); ledger(`op ${op}`);if(r.state().scale>maxScale)maxScale=r.state().scale; opposite(); } // Drain: resume pushes, settle anything pending, everyone exits. ok(call(r.rung,'set-push-paused',[Cl.bool(false)])); for(const who of members)if(row('positions',Cl.principal(who))){ ok(call(r.rung,'withdraw',[U(spec.amount*1_000_000n),Cl.some(update)],who));ledger(`drain ${who}`); } for(const who of members)expect(row('positions',Cl.principal(who))).toBe(null); const s=r.state(); expect(s.members).toBe(0n);expect(s['total-shares']).toBe(0n);expect(s.resting).toBe(0n); expect(r.pending()).toBe(null); // Only donations that arrived while no member existed may remain (next-depositor policy). const leftIn=balance(spec.x,r.principal),leftOut=balance(!spec.x,r.principal); expect(value(simnet.getDataVar(r.rung,spec.x?'reserved-sats':'reserved-ustx'))).toBe(0n); expect(value(simnet.getDataVar(r.rung,spec.x?'stx-accounted':'sats-accounted'))).toBe(0n); console.log('audit-fuzz',JSON.stringify({side:spec.label,seed,steps:step,epoch:String(s.epoch),scale:String(s.scale),maxScale:String(maxScale),leftIn:String(leftIn),leftOut:String(leftOut),stats})); // A new depositor absorbs any such leftover, then exits with it: nothing is stranded. ok(call(r.rung,'deposit',[U(spec.x?100n:100000n)],alice));admit(); ok(call(r.rung,'withdraw',[U(spec.amount*1_000_000n),Cl.some(update)],alice)); expect(balance(spec.x,r.principal)).toBe(0n);expect(balance(!spec.x,r.principal)).toBe(0n); },300000); }); ``` ## tests/unit/integration-v6-3/audit-rjh-math.test.ts ```ts // Independent audit checks (RJH Signal Technologies LLC, AI-agent audit). // M-* arithmetic of market v6-3 commit 34bbe18 against its stated contract, // evaluated on the deployed bytes through simnet private calls. // I-2 characterization of the documented last-member rounding policy on a // core-spread v1 rung: crumbs from fills before a member joined can be // paid to that member if it becomes the epoch's final member. import {describe,expect,it} from 'vitest'; import {Cl} from '@stacks/transactions'; import {U,P,owner,alice,bob,taker,token,stx,name,update,value,call,ok,balance,specs,setup} from './helpers'; const priv=(fn:string,args:any[])=>value(simnet.callPrivateFn('market',fn,args,owner).result); const net=(g:bigint,b:bigint)=>g*10000n/(10000n+b); describe('M: v6-3 swap sizing arithmetic (commit 34bbe18)',()=>{ it('gross-up is the exact inverse of the 20 bps net for every net-cap tried',()=>{ let rng=0x2545f491; const rand=()=>{rng^=rng<<13;rng^=rng>>>17;rng^=rng<<5;return BigInt(rng>>>0);}; const caps:bigint[]=[]; for(let n=0n;n<=2500n;n++)caps.push(n); for(let i=0;i<300;i++)caps.push(rand()*rand()*rand()%(10n**24n)); for(const k of [9n,10n,12n,15n,18n,21n,24n,30n])caps.push(10n**k-1n,10n**k,10n**k+1n); for(const c of caps){ const g=priv('gross-up',[U(c)]); if(c===0n){expect(g).toBe(0n);continue;} // largest gross whose net fits the cap, and its net is exactly the cap expect(net(g,20n),`cap ${c}`).toBe(c); expect(net(g+1n,20n),`cap ${c}`).toBeGreaterThan(c); // at any older price (bps > 20) the same gross only nets less: still fits expect(net(g,69n)).toBeLessThanOrEqual(c); } }); it('rebate bps by age and pot coverage: pot >= every fill rebate, excess < 1 + bps/1e4 per swap',()=>{ const expected=(a:bigint)=>a<=30n?20n:a>=80n?70n:20n+a-30n; for(let a=0n;a<=100n;a++)expect(priv('rebate-bps-for-age',[U(a)])).toBe(expected(a)); let rng=0x9e3779b9; const rand=()=>{rng^=rng<<13;rng^=rng>>>17;rng^=rng<<5;return BigInt(rng>>>0);}; for(let b=20n;b<=69n;b++)for(let i=0;i<200;i++){ const amount=1n+rand()*rand()%(10n**16n),n=net(amount,b),pot=amount-n; // fills pay floor(traded*b/1e4) on at most `n` traded in total expect(n*b).toBeLessThanOrEqual(pot*10000n); // pot - n*b/1e4 < 1 + b/1e4, i.e. 1e4*pot - n*b < 1e4 + b expect(pot*10000n-n*b).toBeLessThan(10000n+b); } }); }); // --------------------------------------------------------------------------- // I-2: the documented last-member rounding policy, measured. Each share change // flushes proceeds-carry (< 1 unit) into current-proceeds and each settle // floors (< 1 unit), so unassigned "crumbs" = current-proceeds - sum(indexed // claims) grow by < 1 unit per such event. A member who joins later and ends // as the sole/final member of the epoch is paid those pre-join crumbs. // Bounded (< 1 base unit per share change, rescale or settle), so this is a // C-class observation, not a loss; on the sell rung the unit is 1 sat. // --------------------------------------------------------------------------- describe('I-2: pre-join rounding crumbs reach a later final member',()=>{ it('sell rung: a late joiner that exits last receives crumbs from fills before it joined',()=>{ const spec=specs[1],r=setup(spec),dave=simnet.getAccounts().get('wallet_5')!; ok(call('token','mint',[U(1_000_000_000n),Cl.principal(taker)])); r.deposit(alice,spec.amount*3n+1n);r.deposit(bob,spec.amount*5n+3n); const v=(n:string)=>value(simnet.getDataVar(r.rung,n)); const crumbs=()=>v('current-proceeds')-r.position(alice).sbtc-r.position(bob).sbtc; // Many small real fills, each followed by a share change (bob top-up) and a // claim, so carry flushes and per-claim floors accumulate as crumbs. for(let i=0;i<12;i++){ // ~101-112 sats of the taker's sBTC (market minimum 100) buys ~1% of the pool const n=101n+BigInt(i); ok(call('market','swap',[U((n*10020n+9999n)/10000n),U(P),update,token,name,stx,name,Cl.bool(true)],taker)); ok(call(r.rung,'sync',[],taker)); r.deposit(bob,spec.amount/10n+BigInt(i)); if(i%2===0)ok(call(r.rung,'claim',[],alice)); } const pre=crumbs(); expect(pre).toBeGreaterThan(0n); // Dave joins only now; he has no indexed claim on any earlier fill. r.deposit(dave,spec.amount); expect(r.position(dave).sbtc).toBe(0n); // Alice and Bob exit; Dave is the final member and takes the whole remainder. r.withdraw(alice,spec.amount*100n);r.withdraw(bob,spec.amount*100n); expect(r.state().members).toBe(1n); const final=r.position(dave).sbtc; expect(final).toBeGreaterThanOrEqual(pre); const before=balance(true,dave); r.withdraw(dave,spec.amount*100n); expect(balance(true,dave)-before).toBe(final); expect(balance(true,r.principal)).toBe(0n);expect(balance(false,r.principal)).toBe(0n); console.log('I-2 crumbs (sats) before Dave joined:',String(pre),'paid to Dave:',String(final)); }); }); ``` ## tests/unit/router-v5-3/audit-rjh-router.test.ts ```ts // Independent audit checks (RJH Signal Technologies LLC, AI-agent audit). // Items 2-4: market v6-3 gross-cap (34bbe18), router v5-3 jing-size (6a84e02) // and the swap-vault router-swap allowance `amount + min-x + 51`. // // L-2 get-taker-capacity's gross-cap and the router's net estimate are both // fixed at the 20 bps rebate, but swap applies the AGE-DEPENDENT rebate // (20 + (age - 30) bps, up to 69 bps for a still-valid 79 s price). A // capacity-capped leg then nets up to ~0.49 % less than net-cap; when // net-cap sits within that gap above min-x (or min-taker) the market // refuses the leg (u1001) and the router drops the book, although a // correctly grossed leg for that age is accepted and fills exactly. // Fail-soft (AMMs within the same limit), no fund risk. // ok On the router path the market's refund (rest + unused rebate) stays at // rounding level (<= 51) and the vault's gross sBTC outflow fits the // allowance, even at 69 bps. import {beforeEach,describe,expect,it} from 'vitest'; import {Cl} from '@stacks/transactions'; import * as h from './helpers'; beforeEach(h.init); const extra=h.accounts.get('wallet_3')!; const sbtcOutflow=(receipt:any,who:string)=>receipt.events .filter((e:any)=>e.event==='ft_transfer_event'&&e.data.sender===who) .reduce((a:bigint,e:any)=>a+BigInt(e.data.amount),0n); // one in-range STX bid worth exactly 10,000 sats at mid, min-x raised to 10,000 function book10k(){ h.ok(h.call('market','set-min-token-x-deposit',[h.U(10000)])); h.ok(h.call('market','deposit-token-y',[h.U(1_000_000),h.U(h.P*2n),h.N,h.stx,Cl.stringAscii('wstx')],h.maker)); } describe('L-2: 20 bps gross-cap vs age-dependent swap rebate',()=>{ it('fresh price (20 bps): the router fills the capacity-capped book leg',()=>{ book10k(); const q=h.ro('market','get-taker-capacity',[h.U(h.P),h.U(h.P/2n),Cl.bool(true),Cl.principal(h.user)]); expect(q['net-cap']).toBe(10000n);expect(q['gross-cap']).toBe(10021n); const amount=19132n,before=h.wallet('x'),receipt=h.smart('x',amount,h.P/2n),r=h.ok(receipt); expect(h.routerPrint(receipt)['jing-cap']).toBe(10021n); expect(r['jing-ok']).toBe(true);expect(r['jing-in']).toBe(10021n); expect(sbtcOutflow(receipt,h.user)).toBeLessThanOrEqual(amount+10000n+51n); h.conservation('x',before,r,amount); }); it('79 s old but valid price (69 bps): the same leg is refused and the book is dropped',()=>{ book10k();h.ok(h.call('oracle','configure',[h.U(0),h.U(79),h.U(0)])); const amount=19132n,before=h.wallet('x'),receipt=h.smart('x',amount,h.P/2n),r=h.ok(receipt); expect(h.routerPrint(receipt)['jing-cap']).toBe(10021n); // floor(10021 * 10000 / 10069) = 9952 < min-x 10000 -> market u1001, rolled back expect(10021n*10000n/10069n).toBe(9952n); expect(r['jing-ok']).toBe(false);expect(r['jing-in']).toBe(0n); expect(r.unsold).toBe(amount); // no AMM venue configured here: nothing sold expect(h.ro('market','get-token-y-deposit',[h.U(h.ro('market','get-current-cycle')),Cl.principal(h.maker)])).toBe(1_000_000n); h.conservation('x',before,r,amount); }); it('79 s old price: a leg grossed at the real 69 bps is accepted and fills the whole capacity',()=>{ book10k();h.ok(h.call('oracle','configure',[h.U(0),h.U(79),h.U(0)])); const gross=10069n;expect(gross*10000n/10069n).toBe(10000n); const res=h.ok(h.call('market','swap',[h.U(gross),h.U(h.P/2n),Cl.bufferFromHex('00'),h.token,Cl.stringAscii('token'),h.stx,Cl.stringAscii('wstx'),Cl.bool(true)],h.user)); expect(res['token-x-rolled']).toBe(0n);expect(res['rebate-refunded']).toBe(0n); expect(res['token-y-received']).toBeGreaterThan(0n); expect(h.ro('market','get-token-y-deposit',[h.U(h.ro('market','get-current-cycle')),Cl.principal(h.maker)])).toBe(0n); }); }); describe('router path refunds and the vault allowance (no finding)',()=>{ it('pro-rata batch plus a walked bid at 69 bps: refund <= 51 and outflow within amount + min-x + 51',()=>{ h.ok(h.call('market','set-min-token-x-deposit',[h.U(1000)])); h.ok(h.call('token','mint',[h.U(1_000_000),Cl.principal(extra)])); // in-range bid worth 10,000 sats, an in-range ask on the taker's own side, // and an out-of-range bid at 0.9 x mid that the taker's remainder walks h.ok(h.call('market','deposit-token-y',[h.U(1_000_000),h.U(h.P*2n),h.N,h.stx,Cl.stringAscii('wstx')],h.maker)); h.ok(h.call('market','deposit-token-x',[h.U(5000),h.U(h.P/2n),h.N,h.token,Cl.stringAscii('token')],extra)); if(h.ro('market','get-token-x-pending-deposit',[Cl.principal(extra)])) h.ok(h.call('market','settle-token-x-deposit',[Cl.principal(extra),Cl.bufferFromHex('00'),h.token,Cl.stringAscii('token')],extra)); h.ok(h.call('market','deposit-token-y',[h.U(500_000),h.U(h.P*9n/10n),h.N,h.stx,Cl.stringAscii('wstx')],h.owner)); h.ok(h.call('oracle','configure',[h.U(0),h.U(79),h.U(0)])); const q=h.ro('market','get-taker-capacity',[h.U(h.P),h.U(h.P/2n),Cl.bool(true),Cl.principal(h.user)]); const amount=30000n,before=h.wallet('x'),receipt=h.smart('x',amount,h.P/2n),r=h.ok(receipt); const leg=h.routerPrint(receipt)['jing-cap']; expect(leg).toBe(q['gross-cap']); expect(r['jing-ok']).toBe(true); const refunded=leg-r['jing-in']; expect(refunded).toBeLessThanOrEqual(51n); expect(sbtcOutflow(receipt,h.user)).toBeLessThanOrEqual(amount+1000n+51n); h.conservation('x',before,r,amount); console.log('router audit: net-cap',String(q['net-cap']),'gross-cap',String(q['gross-cap']),'refunded',String(refunded)); }); }); ```