WARs DEXSupra

Trust · Safety and operator powers

wars_dex audit trail

No external audit firm (decision of 2026-10-12). In its place, four things, each leaving a record here:

  1. Fresh-eyes AI reviews. Every change to the package is reviewed before it ships by a new AI session that did not write it, briefed only with the spec, the invariants and the code, and told to break it with exploit tests. After mainnet, the whole package is reviewed again every month.
  2. Properties, not opinions. Move property tests (tests/wars_pool_tests.move) run randomised sequences of swaps, deposits and withdrawals against the invariants; the Move Prover (sources/wars_pool.spec.move) proves the ones it can.
  3. This file. Every review: date, commit reviewed, reviewer, findings, the fix commit and the test that pins each finding, in the shape of an audit report so a partner or an exchange can read it.
  4. Caps and a bounty. Pools capped at 5,000 SUPRA for the first two weeks on mainnet (set_supra_cap), and a public bug bounty in SUPRA, for what the reviews miss.

Invariants

InvariantWhere it is checked
I1A swap never lowers k = reserve_a × reserve_b (fee-adjusted, the v2 check)on chain in swap (E_K); Prover P1; property runs (check 100)
I1sUpgrade 14: a swap on a stable pool never lowers the curve's D (balances without the fee the pool keeps; to the solver's precision of one scaled unit, with the fee on top)on chain in swap (E_K); tests/stable_tests.move; reference-model fuzz (audit 2026-10-23)
I2Adding liquidity never mints more LP than the deposit's share; removing never pays more than the burned share; k / supply² never falls except by the protocol's settled shareproperty runs (checks 101 to 104, 110, 111)
I3Reserves equal the stores' balances, less donations since the last syncproperty runs (check 105)
I4The 1,000 locked LP units stay at the pool's address, so supply and both reserves never reach zeroproperty runs (check 106); F1 tests
I5Only an asset's holder moves it: no signer but the pool's own (held by the module) can withdraw from a poolF1 tests
P1get_amount_out < reserve, and the pool is paid at least the constant-product priceProver
P2get_amount_in always buys at least the output asked forProver; swap_maths_rounds_in_the_pools_favour
P3A pool's assets, stores and fee tier never change; a pool is never removedProver (global update invariants over every function)
P4A deposit never takes more than offered, at the pool's priceProver
P5Every setting changes only by the operator's signature; the role only by the proposed operatorProver
P6The protocol share stays within 0 to 1/2 (1/6 before upgrade 3) and the creation fee within 0 to 10,000 SUPRAProver (struct invariants)
P7The Floor bridge: only the operator opens it; only the account it names reserves or seeds a pool; a reserved flag is only ever cleared (a pool is seeded once) and a Floor pool's token never changesProver; tests/bridge_tests.move
P8An open LP lock's id, pool, owner and amount never change; its unlock time only moves later (upgrade 6 spec, A6-I5)Prover (written; not yet run, see the upgrade 6 entry); lp_lock_tests, audit6_tests
Conservation: across a whole random run, users + treasury + pool hold exactly what existedproperty runs (checks 107 to 109)

Tooling

Log

2026-10-12 · review 1 · phase 1 pool module (pre-commit working tree, first version)

Reviewer: a fresh Claude session, briefed with the spec, the invariants and the code only; it wrote exploit tests in a copy. Result: 1 critical, 1 medium/low design point, 2 low, 5 informational. All the critical and low ones fixed in the commit that adds this file; each pinned by a test (audit_f… in tests/wars_pool_tests.move).

