Internal Audit 2026-09

Source of truth: github.com/btr-protocol/audits. This page mirrors the report with a status note as of 2026-09-27.

Internal audit, September 2026

Counts and scope are as of 2026-09-27, the date of this report. Code changed after the heads in §2 is not covered; findings against it are published under the disclosure policy once fixed and deployed.

Internal audit of the BTR DEX, 2026-09-02 to 2026-09-27, before any mainnet deployment. This page describes the launch code only. Findings against code the launch redesign deleted are counted, not listed. Previously published as 2026-09-16 (“Security review, September 2026”); that address redirects here.

Findings are published under the disclosure policy: a finding appears once its fix is deployed to every chain running the affected code. Rows filed against the launch code itself (campaigns of 2026-09-26 and 09-27) are counted here and held until that code is deployed. Locations are file plus symbol; line numbers are omitted for closed repositories.

1. Funnel

StageRowsNote
Filed946835 in the 2026-09-02 to 09-17 campaign, 111 in the launch-code campaigns
Not a real finding184duplicate, subsumed or refuted by two independent reviewers
Real762
Below the reporting bar110informational, closed with no code change (owner bar, 2026-09-10)
Moot97against surfaces the launch code deleted: the V4 and V5 oracles and their beacon, the first single-word mark store, the session and signer-set contracts, the Wombex claim periphery, the testnet operator scripts
Real at the launch code555
Root-cause rows440after merging rows that share a mechanism and a fix
Published below282in-scope components (dex-evm, shared, sdk, front, core), in 25 bundles; 1 more held until applied on a live chain
Launch-code rows held85in-scope, merged into the bundles below, published once deployed

The rest are located in the back-end services, the keepers, the price-feed producer and the operational environment. They are disclosed to auditors under non-disclosure, because their write-ups name infrastructure and key custody.

2. Scope

The launch code, heads of 2026-09-27: dex-evm aa1286f, shared 1f2d4fc, sdk c5d4072, front fcc27d4d, core 5225fad. Code after these heads is outside this report. Every fix cited in the private ledger is an ancestor of its head, asserted mechanically by the workbook gate. The shape that ships:

  • Marks. One mark store per chain in the Pool implementation, pushed by a committed relayer set with k-of-n signatures per tier; per-lane band, halt and anchor; owner reanchor for a lane dark past its band. Four lanes per word per tier; a 9-lane push costs 73,972 gas (full tx). The V4 and V5 oracles, their beacon, the first single-word store and the per-leg repoint lane are gone.
  • Pools. Factory-minted PoolProxys; one fleet upgrade at the GOVERNANCE tier re-validates the implementation, migrates the mark store and moves every proxy in one transaction.
  • Solvency. One pool-level coverage rate C; with a leg dark, credits refuse and same-asset exits pay min(c_leg, C_lower).
  • Hooks. Venus first; NAV counts idle underlying; a write-down latches the hook.
  • Flash and coop. Flash lends liquid reserves only. Cooperative arbitrage fills through CoopArb at a discount on the minimum fee and σ part of the spread, never on the risk premia.
  • Governance. GOVERNANCE 7 d, LISTING 1 d, TUNING 1 h, guardian veto on each; one guardian Safe. Steward fee, vega and coop fences cap raises only; a guardian coop kill voids any queued re-arm.

3. Method

Multi-model and adversarial, refute-first: every candidate goes to two independent reviewers instructed to refute it, a split to a third. Severity is graded at the shipping configuration. Each finder returns a coverage log over its whole target set and is re-dispatched below full coverage. A campaign stops after six successive clean cohorts, three reading the whole system and three a fixed scope, all after the last fix. Full method: METHODOLOGY.md.

CampaignCodeFindersRowsMediumLowInfoStop rule
2026-09-02 to 09-17pre-launch mains-835---superseded by the launch redesign
Alaunch code306141void: finders too shallow (~8 tool calls each); fixes re-audited in A’
A’launch code423612213not reached
Blaunch code80171151not reached
Claunch code247052not reached
Cooparbitrage stack-161132closed by the coop fix set, re-read in D
Dlaunch code16428 (18 rows, 10 aliases)21610reached 2026-09-27
Flaunch code after the phase-2 changes152272214reached 2026-09-27

No campaign on the launch code filed a High or Critical. Campaigns A to F total 121 findings (18 Medium, 82 Low, 21 Informational): 111 rows plus the 10 aliases in D, which merged into existing rows; the coop pass adds 16 (1, 13, 2), tracked outside the row ids. F’s 27 rows merged into existing bundles except three new root causes.

4. Findings

Root-cause bundles at the launch code, High to Informational. Rows counts the published rows of the 2026-09-02 campaign that still describe live code. The 85 held launch-code rows in this scope are merged into these bundles in the private ledger and join the count here once deployed. “Closed” means no code change was warranted (a record correction or a design position stated with its control).

