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.
- Date: 2026-10-04
- Branch / base commit:
feat/revenue-raise@3d7b592(fixes committed on top) - Scope: every contract under
contracts/src/(2,831 nSLOC) plus the bridge relayersnode/relayer/relay.js(live) andrelayer/index.mjs(alternate). - Test suite after fixes: 446 Foundry tests passing (33 suites), plus the relayer
node --testsuites (2 + 2).
Method
- Manual review, prioritising code added in this cycle:
src/sale/*,src/names/*, the KhromeBridge fee path and the Khromeswap_mintFeechange, then every other contract. - Static analysis: Slither 0.11.6 using the repository's
slither.config.json. The config itself was broken:detectors_to_runwas 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. - A failing test first for every confirmed issue, then the fix, then the full
suite green. Regressions live in
contracts/test/security/InternalAudit202610.t.solandnode/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
MonetaryControllermonotonicity (I6) is lemma-backed, not fully machine-closed (seedocs/l1/12-audit-prep.md).- The bridge is relayer-trusted (single hot relayer key). Moving to a threshold or light-client design is a roadmap item; the audit should assess the current trust model as stated.
- The sale contracts encode counsel-dependent choices (Reg S clock anchoring,
Rule 144 gating, sanctions freeze behaviour). See
docs/raise/counsel-and-entity/counsel-intake-email.md. - Admin keys are not yet behind Safes and the 48-hour timelock on the live beta
chain (
docs/l1/15-launch-execution.md).