#SeverityFindingStatusPinned by
F1Criticalobject::owns walks the owner chain. Stores were owned by their pool, pools by the registry, the registry by the deployer, so the deployer's own signer could empty any pool and take its locked LP through the framework's public fungible_asset::transfer, even after handing the operator role away (and could then brick the pool by draining it to zero).Fixed: the registry and every pool own themselves (own_itself); any other signer now aborts in owns at the nesting limit.audit_f1_the_deployer_cannot_move_a_pools_reserves, audit_f1_the_deployer_cannot_move_the_locked_lp, audit_f1_the_pools_and_the_registry_own_themselves
F2LowChanging the protocol share reached back: fees earned while the share was off were minted to the treasury when it was turned on again (and a raise applied to old growth).Fixed: share_epoch counts changes; growth across a change is left to the LPs. The harvest settles every pool before a change.audit_f2_turning_the_share_back_on_takes_nothing_earned_while_off, audit_f2_raising_the_share_does_not_reach_back
F3Medium/low (design)One pool per pair: anyone can create a pair's pool first, at a fee tier and price of their choosing, and the pool's tier is fixed for life. Today nothing depends on creating a specific pool.Closed by phase 3 (the Floor bridge): the Floor reserves a pair's pool at launch (reserve_pool, floor-only), so nobody can open it first; its tier is the Floor's. Other pairs stay first come, first served (nothing graduates into them).bridge_tests (pair reserved once, pair cannot be created, seeded once)
F4Lowsync could fold donated SUPRA past the launch cap, blocking SUPRA-in swaps and deposits on that pool until sells brought it back.Fixed: sync checks the cap.audit_f4_sync_respects_the_launch_cap
F5InfoAn asset's issuer holding a TransferRef or BurnRef can freeze or take from a pool's store of that asset. Inherent to fungible assets.Accepted. The Pools page should warn about assets with live refs (later).—
F6InfoAnyone can call collect_protocol_fee often to round the protocol's share down by about one LP unit a call.Accepted (negligible; costs the caller gas).—
F7InfoA protocol fee above u64 was skipped silently.Fixed: it aborts (E_OVERFLOW). Unreachable in practice.—
F8InfoEvery swap needed SUPRA's paired asset to exist (supra_meta).Fixed: is_supra checks without aborting.—
F9InfoNo Mint event at creation; pool_info.locked_lp was a constant; pool_list could overflow on a huge start.Fixed, all three.audit_f1_the_pools_and_the_registry_own_themselves (locked balance)

Also raised: the package's upgrade policy is compatible, so whoever holds the deployer key can upgrade the code. Not a code defect; it is in the "Before mainnet" list of README.md (multisig-held publisher or immutable).