IDSeverityRowsFindingControl at the launch codeStatus
F-01 (with F-23, F-24, F-35)High117Oracle marks: a rejected push could wedge a lane for good, a rebias left the next push unbanded, lane state was shared across a slot, and a quarantined lane could heal past its own bandOne mark store per chain in the Pool implementation (MarkStoreP8, wire 8). Every lane is banded against its live mark or, when dark, its anchor, with a capped band; the owner reanchors a lane dark past its band at TUNING. Per-lane clock, σ and halt; every σ read clamps to the class floor.
F-02 (with F-75)High7-Access control: role bootstrap and the quorum check were fail-open before the signer set was seeded; a pending rotation could be overwritten and a compromised treasury owner could veto its own evictionbootstrapRole reverts post-arm and arming needs a non-zero factory; queueRole refuses a live pending rotation; the treasury-owner self-veto is capped at one per rotation. Mark-store quorum is k strictly ascending signatures on a committed roster.
F-03 (with F-14)High153Pool provenance: permissionless pools were born with no authority or fee sink, and an off-factory clone ran the real implementation under attacker governancePools are factory-minted PoolProxys bound to the factory at initialize; Admin, Flash and CoopArb refuse any pool the factory did not mint.
F-04High6-Hook ledger writers booked reserves, liabilities and the liquidity index without proving the underlying balance movedhookCreditYield proves the balance covers reserves, fees and the credit before any book move, books face at C, and refuses new credit under a halt while recall and write-down stay open; the liability floor keeps a total loss from writing a zero index.
F-05 (with F-20, F-48, F-58, F-62, F-49)High225Deploy ceremony: scripts could brick an immutable deployment, ran testnet-only paths on a mainnet-class chain, targeted retired surfaces, signed unbound salts, and shipped parameters that breached the fee floorProtocol.s.sol → Pools.s.sol → Assets.s.sol: chain class and RPC attested, gas proven before each broadcast, CREATE3 salts bound, class fields width-checked, manifest parameters validated against the fee floor before signing.
F-07High1-The release shipped a governance ladder before the owner ratified itThree ratified tiers with a guardian veto on each: GOVERNANCE 7 d, LISTING 1 d, TUNING 1 h.
F-08 (with F-59, F-63)High155Authority lanes: guardian and foreign-pool seats reached pools the protocol does not administer, an owner unhalt could relist a re-anchored leg, a steward raise voided unrelated owner operations, and a sentinel-pool halt carried no release clockOn a foreign pool only its own seat cancels and the guardian holds no unhalt edge; collapseAnchor refuses foreign pools; one halt bit, lifted by the owner only; unhaltAll names the legs to keep halted; a κ raise voids only a conflicting queued op.
F-09 (with F-42, F-68)High121Off-chain pricing mirrors drifted from the Solidity law: scale, confidence, coverage wall and exit capThe Rust core is an integer mirror of PricingLib with pinned equivalence digests; the SDK and front read quotes from the back end and mirror only the exit cap and σ floor.
F-10 (with F-43, F-26, F-22)High12-Front transaction builder: debited the full typed amount on the first leg, pinned decimals by symbol, re-anchored its own floor and allowed a double submitAmounts are encoded in each chain’s decimals, later hops sized from the previous hop’s floored output; the form encodes the displayed floor, refuses a lower re-quote and drops a second click.
F-11 (with F-12, F-16, F-52, F-70)High142Front chain reads: stale or failed reads rendered as permissive values, the safety console served a stale ABI, and display surfaces overstated what the pool would quoteFailed sub-reads render as unknown and the coverage wall is a required input; ABIs are generated from the contracts; the oracle and safety pages read the factory and both tiers directly.
F-13 (with F-15, F-19, F-47, F-40)High1713Coverage and settlement: LP paths settled at per-slice rates, liability re-denomination applied C twice, one unusable leg froze the pool, a pool with reserves and no liabilities read full coverage, and an issuer freeze strands a cross exitOne pool-level coverage rate C on every arm. With a leg dark, credits refuse and same-asset exits pay min(c_leg, C_lower); write-down books off the C bounds; the index floors at 1e9 and no invest lands below 1e12. An issuer freeze is page-only (accepted).
F-17High7-Exact-in swaps were non-monotone above the output argmaxGross output is clamped at the closed-form argmax before the coverage toll; the Rust core mirrors the clamp.
F-18 (with F-41)High11-Pool configuration writers lacked bounds and roster invariants, and the interior ceiling was a field-width artifactConfig writers are bounded (κ, hook token match, roster survives deregistration); the interior cap charges only interior-capable legs and the fence budget fits the uint16 spread.
F-21 (with F-73, F-25)High10-Build and test pins: optimizer settings broke cross-repo parity, artifacts were unpinned, and coverage and upgrade-order pins were absent, bare or on the wrong pathOne optimizer profile shared with shared under a CI parity gate, re-derived library CREATE2 pins, storage frames pinned by ArtifactGuards.t.sol.
F-27Medium610The factory upgrade lane had no storage-version gate and no execute-time revalidationexecuteReferenceUpgrade (GOVERNANCE) re-validates AC, admin, flash, a forward storageVersion and the new store logic’s immutable FACTORY, migrates the mark store, then moves every proxy in one transaction; store governance runs on the live implementation only.
F-28 (with F-55, F-50, F-76)Medium121Published ABIs diverged from the contracts and the SDK build fetched them from a live API with a tautological integrity checkgen.py --check runs as a CI gate; the SDK pins ABIs by content hash (abis.lock.json) and fails closed on a mismatch, with no live-API dependency.
F-29 (with F-53, F-57)Medium7-Documentation, natspec and interfaces described retired levers or overclaimed against shipped constantsDocs and natspec rewritten against the launch code; the audit docs page is generated from this report.
F-30 (with F-31, F-44, F-60, F-69)Medium20-Router floors: the client authored or mis-allocated floors, accepted a server floor at any tolerance, trusted backend pool addresses, measured output at the wrong place, and chained legs were not self-directedThe server authors every floor; the SDK refuses a part without one, bounds it by the user’s slippage, requires an official pool per hop, scales the chained floor, and the router floors on the recipient’s delivered balance.
F-32 (with F-33, F-34)Medium221Queued and instant risk writes: a queued absolute update overwrote a defensive tighten, instant lanes had no cumulative limit, and the native sentinel resolved under a second keyQueued asset-parameter ops carry a per-field snapshot and revert on a conflicting field; a tighten is refused while an UPDATE_RISK op is live; steward writes sit in an on-chain 24 h cumulative window; a vega raise re-checks the dispersion cap.
F-36 (with F-37, F-61, F-64, F-65)Medium1816Yield hooks: yield booked at par, cancel authority protocol-scoped, a refusing hook could not be replaced, adapters trusted venue amounts, and force-clear ran inside flash contextHooks book credit at C from delivered tokens, count idle underlying in NAV, latch writtenOff after a write-down, defer refused credit so a trim still runs, and cap liquidations at invested. Flash lends liquid reserves only.
F-45Medium5-Trading routes rendered before the access gate and CI had not run for two daysEvery trading route sits behind the invite and disclaimer gate; CI pins a released toolchain.
F-46 (with F-71, F-72)Medium15-Wallet plumbing: a broadcast tx could read as cancelled, batches dropped failure detail, and transports stamped a stale chain idThe lifecycle reports what was signed, broadcast and mined; mined predecessors confirm when a later call reverts; sendCalls re-reads the chain id and refuses a moved chain; storage access never throws.
F-51 (with F-74)Medium9-SDK transport: encoding and nonce allocation produced unsendable or gapped transactions, and RPC endpoints were trusted without chain attestationEvery endpoint attests its chain and a wrong one is evicted; the encoder checks lengths and bounds; the nonce allocator releases on a failed send.
F-67Low73Fee-free LP flows were a toll-free substitute for a swap, and the internal depeg breaker compared the wrong pairLP flows pay the same-asset exit toll (saturated on a dark mark); per-asset USD depeg bands test the mark against 1.0; flash fees round toward the pool.
F-77Informational1-keccak256 in the Rust core panicked on inputs whose length was a non-zero multiple of the rateFixed; equals EVM KECCAK256 for every input length.

