Khromosome contracts — internal security review (October 2026)

This is an internal review, not an independent audit. It was performed by the project's own engineering process (manual review + static analysis + regression tests). It does not replace the external audits listed in audit-rfp/SCOPE.md, which remain a gate for mainnet and for any sale.

Method

  1. Manual review, prioritising code added in this cycle: src/sale/*, src/names/*, the KhromeBridge fee path and the Khromeswap _mintFee change, then every other contract.
  2. Static analysis: Slither 0.11.6 using the repository's slither.config.json. The config itself was broken: detectors_to_run was a JSON list and named four detectors that do not exist, so Slither, and the CI job that uses it, crashed before analysing anything. It was repaired in this review.
  3. A failing test first for every confirmed issue, then the fix, then the full suite green. Regressions live in contracts/test/security/InternalAudit202610.t.sol and node/relayer/release-key.test.js / relayer/release-key.test.mjs.

Findings

ID Severity Component Title Status
KHR-01 High MonetaryController Genesis predeploy lets anyone re-bind the system caller → consensus halt Fixed
KHR-02 High KhromeSubscription Upgrade top-up priced from current tier config; a re-price lets users withdraw other stakers' KHROME Fixed
KHR-03 Medium KhromeChain Malformed signature accepted when a producer's signingKey is address(0) Fixed
KHR-04 Medium KhromeNames With a zero-rent tier, anyone can renew a name to an expiry near 2^256; the grace-period add then overflows and bricks the name Fixed
KHR-05 Low (legacy) KhromeVault (superseded by KhromeDrive) Malformed signature records owner = address(0), enabling fileId squatting/deletion Fixed
KHR-06 Medium Bridge relayers Two BridgeBack burns in one Ethereum tx share one replay key → second burn never released Fixed

KHR-01 — MonetaryController system caller can be re-bound (High)

The zero-point issuance design embeds MonetaryController at 0x…0777 in genesis, writing SYSTEM_CALLER directly into storage slot 0. Genesis does not set the _systemCallerSet flag that initSystemCaller() checks, so on a real genesis anyone could call initSystemCaller(attacker). After that every onBlock() system call from khrome-reth reverts with "not system". The reth hook treats a revert as a consensus halt, so the chain would stop.

Fix: initSystemCaller also requires SYSTEM_CALLER == address(0). Test: test_attackerCannotRebindGenesisSystemCaller. Note: the controller is not live on chain 777001; this affected the devnet and the planned mainnet genesis.

KHR-02 — Subscription upgrade accounting (High)

upgrade() computed the top-up as newTier.stakeRequired - currentTier.stakeRequired using the current config, then set stakedAmount = newTier.stakeRequired. If the owner re-priced a tier between a user's subscribe and upgrade, stakedAmount no longer matched what the user deposited. A later unsubscribe could then pay out more than the user put in, funded by other stakers. In the opposite direction it overcharged or blocked the user.

Fix: the top-up is measured against s.stakedAmount, and stakedAmount always equals the KHROME actually deposited.

KHR-03 — KhromeChain zero-address signer (Medium)

_recoverSigner returns address(0) for malformed signatures. A producer registered with signingKey == address(0) therefore accepted any garbage signature. Fix: reject signer == address(0) explicitly.

KHR-04 — KhromeNames expiry overflow (Medium)

setPrices allows a zero-rent tier. At zero price anyone can renew someone else's name with an enormous duration, pushing expiry near 2^256. Every later expiry + GRACE_PERIOD then reverts on overflow, so the name can never be renewed, transferred or re-registered. Fix: cap expiry at MAX_EXPIRY = type(uint64).max (DurationTooLong).

KHR-05 — Legacy KhromeVault zero-address owner (Low)

Same root cause as KHR-03 in the superseded KhromeVault (replaced by KhromeDrive, which uses EIP-712 and is unaffected). Fixed for completeness so the legacy contract is never redeployed with the bug.

KHR-06 — Relayer replay key per transaction, not per event (Medium)

Both relayers keyed KhromeBridge.release(…, ethTxHash) by the Ethereum transaction hash. A contract wallet or aggregator that calls bridgeBack twice in one transaction emits two events with the same hash. The second release reverts on credited[txHash] and that user's burn is never returned on Khromosome.

Fix: assignReleaseKeys() gives the first event per tx the legacy key, so releases already credited are still recognised, and gives each later event keccak256(abi.encode(txHash, ordinal)).

Static analysis triage (Slither, High/Medium)

Slither reported 3 High and 20 Medium results. Every one was reviewed against the source, and all are false positives:

Detector Location Verdict
weak-prng KhromeNames._premium False positive — elapsed % 86400 is the in-day linear interpolation of the premium decay, not randomness.
arbitrary-send-eth KhromeNodeLicense._pay False positive — only called to refund a license's own recorded purchase price to its holder, or to send proceeds to treasury after finalize().
uninitialized-state OperatorRegistry._activeByJurisdiction False positive — a mapping; default-empty is the intended initial state.
reentrancy-no-eth KhromeswapPair.burn/swap False positive — both are under the Uniswap-V2 lock modifier; ordering matches upstream V2.
reentrancy-no-eth KhromeswapFactory.createPair False positive — the only external call is initialize on the pair the factory just deployed via CREATE2.
incorrect-equality (13) sentinels (== 0 on timestamps, amounts, supply) False positive — existence/zero checks, not balance comparisons that an attacker can manipulate.
uninitialized-local (2) cum, owed accumulators False positive — Solidity zero-initialises locals; used as running sums.
unused-return (2) ECDSA.tryRecover third value; createPair return in router False positive — the error enum is checked; the router re-reads the pair address from the factory.

Low/informational results (timestamp use, calls-in-loop in owner-only batch functions, low-level calls with checked returns) were reviewed and accepted.

Known limitations carried to the external audit