Checked and found fine by the reviewer: swap and liquidity rounding (always in the pool's favour), the u256 k check, the protocol-fee formula against Uniswap v2 at 1/6, the first-depositor and donation profiles, LP token refs (no TransferRef exists, so nobody can freeze LP), object transfer disabled on the pool, stores and registry, dispatchable assets refused (accepted since upgrade 7, see below), no reentrancy path, deadline and minimum checks on every entry function, collision-free pool seeds, take for SUPRA, no unbounded loops.

The same ownership pattern was looked for in the Floor package: its vault is a resource account (no key exists for it) that holds its assets directly, so F1 does not apply there.

2026-10-12 · review 2 · the fixes above (commit 7e1d05b)

Reviewer: a second fresh Claude session, briefed with the fixes and told to break them; 8 more exploit tests in a copy. Result: no critical, high, medium or low finding; all four fixes hold. F1 was checked against every framework path that consults ownership (withdraw, transfer, primary_fungible_store::withdraw, upgrade_store_to_concurrent, object::burn, object::transfer): the deployer, the operator, the treasury and a child object made under a pool all abort. No ref that could move a pool or registry is kept anywhere. F2's epoch rule was traced through every path that settles or marks, including exemption flips interleaved with share changes.

#SeverityFindingStatus
R2-1InfoA donation folded in by sync counts as fee growth, so the protocol share of it goes to the treasury (as in Uniswap v2).Accepted; documented on sync and in the README.
R2-2InfoBecause pools own themselves, object::owns(pool or its store, X) aborts instead of returning false, and object::root_owner never ends, for third-party code that asks. Nothing in the framework paths the module uses calls them.Accepted (it is the F1 fix); documented in the README for integrators.
R2-3InfoThe creation Mint event counted the 1,000 locked units as the creator's.Fixed: Mint.lp is the creator's amount; supply = 1,000 + Mint − Burn + ProtocolFee.
R2-4Infoshare_epoch was in neither ConfigChanged nor config().Fixed: both carry it (config() gains an 8th value).
R2-5Infosync checked the cap even when it lowered the SUPRA reserve.Fixed: only when SUPRA goes up.
R2-6Infolocked_lp can be inflated by sending LP to the pool's address (the LP is lost).Accepted, cosmetic.
R2-7InfoThe accept_operator spec leaves out the @0x0 abort (hidden by the partial-aborts pragma).Accepted; not a bug.

After the fixes: 43/43 tests, the Prover passes, the indexer test passes.

2026-10-12 · review 3 · the whole module as published to testnet (commit 4ceb34b), Fable

Reviewer: a fresh session on Claude Fable 5.1, briefed with the spec, the invariants, the two reviews above and the code, and told to break it. It wrote 25 adversarial tests in a copy, now in the repo as tests/review3_tests.move. Result: no critical, high, medium or low finding. Five informational items, two of them fixed.

#SeverityFindingStatus
R3-1Infoswap_exact_out delivers get_amount_out(need), which can be a little more than amount_out (the input is rounded up to a whole unit). Never less, and the user pays exactly the quote.Accepted; documented on both functions. The existing exact-out test now asserts the output equals quote_out(need).
R3-2Infoset_protocol_share to the value it already had bumped share_epoch, forfeiting every pool's unsettled protocol fee to its LPs.Fixed: the same value is a no-op. Pinned by audit_r3_2_resending_the_same_share_keeps_pending_fees and r3_info_resetting_the_share_to_the_same_value_keeps_pending_fees.
R3-3InfoSide effect of F4: with a pool at the launch cap, one donated octa of SUPRA made sync abort, stranding any other donation to that pool.Fixed: sync folds SUPRA in only up to the cap and the rest waits in the store; the other side always folds in. Pinned by audit_f4_sync_respects_the_launch_cap (rewritten) and r3_info_a_dust_supra_donation_no_longer_blocks_sync_at_the_cap.
R3-4Infoquote_out ignores swaps_paused and quotes an input that would overflow the reserve; the swap then aborts with an arithmetic error rather than a module code. Views only.Accepted.
R3-5InfoSUPRA paid out lands in the paired fungible-asset store, never back in the CoinStore. coin::balance and take cover both, so nothing is lost; a wallet reading only CoinStore shows less.Accepted; noted in the README.

Also noted: every SUPRA path needs the coin-to-fungible-asset pairing to exist on the network (it does on testnet; confirm on mainnet before publishing).

Attacked and found fine, each pinned by a r3_ test: value extraction by random dust round trips and at u64 scale (k never falls, rounding in the pool's favour at every step, quotes equal swap results to the unit); the first-depositor inflation attack (costs the attacker a thousand times the victim's loss); the protocol fee equal to Uniswap v2's bit for bit and never above its share across a 250-step run interleaving share changes, exemption flips, donations and harvests; ownership (operator, deployer, treasury and strangers cannot transfer the pool or its stores, withdraw reserves or the locked LP); entry argument checks and the cap boundary; pool_list paging bounds; nested pools of LP tokens; coin and fungible-asset SUPRA holders on every entry function. F1, F2, F4, F7 to F9 and R2-3 to R2-5 re-checked and holding.

After the fixes: 69/69 tests (45 + 24 adversarial), the Prover passes, the indexer test passes.

2026-10-12 · review 4 · the Floor bridge, phase 3 (commits 56f103d to ab2af12), Fable

Reviewer: a fresh session on Claude Fable 5.1, briefed with the bridge spec, P7, the three reviews above and both packages, and told to break the bridge from both sides. It wrote 20 adversarial tests on the DEX (tests/review4_tests.move) and 18 on the Floor (../contracts-floor/tests/review4_tests.move). Result: no critical, high or medium finding. Two low findings (liveness, operator-recoverable, no value at risk), both fixed; the rest informational.

#SeverityFindingStatus
R4-1LowTwo switches in two packages had to agree: with the Floor's set_dex on while the DEX bridge was closed or named another account, every launch aborted in the DEX (E_NOT_FLOOR) and a pre-switch token's filling buy aborted in graduate, parking the curve just under its target.Fixed: set_dex(on) refuses a bridge that does not name the vault, or a missing SUPRA pairing (0x30018); and launch / graduate take the old paths when the bridge has moved since. Pinned by r4_set_dex_refuses_a_bridge_that_does_not_name_the_vault, r4_a_launch_with_the_bridge_moved_away_reserves_nothing_and_still_works, r4_a_pre_switch_curve_graduates_by_the_old_path_while_the_bridge_is_closed.
R4-2LowA stage-5 harvest slice worth under one unit on either side aborted inside the DEX (remove_liquidity refuses a zero output), so with min_harvest 0 a 300-octa swap made a whole harvest_range page abort.Fixed: harvest_plan returns no LP when either output rounds to zero; the page skips the token. Pinned by r4_a_dust_harvest_on_a_dex_pool_is_skipped_not_aborted, r4_harvest_range_skips_dust_on_a_dex_pool.
R4-3InfoTwo bridge_tests donation tests deposited into the pool address's primary store, which no pool code reads.Fixed: they donate into the pool's real stores (store_addrs); the claims hold there too (r4_donations_before_the_seed_only_enrich_the_floor).
R4-4Infosync still applies the launch cap to a Floor pool while check_cap skips it: an over-cap donation waits in the store.Documented (README).
R4-5Inforeserve_pool / seed_pool ignore creation_paused.Intended (graduations keep going); documented.
R4-6Infoset_floor to a new address hands every open reservation to it.Documented; the vault resource account never changes, and R4-1's guard refuses a foreign bridge.
R4-7InfoA token rescued on the curve leaves its reserved pool reserved forever (an empty registry entry).Accepted and documented; holders are made whole by the vault. A Floor-only release path can come with a later upgrade if the dead entries bother anyone.
R4-8Inforeserve_pool needs SUPRA's paired fungible asset; it exists from genesis on testnet.Covered by R4-1's set_dex check; on the mainnet checklist.
R4-9InfoAt harvest_bps 100% the Floor's principal is left one LP unit short by mine * inv0 / inv flooring.Accepted (not extractable). Pinned with a tolerance by r4_harvest_never_takes_the_principal.
R4-10InfoToken.volume / trades do not count stage-5 trades (the server reads DEX events); floor::arb handled stage 4 only.Volume: accepted. Arb: done in Floor upgrade 12 (floor_arb, the quotes and the scan work on a stage-5 token through wars_dex::wars_pool::swap; the quote is the DEX's own get_amount_out). Pinned by on_the_dex_a_dump_opens_a_gap_and_the_arb_closes_it and two more in arb_tests.

Attacked and found fine, each pinned by an r4_ test: a reserved pool untradeable on every path, entry and composable (swap, exact-out, add, remove, quotes); operator switches and fee settlement on a reserved pool are no-ops; neither the operator nor the Floor's own signer can move a reserved pool or its stores; donations before the seed set neither the price nor the LP count; no creation fee or cap for the Floor even at a 10,000 SUPRA fee and a 1-octa cap; a reservation seeds only its own pair, two reservations keep their tiers; I1 to I4 across seed, 20 swaps, a stranger's add and remove and the Floor's partial remove; the Floor's LP is exactly sqrt(ab) - 1000; the protocol-share epoch rule on a later un-exempted Floor pool; a mixed-page view check. On the Floor: the DEX creation pause blocks nothing; a reserved token graduates on the DEX even after set_dex(off) with Atmos on; launch_and_buy reserves and the lock survives; a squatter cannot take a reserved pair; 50,000 SUPRA pushed into a reserved pool's real stores do not move the opening price; a sandwiched harvest leaves the attacker poorer and the principal intact; a stage-5 rescue takes only the Floor's share; stage-5 quotes equal the DEX's results to the unit; the review-A redeem fix holds on the DEX path. Upgrade compatibility: no existing struct changed in either package (new resources and events only).

After the fixes: DEX 111/111 (91 + 20), Floor 96/96 (77 + 19), the Prover passes.

2026-10-12 · upgrade 2 · the per-swap platform cut on Floor pools

Decision (AoS): the platform's share of a Floor pool's fee is paid on every swap instead of by the Floor's harvest. swap on a Floor pool charges the fee in SUPRA both ways (on the input when SUPRA comes in; else on the constant-product output) and sends floor_fee (bps, wallet; default 35% to the treasury, set_floor_fee) of it to the wallet in the same transaction; the rest stays in the pool. The k check keeps I1 with the cut taken out: adj_in adds the cut back on the input side, adj_out takes the pool's kept fee out on the output side, so the fee-adjusted k never falls on either path (a_full_share_sends_the_whole_fee_and_k_still_never_falls, 40 swaps at a 100% share). quote_out, quote_in, swap_exact_out and swap_exact_out_entry go through the same amounts_for / input_for, so quotes match swaps to the unit on both paths (exact_out_for_supra_pays_the_quoted_input_and_the_cut). Ordinary pools are untouched (an_ordinary_pool_pays_no_cut_either_way). New resource FloorFee at the registry and two events; no existing struct changed (compatible upgrade). Tests: tests/fee_tests.move (10), the two bridge_tests reserve checks now net of the cut. Floor side: harvest_plan returns nothing for a DEX pool (upgrade 13), five Floor tests rewritten to pin the per-swap payout and that nothing moves the principal. DEX 121/121, Floor 100/100, DEX Prover passing. The Floor Prover run fails in the cached framework's object.spec.move ("cannot appear in both arithmetic and bitwise operation") on the base branch too: a framework/prover version mismatch, not this change; noted here until the framework cache is refreshed. Fresh review before mainnet, as for every DEX change.

2026-10-13 · upgrade 3 · protocol share up to 1/2

The operator's protocol share on ordinary pools may now be set from 0 to 1/2 of each swap fee (MAX_PROTOCOL_SHARE 3000 of 6000; it was 1000, 1/6, Uniswap v2's fee switch). The default stays 1/6 (DEFAULT_PROTOCOL_SHARE), and an upgrade does not touch the published config. mint_protocol_fee already takes the share as a parameter: its settlement S·φ·(√k − √k₀) / ((1 − φ)·√k + φ·√k₀) is Uniswap v2's general form and holds for any φ < 1, pinned at 1/6 by r3_protocol_fee_equals_uniswap_v2_exactly and at 1/2 by at_a_half_the_treasury_gets_half_of_the_fees_as_lp. Launchpad pools stay exempt. A constant change only: a compatible upgrade.

2026-10-13 · the module is renamed wars_pool

wars_dex::pool is now wars_dex::wars_pool, so transactions read <address>::wars_pool::swap_exact_in on the explorer. The code is otherwise unchanged, apart from one new view, is_pool(address). The registry seed moves with the name (wars_dex::wars_pool::registry), so the registry and every pool address are new. On testnet the package upgrade keeps the old pool module beside the new one, because a published module can't be removed: its pools stay withdrawable there, but the site, the Floor and the arbitrage package use wars_pool only. Mainnet publishes wars_pool alone. DEX 122/122.

2026-10-13 · upgrade 4 · the protocol share is paid on every swap

The protocol share no longer waits in the pool as sqrt(k) growth for a settlement. It leaves for the treasury inside each swap, the way the Floor pools' platform cut has since upgrade 2. A pool with SUPRA now charges its fee in SUPRA whichever way the swap goes (fee_on_output: on the output when SUPRA comes out), so the treasury is paid in SUPRA. A pool without SUPRA charges on the input and pays the treasury in the asset paid in. The cut (cut_for) is fee · protocol_share / 6000, none on an exempt pool. A Floor pool pays its floor_fee share to its wallet and never the protocol share, exempt or not. The k check in swap already took a cut on either side, so it is unchanged.

No new settlement period starts: seed_into and set_protocol_fee_exempt mark with share 0, and mint_protocol_fee always returns 0. A pool with a period from before the upgrade still settles that growth once, at the old rule (the epoch check included), through collect_protocol_fee or its next liquidity change. lp_value is therefore exact. Struct layouts and public signatures are unchanged: a compatible upgrade. Sellers on SUPRA pools now pay the fee on the SUPRA out rather than on the token in; the difference is within 0.1% of the output at the 1% tier (selling_for_supra_pays_the_platform_in_supra_not_tokens).

Tests: fee_tests covers both directions on an ordinary SUPRA pool, exempt and zero-share pools, and a pool without SUPRA. The tests of the LP settlement now start a pre-upgrade period with the test-only legacy_mark_for_test. The epoch tests that depended on a new period starting after a settlement were rewritten for the per-swap rule. DEX 125/125, Floor 124/124, arb 19/19.

2026-10-13 · phase 2 · the router (wars_dex::router) · review 5

A new module, no change to wars_pool: multi-hop swaps along a path of 2 to 4 assets (swap_exact_in, swap_exact_out, the composable swap_path), split swaps over up to three paths with common ends (swap_split_exact_in), and the views quote_out / quote_in. Every hop is a plain wars_pool::swap, so each pool's fee, its protocol or platform cut, the pause, the reservation and the launch cap apply hop by hop. The router has no storage and holds nothing between hops. A compatible upgrade (an added module).

Reviewer: a fresh Claude session, briefed with the code, this file and the README; it wrote exploit tests in a copy (now tests/router_review_tests.move). Result: nothing Low or above; three informational findings, fixed.

#SeverityFindingStatusPinned by
R5-I1Infotake asked wars_pool::supra_asset(), which aborts without SUPRA's coin pairing, so on such a network every routed swap aborted, even one with no SUPRA on the path (the shape of review 1's F8).Fixed: take checks the pairing without aborting.review_i1_routes_without_supra_work_without_the_supra_pairing
R5-I2InfoA split's amounts were added in u64, so parts summing past u64 aborted with a VM arithmetic error.Fixed: added in u128, over u64 is E_BAD_SPLIT (0x1001F).review_i2_split_parts_that_overflow_u64_are_a_bad_split
R5-I3InfoA split read its first path's ends before checking it, so an empty first path aborted with a vector error.Fixed: the first path is checked first (E_BAD_PATH).review_i3_split_with_an_empty_first_path_is_a_bad_path
R5-I4InfoQuotes don't check pauses or reservations; a route through a reserved pool is refused by wars_pool (0x30016) at swap time.As designed: hop aborts carry wars_pool's codes.review_route_through_a_reserved_floor_pool_is_refused