Moot, counted not listed: F-06, F-39, F-54, F-56, F-66 and the V4/V5 rows of the bundles above (96 rows in total across all components).

5. Residual risk

Open findings are held at every severity. At the launch code, 12 root-cause rows are open or acknowledged across all components (1 High and 1 Medium in operations, the rest Low or Informational); they are published on the same rule once their residual closes.

Accepted by design:

  • One leg with an unusable mark freezes deposit, donate, cross exit and hook credit pool-wide (fail-closed); same-asset exits stay open.
  • Every armed spoke swap needs the reference tier fresh: a second liveness dependency.
  • The coverage toll recovers at most half the LVR it prices; κ is sized on residual loss.
  • An issuer pause or blocklist on a listed token is page-only; no lever unfreezes the leg.
  • Price marks rest on single-organisation k-of-n signer trust.

Before the next chain deploy: fork suites and the live fork push at the launch heads, the library redeploy at the ceremony, and third-party review. Reports are linked from the Security Overview as they land.

6. Reporting a finding

Findings against deployed contracts go to security@btr.markets. Please do not open a public issue for anything exploitable. We confirm receipt, say whether the finding is already in the private ledger, and tell you when the fix is deployed; once it is, the finding is published with attribution unless you ask otherwise. See Bug Bounty for scope, rewards and safe harbour.