← All documentation

KAURAX — Internal Security Review

Date: 2026-09-08 Reviewer: internal. This is not an audit. No external firm has examined this code, and an internal review by the person who wrote the code is the weakest kind there is.

Findings are listed whether or not they are convenient. Two of them were found by tests written during this work; two were live outages I caused and fixed.


Summary

SeverityCountOpen
CRITICAL00
HIGH33
MEDIUM52
LOW43
INFORMATIONAL32

No CRITICAL findings does not mean the system is safe. The HIGH findings are structural and all three are open.


HIGH

H-1 — No fault proof system

Open. This is the finding.

Output roots are accepted because the proposer key signed them. Nothing verifies they correspond to any execution. A dishonest proposer can commit an arbitrary root and, once it finalizes, withdraw against it.

The dispute game reduces the exposure — anyone can object, and objecting is cheap relative to lying — but the guardian decides, so the ceiling on security is the guardian's honesty and competence.

Fix: implement docs/FAULT_PROOF_SPEC.md. Nothing else closes it.

H-2 — Guardian is the final arbiter

Open. By design, documented.

KauraxDisputeGame.resolve is onlyGuardian. A captured guardian can rule for a lying proposer, or against an honest one. It cannot rewrite finalized history — the oracle refuses to delete a finalized output — but within the window it decides outcomes.

Mitigations in place: every move and resolution is on chain with a reason string, so decisions are auditable after the fact; guardian inaction refunds both sides rather than deciding by silence.

Fix: the verifier. Interim: the guardian should be the multisig behind a timelock, which is deployed but not yet holding the role.

H-3 — No external audit

Open.

254 contract tests are evidence of intent, not of correctness. The settlement, bridge and dispute contracts have never been read by an independent party.

Fix: audit before any deployment carrying value.


MEDIUM

M-1 — A dispute could outlive the finalization window

Fixed.

The oracle finalizes on a timer that a live dispute does not pause. A game that runs past that point ends with the output already finalized and undeletable. The game pays the challenger and emits DisputeOutlivedFinalization rather than reverting and stranding the bond, but the wrong commitment survives.

Found by: test_cannotTimeoutTwice failing with CannotDeleteFinalized during development.

Fix applied: isOutputFinalized consults the dispute game, and the portal refuses to finalize a withdrawal against an unsettled output — the second half matters, because the portal gates on the proof's age, so without it a withdrawal could complete against a commitment still under dispute.

M-2 — Proposer bonded late

Fixed.

The proposer stakes on its first defence, not at proposal time, so an output root carries no stake until challenged. A proposer that never intends to defend loses only the output.

Fix applied: the oracle escrows PROPOSER_BOND at proposal time. Forfeited to the challenger on a loss; refunded to the proposer when the output finalizes; refunded to proposers of outputs deleted as collateral, which were never adjudicated.

M-3 — Operator keys held locally on the devnet

Open.

KAURAX_SIGNER_MODE=local puts the sequencer, batcher and proposer keys in the process environment. Compromising the node yields all three.

Mitigation: the signing seam is implemented and tested (19 tests); the node can run without ever holding a key. The devnet does not use it.

M-4 — RPC served over plain HTTP

Open.

No TLS, so RPC traffic is readable and modifiable in transit. Blocked on DNS records and on the host blocking 80/443 for trial accounts.

M-5 — CORS misconfiguration caused a silent total failure

Fixed.

The API read API_CORS_ORIGINS; the deployment set CORS_ORIGINS. The allow-list was empty, so no preflight was answered and every cross-origin browser call failed — while curl, which sends no preflight, kept returning 200. A one-word difference produced a complete outage for real users and no signal for the operator.

Fix: both names are now accepted. The lesson generalises: a security control that fails closed and silently is worse than one that fails loudly.


LOW

L-1 — One live game per output allows mild griefing

Open, accepted. A challenger can open a game and abandon it, delaying the next challenger by one timeout. The alternative — parallel games — lets one bond force a proposer to defend many at once, which is worse. Cost of the grief is the challenger's bond.

L-2 — Faucet uses a published devnet key

Open, devnet only. The faucet account is a well-known Anvil key. Anyone can drain it. It is isolated from the operator roles, so the blast radius is the faucet's own balance. A public testnet needs a key that is not published.

L-3 — getGame panicked on an unknown id

Fixed. Returned an array out-of-bounds panic instead of UnknownGame. Found by test_unknownGameReverts.

L-4 — Unquoted mnemonic in .env

Open, cosmetic. KAURAX_DEV_MNEMONIC=word word word breaks . ./.env in a shell. Docker Compose parses the file directly so the stack is unaffected.


INFORMATIONAL

I-1 — Determinism is assumed, not specified

revm is deterministic in practice. Nothing pins the EVM version or tests against a second implementation. A prerequisite for any verifier.

I-2 — Explorer reports verified: false for all contracts

Correct — there is no source verification service. Listed so nobody mistakes it for a bug.

I-3 — Sequencer is a single operator

Not a defect at this stage. Forced inclusion bounds the damage: an ignored forced transaction halts settlement for everyone, verified on a live chain.


Checked and found sound

Stated so the review is not only a list of problems. Each was examined and, where a test exists, it is named.

AreaFinding
Reentrancy in bond settlementSound. Settled flag written before any transfer; verified with an attacker contract that actually re-enters (test_reentrantWinnerCannotSettleTwice)
Bond accountingSound. Contract holds nothing after every terminal path; conservation fuzzed
Failed payoutsSound. Reverts rather than silently keeping funds
Double resolutionSound. Guarded by the settled flag and by status checks
Access control on the gameSound. Proposer, challenger and guardian roles each tested for unauthorised callers
Timeout fairnessSound. Winner determined by whose turn lapsed, not by who called
Withdrawal proofsSound. Merkle proof required; no operator approval path
Deposit censorshipSound. Derived from L2 events
Admin RPC exposureSound. anvil_* and evm_* refused on the public endpoint, verified externally
Internal portsSound. Engine, PostgreSQL and Grafana unreachable from the internet, verified externally
Integer overflowSound. Solidity 0.8 checked arithmetic; no unchecked blocks in the dispute game