Held: an exact-out sweep of 81 cases (2 and 3 hops, fee on input and on output, SUPRA first, middle and last, a Floor hop) always took exactly quote_in's input and paid quote_out's output to the unit, at least the amount asked; cycles abort on every entry; two split paths through one pool pay the moved price under the total min_out. Compiles with the Supra CLI's compiler v1. DEX 150/150.

2026-10-14 · review 6 · wars_pool, router and the cycle arb at testnet 85e4c6d

A fresh session reviewed the package and found 0 critical, 0 high, 0 medium, 3 low and 5 info findings. The full report is in docs/supwars/MOVE-CONTRACTS-AUDIT-2026-10-14.md (DEX section), and the exploit tests are in docs/supwars/audit-2026-10-14/audit6_tests.move.

#SeverityFindingStatus
A6-L1LowA pool whose settlement period from before upgrade 4 is still open pays the protocol share twice (per-swap cut, then LP minted at settlement)Fixed in upgrade 6 (below), plus a one-off operator step at publish
A6-L2LowAnyone with one LP unit can grow the shared LpLocks.by_pool vector without bound, raising lock gas and eventually blocking new locks on that poolFixed in upgrade 6: LockIndex
A6-L3LowLock, extend, burn and creation fees have no user-side maximum, so the operator can raise them in the same blockFixed in upgrade 6: *_v2 entries with max_fee, used by the site
A6-I1 to I5Infoburn_lp fee optional; dust sells pay no cut; operator power through the bridge; arb views overflow on large reserves; spec coverage lags the codeI2, I4, I5 fixed in upgrade 6; I1, I3 accepted (below)

Held: k never falls on either fee path; pools, stores and locks are self-owned; the router and the cycle keep nothing and check limits on the final amount. F1, F2, F4 and contracts-review-5 stay fixed; D-01 (publisher key) and C-01 (Atmos signature) stay open. DEX 225/225, arb 26/26.

2026-10-14 · upgrade 6 · the review 6 fixes (wars_dex and wars_arbitrage)

Fixes every Low finding of review 6 and the cheap Info ones. A compatible upgrade of both packages: two new structs (LockIndex, PoolSlot), new entry functions and one new view; no existing struct, public or entry signature or event changed, and no function the Floor calls changed (the Floor builds and passes against it unchanged). The exploit tests of docs/supwars/audit-2026-10-14/audit6_tests.move are now regression tests in tests/audit6_tests.move, asserting the fixed behaviour.

#FixPinned by
A6-L1swap (wars_pool.move:463) closes an open pre-upgrade-4 period without minting at a pool's first swap: from then on the growth comes from swaps that pay the share in the swap, and mint_protocol_fee cannot tell it from old growth, so the period's growth stays with the LPs (the same rule as an epoch change). Growth with no swap since still settles once at the old rule. The swaps between upgrade 4 and this upgrade are already in any open testnet period, so the operator voids those at publish with two set_protocol_share calls (README, "Upgrade 6 publish"). Mainnet is unaffected either way. The settlement maths tests now open a legacy period with the test-only legacy_period_for_test(pool, k_last) after their swaps, standing for pre-upgrade growtha6_l1_the_first_swap_closes_a_legacy_period_unminted, a6_l1_legacy_growth_with_no_swap_since_still_settles_once, a6_l1_bumping_the_epoch_voids_the_legacy_period; r3_protocol_fee_equals_uniswap_v2_exactly and the other settlement tests
A6-L2New LockIndex at the registry (:218): (pool, position) -> id, id -> position and a count per pool, one table entry per lock, with swap-remove on unlock (:948-985). lock_lp / lock_lp_v2 add there (:723) and never touch LpLocks.by_pool; unlock_lp takes a lock out of the index, or out of the old vector for a lock made before the upgrade (:759). That vector can only shrink now, so lock and unlock cost O(1) whatever a griefer adds. pool_locks (:854) lists the old vector's locks first, then the index; new view pool_lock_count (:875). Chosen over a minimum lock size: it needs no new setting, keeps every lock legal, and keeps the old data readable without a migration. by_owner stays a vector: only the owner grows their owna6_l2_one_unit_locks_do_not_grow_a_shared_vector, a6_l2_the_index_stays_exact_beside_pre_upgrade_locks; lp_lock_tests unchanged
A6-L3create_pool_entry_v2 (:526), lock_lp_v2 (:701), extend_lock_v2 (:734), burn_lp_v2 (:771) take max_fee and abort with E_FEE_ABOVE_MAX (0x1001C) above it (pay_lp_fee, :925). The old entries call them with no maximum and behave as before. The Pools page sends the v2 entries with the fee it showed (a lock allows the band its length can fall into in the next 10 minutes) once /api/dex reports maxFee (server/dex.js reads pool_lock_count); chain.js lists the four with gasa6_l3_a_raised_lock_fee_aborts_lock_lp_v2, a6_l3_a_raised_extend_fee_aborts_extend_lock_v2, a6_l3_a_raised_burn_fee_aborts_burn_lp_v2, a6_l3_a_raised_creation_fee_aborts_create_pool_entry_v2, a6_l3_v2_entries_pay_the_fee_within_the_max, a6_l3_create_pool_entry_v2_pays_the_fee_within_the_max
A6-I1Accepted and documented on burn_lp_v2: a plain LP transfer to the pool address is the same burn without the fee. Counting burns separately would need a new tally and gains only fee revenuea6_i1_burn_by_transfer_is_accepted_and_burn_lp_v2_charges
A6-I2The fee on the output rounds up (amounts_for, :1302), so a dust sell pays at least one octa. Exact-out still delivers at least amount_out (g − ceil(g·f/B) ≥ a whenever g·(B−f) ≥ a·B), and the k check only gets easier. The Swap page (dex-paths.js), the arb bot's DEX hop (server/arb.js) and qa/dex_qa.js round the same way; the router numbers pinned on both sides moved by one unit (987128711, 483899765)a6_i2_a_dust_sell_pays_a_fee, qa_info_dust_sells_pay_no_fee (now asserts the octa), two_hops_through_supra_pay_what_the_two_pools_quote, fee_tests
A6-I3Accepted: operator trust, as R4-6. Restricting set_floor needs the Floor vault's address in this package, which would tie the DEX to one Floor deployment; to be decided with the multisig before mainnet—
A6-I4cycle.move: quote_mixed takes the Atmos fee in u128 (:165); best_two_pool_mixed reduces each side's x·y·k/BPS (below 2^128) before multiplying (:190-191) and returns (0, 0) when the sized input would not fit u64 (:196); isqrt starts at n/2 + 1, which cannot overflowa6_i4_quote_weighted_takes_a_huge_amount, a6_i4_best_two_pool_sizes_a_trade_on_huge_reserves (contracts-arb/tests/qa_cycle_edge_tests.move; both fail on the old code)
A6-I5wars_pool.spec.move: operator-only specs for set_floor_fee, set_lp_fees, set_protocol_fee_exempt, the creation fee bound on set_creation_fee, create_pool_entry_v2's max, and P8 (an open lock's id, pool, owner and amount never change, its unlock time only rises). The specs type-check in every test build; the Prover was not run in this session (no Boogie or Z3 here), so run move-test.js prove dex before the publish. Not added: locked[pool] equal to the sum of open locks (a sum over a table needs a ghost variable) and router specstype-check in move-test.js dex

Tests: DEX 238/238 (225 + 13 in tests/audit6_tests.move; 10 existing tests rewritten for the rounding and the legacy period), arb 28/28 (26 + 2). QA: dex_qa 290/290, http_qa 462/462, contracts_qa static checks for wars_pool and arb all passing, chains/supra/test/dex_test.js and dex_paths_test.js passing. Publish: wars_dex, then the operator's two set_protocol_share calls, then wars_arbitrage, then the site (README, "Upgrade 6 publish"). Fresh review before mainnet, as for every DEX change.

2026-10-15 · upgrade 7 · assets with dispatch hooks

Pools accept assets with dispatch hooks (tokens launched on Atmos carry its hooks). Every move in or out of a pool store goes through dispatchable_fungible_asset (put, pull) and is counted by what the store gained or lost, so I3 holds whatever a hook does. Independent review of the change (same day), all fixed in the same upgrade:

Checked fine by the reviewer: no value path from a hook to SUPRA, other LPs, the treasury or the Floor cut; I1 and I3 hold, including for a hook that adds to the store; a hook calling back into wars_pool aborts with the VM's re-entry lock (RUNTIME_DISPATCH_ERROR 4037), pinned by a_hook_cannot_reenter_the_pool; sync and the helpers read the raw store balance, never a derived balance; the upgrade is compatible (no struct or public signature changes). What no code can undo, documented and warned about on the site: a hook that refuses to move stops its pool for both sides, a hook that burns extra from the pool's store on a payout makes buys fail the k check, and a hook's module can be upgraded by its owner. Hooked pools need the chain's dispatchable fungible asset feature on.

2026-10-16 · review 10 · upgrade 10 · coin-standard tokens (wars_dex::coins), Fable

Scope. The new module sources/coins.move: nine generic entries (swap_exact_in<X>, swap_exact_out<X>, swap_path_exact_in<X>, swap_path_exact_out<X>, add_liquidity<X>, add_liquidity2<X,Y>, create_pool<X>, create_pool2<X,Y>, zap_in<X>), three views (asset_of<X>, balance<X>, coin_of) and their helpers, read against the wars_pool and router functions they call, against the asset-address entries they must match, and against the framework's coin.move (withdraw, balance, deposit, paired_metadata, paired_coin, coin_to_fungible_asset, the paired refs). wars_pool.move and router.move are unchanged by this upgrade.

Method. A fresh session, briefed with this file's invariants and the code only, traced every entry for what leaves the caller and what comes back, the min/max/deadline/zero checks, every guard of the asset-address entry (assert_live, pauses, Floor reservation, E_ON_FLOOR, creation fee and max_fee, launch cap, hook refusal on exact-out, path shape), type-argument abuse, the framework's pairing side effect, hook re-entry, rounding, events and views, and wrote 25 adversarial tests (tests/coins_audit_tests.move, with a test-only hooked token whose withdraw hook calls back into wars_pool when armed). Package: 303/303 (coins_tests.move 20, audit 25).

Result: no critical, high or medium finding; one low, eight informational. The module adds no state, holds no refs and no signer but the caller's, and reaches pool state only through the public wars_pool / router functions the asset-address entries use, so every one of their guards holds; the caller never loses more than the pool receives (plus the creation fee and the swap fee) and the unused remainder always comes back.

Attacked and found fine, each pinned by an audit_ test: exact-out takes exactly quote_in and pays at least what was asked; routed swaps pay the quotes to the unit with nothing stranded between hops; deposits and the zap conserve value; Floor-reserved pools refuse coin deposits and swaps; a coin the Floor holds gets no other pool; swap and creation pauses, the launch cap, the creation fee, duplicate pools and a coin with itself are refused; cycle paths are refused on both path entries; a type that is not a coin cannot be spent; SupraCoin as X pays what the asset entry pays; an asset-only holder with no CoinStore<X> sells through the coin entry. No test written as an exploit passed unexpectedly.

Verdict. Safe to publish as a compatible upgrade (additive module, no struct or signature changes). The compile against the real framework passed here; the v1-compiler check (move-test.js compile dex with APTOS_V1) could not be run in this environment and should be run before the publish, as the README says.

2026-10-23 · upgrade 14 · stable pools, Fable

Scope: wars_pool (the StableSwap curve, create_stable_pool*, the D check in swap, pools_for, the new views), the two coins create entries, and the off-chain copies (public/js/dex-paths.js, server/stableflags.js, server/dex.js, server/arb.js, public/js/pools.js) at 86e0641. Independent Fable audit, report and proof scripts in the project's audits/2026-10-23-dex-stable-pools/. No Critical, High or Medium findings.

Checked by the build: 16 stable tests (creation and address, duplicate, fee and decimals checks, fee cap, pauses, swap prices against the mock, exact-out, 6 and 8 decimals, fee on output, round trips, a huge drain, liquidity, locks, burns, router paths, zap), all 398 dex tests and every dependent package (arb 51, arbspikey 33, arbdexlyn 31, rewards 84, migrate 36, orders 116, floor 340). A Python reference model ran hundreds of thousands of random swaps with no D fall and no abort; the JS copy matches the Move results to the unit.

Verdict. Safe to publish as a compatible upgrade.