NullNet
Audit date ·
Audits are prepared by the site owner with a self-hosted (local) AI, checked against the exact commit above, and hand-edited before publishing. This is not financial, legal, or investment advice. Read it as one person's notes on one commit at one date. Do your own research.
This is a point-in-time review of code quality and tokenomics at the commit above, not a safety certification. Projects can change after this date. Check for newer releases or a newer audit before relying on these scores.
Shielded-only ZK L1 in Rust; issuance is a pure function of height and proven by the binding signature. A 3% premine sits under one key with no timelock or multisig.
🪙 NULLNET (NULL) — Code & Tokenomics Audit
Repo: NullNetProtocol/null-chain at commit 37a77c47edb7db9a45040089ac3b18963c377ade
Date: 2026-10-19 Scope: recon, code review (subsidy, issuance enforcement, coinbase balance, block validation, wallet), tokenomics. The circuit was read as a statement, not cryptanalytically verified. No chain is live, so on-chain supply is unverified.
🎯 Verdict: MIXED (risk 7.0)
NullNet is a shielded-only ZK chain written from scratch in Rust, and it is the most disciplined codebase in this set. Issuance is a pure function of height, the validator recomputes the coinbase balance from subsidy(height) + fees and the binding signature must prove exactly that number, so a coinbase cannot claim more or less than the rule allows. There is no unsafe in the consensus crates, arithmetic is checked everywhere, CI runs on three platforms and the release workflow builds the shipped artifacts with SHA-256 sums. It lands in MIXED for two things: a 3 % premine (630,000 coins) paid to a development fund held by a single key with no timelock and no multisig — the project says so itself — and the fact that on a shielded chain the supply cap cannot be observed, only enforced per transaction. This scores code and tokenomics only.
🔑 At a glance
| Field | Value |
|---|---|
| Project | NullNet (NULL) |
| Chain / consensus | Own L1, Equihash (144,5) PoW, 120 s blocks, shielded-only Halo2 circuit |
| Code score | 9.0 / 10 |
| Tokenomics score | 7.0 / 10 |
| Risk score | 7.0 / 10 (the lower of the two) |
| Verdict | MIXED |
🔗 Links & sources
| What | Link | Status |
|---|---|---|
| 🏠 Homepage | not found | — |
| 💻 Source | https://github.com/NullNetProtocol/null-chain at 37a77c4 | ✅ cloned in this audit |
| 🔎 Explorer | not found | — |
| 📄 Whitepaper | not found | — |
| 📣 Announcement | not found | — |
| 💬 Socials | not found | — |
🧮 Risk score inputs
| # | Finding | file:line | Reachable | Deduction | Applies to |
|---|---|---|---|---|---|
| 1 | The circuit range-checks note values to 64 bits, which is wider than MAX_MONEY; the cap is enforced only by the per-transaction balance equation and by parsing, not in-circuit. On a shielded chain no node can observe total supply | crates/circuit/src/gadgets/range.rs:28-43, crates/protocol/src/amount.rs:14-33 | live | −0.5 | code |
| 2 | The genesis coinbase must be regenerated by hand at launch; every run produces a new proof and a new genesis hash that must be re-pinned. The project states that a launch which forgets to regenerate it would burn the premine | crates/chain/src/genesis.rs:51-58, docs/decisions.md:890-895 | live at launch | −0.5 | code |
| 3 | Premine 630,000 coins = 3 % of MAX_MONEY, paid by the genesis coinbase | crates/protocol/src/consensus.rs:47, docs/params.md:56 | live | −1.0 | tokenomics |
| 4 | The development fund is a live spending key with no timelock and no multisig; the project admits the code does not solve its custody | docs/decisions.md:894 | live | −2.0 | tokenomics |
Code deductions, one at a time:0.5 → 1.0
code_score = 10 − 1.0 = 9.0
Tokenomics deductions, one at a time:1.0 → 3.0
tokenomics_score = 10 − 3.0 = 7.0
risk_score = min(9.0, 7.0) = 7.0
Instant override check: none fired. All five checked — (1) no key, role or admin path can mint outside the height-determined schedule; the only value creation is the coinbase balance the validator recomputes; (2) nothing sends seed, key or wallet contents anywhere — no telemetry, no analytics, no outbound calls in the code; (3) no backdoor, killswitch or remote-code path found; (4) not a fork presented as original work — it is written from scratch, and the design sources (Zcash, Halo2, Poseidon, RedPallas) are named; (5) no doc claim about supply or privacy contradicted by the code — the emission arithmetic was recomputed and matches, and the privacy claims are stated with what leaks.
🔒 Code
Commit audited: 37a77c4
- ✅ ISSUANCE ENFORCED IN CONSENSUS —
subsidy(height)is a pure function of height (crates/protocol/src/consensus.rs:58-65), and the validator recomputescredit = subsidy(height) + feeson every block (crates/chain/src/validate.rs:295). The binding signature must prove exactly that balance (crates/protocol/src/validate.rs:56-60,queue_signaturesat:64-80). A coinbase cannot claim more, or less, than the rule allows. - ✅ MINT AUTHORITY — value is created only by the coinbase balance equation. There is no other path that increases supply.
- ✅ VALIDATION ORDER — header shape, timestamps, target, proof of work, transaction root, structure, anchors, nullifier freshness, then batched signatures and proofs, then the commitment root (
crates/chain/src/validate.rs:72-97). PoW is checked before anything expensive, so a forged block costs the sender real work first. - ✅ NULLIFIER UNIQUENESS — enforced inside a transaction (
crates/protocol/src/validate.rs:36-39), across a block (crates/chain/src/validate.rs:318-321), then against persistent state and atomically in storage. Therhochain closes the faerie-gold attack (docs/threat-model.md, Ownership). - ✅ ARITHMETIC —
checked_add,checked_mul,checked_shr,checked_negon every money path (amount.rs:28-33,consensus.rs:120-126,validate.rs:47-60). No silent overflow. - ✅ NO
unsafeIN THE CONSENSUS CRATES — one occurrence in the whole tree, and it is a comment (crates/crypto/src/error.rs:16). - ✅ CI AND RELEASE —
.github/workflows/ci.ymlruns fmt, clippy with-D warnings, tests and docs on Linux, macOS and Windows;release.ymlbuilds the shipped binaries on tag push and publishes SHA256SUMS.Cargo.lockpresent,bincodepinned to=1.3.3because consensus hashes cover its encoding. - ✅ TESTS COVER THE MONEY MATH —
consensus.rs:181-200asserts the halving and that emission plus premine stays under the cap;genesis.rs:102-124proves the embedded premine verifies for exactly the premine and fails for any other balance;fee_for_actions(usize::MAX)returns an error. - ✅ WALLET — BIP-39, Argon2id at 64 MiB / 3 passes / 1 lane (
crates/wallet/src/wallet.rs:73-77,:104), ChaCha20-Poly1305, spending key encrypted under a passphrase-derived key, new directories0700, token files0600(README.mdplatforms section). - ✅ NO TELEMETRY — the only URLs in the code are the node's own RPC bind address and a brand-assets link.
- ⚠️ LOW — the circuit proves values are below 2^64, not below
MAX_MONEY(range.rs:28-43). The cap therefore lives in the parser and in the per-transaction balance, not in the proof. On a shielded pool no node can sum the values, so the cap is unobservable — the threat model says this plainly. - ⚠️ LOW — the genesis is regenerated by hand and every run yields a new proof and a new hash that must be re-pinned (
genesis.rs:51-58). The project records the risk itself: a launch that forgot to regenerate would burn the premine. - ℹ️ Zero-value dummy notes can be spent without a membership proof by design; they carry no value and their nullifier is recorded, so they cannot be reused (
docs/threat-model.md). - ℹ️ The test network's premine pays a published seed phrase's key so anyone can fund a test wallet. Intentional, documented, testnet only.
📊 Tokenomics
| Item | Value | Evidence | Flag |
|---|---|---|---|
| Premine | 630,000 coins = 3 % of MAX_MONEY, paid by the genesis coinbase to the development fund | consensus.rs:47, docs/params.md:56, docs/decisions.md:868 | ⚠️ disclosed, 1–5 % band |
| Mint authority | none outside the coinbase balance the validator recomputes | validate.rs:295, protocol/validate.rs:56 | ✅ |
| Issuance enforced in consensus | yes — recomputed per block and proven by the binding signature | consensus.rs:58-65, validate.rs:295 | ✅ |
| Hard cap | 21,000,000 coins. Enforced by the balance equation and by parsing, not in-circuit | amount.rs:14, range.rs:28 | ⚠️ enforced but unobservable |
| Emission curve | 9.7 coins at height 1, halving every 1,050,000 blocks. Recomputed independently: 20,999,999.8845 coins, 0.1155 under the cap | consensus.rs:49-53, test consensus.rs:181-200 | ✅ |
| Docs vs code | params.md says emission plus premine "sums to MAX_MONEY"; the code sums to 0.1155 coins less. threat-model.md says "sum below MAX_MONEY", which is the accurate wording | params.md:50 vs threat-model.md:44 | ⚠️ loose wording, code is stricter |
| Dev or founder share | 0 % of the subsidy — no founders' output, no dev fee. The project rejected per-coinbase founder outputs and a private mining period | docs/decisions.md:870-878 | ✅ |
| Treasury | the premine is notes under one key, held offline. No timelock, no multisig. The project says custody is a key-management question the code does not solve | docs/decisions.md:894 | ❌ live spending key |
| Genesis reproducibility | yes — the coinbase bytes are committed in crates/chain/src/genesis/main.tx and the hash is pinned by a test (genesis.rs:80-88) | genesis.rs:80-88 | ✅ |
| Premine history | reduced from 5 % to 3 % on 2026-10-15, and the initial subsidy raised from 9.5 to 9.7 so the cap still holds | docs/decisions.md:901-910 | ✅ |
🕵️ Privacy
- ✅ Shielded-only. There are no transparent addresses, so there is no transparent pool to leak.
- ✅ Dandelion++ for transaction relay, encrypted connections (
README.md). - ✅ The wallet file holds the spending key encrypted; the witness tree and scanned heights, which are public chain data, are stored in the clear (
crates/wallet/src/lib.rs:11-13). - ✅ The threat model states what leaks per transaction: the action count (2, 4, 8 or 16) and the nullifier set. It does not claim anonymity beyond that.
- ⚠️ The development fund's notes are shielded, so after genesis the fund cannot be audited. The project acknowledges this as the reason the premine is small.
- ⚠️ The site also advertises disposable email, which is not in this repository. That is a separate service and was not audited here.
🚩 Red flags
- ❌ 3 % premine to a single-key development fund, no timelock, no multisig (
docs/decisions.md:894). - ❌ The supply cap is not proven in the circuit; it is only checked by the parser and the per-transaction balance (
range.rs:28-43). - ❌ The circuit has no external audit; the project's own review is internal and calls itself "not an independent or formal cryptographic audit" (
docs/audit.md:7). - ❌ The genesis must be regenerated by hand at launch and re-pinned (
genesis.rs:51-58). - ❌ Single author, 23 commits, no checkpoints, no DNS seeds, no live chain.
✅ Green flags
- ✅ The validator recomputes the coinbase balance and the binding signature must prove exactly it (
validate.rs:295,protocol/validate.rs:56). - ✅ A test proves the embedded premine verifies for exactly the premine and fails for any other balance (
genesis.rs:115-121). - ✅ Nullifier uniqueness enforced at three levels, and the
rhochain closes the faerie-gold attack (threat-model.md). - ✅ Every signature binds the consensus branch id, so neither side of a fork can replay the other's transactions (
docs/upgrades.md:15-30). - ✅ No
unsafein the consensus crates; checked arithmetic on every money path. - ✅ CI on three platforms plus a release workflow that builds the shipped artifacts with SHA-256 sums.
- ✅
bincodepinned to=1.3.3with golden vectors, because consensus hashes cover its encoding. - ✅ Argon2id at 64 MiB / 3 passes, matching OWASP guidance;
0700directories and0600token files. - ✅ The docs are honest about what is not proven: circuit soundness is named as the largest residual monetary risk (
docs/audit.md:37,:779). - ✅ The premine was cut from 5 % to 3 % rather than kept, and the placeholder address whose key was discarded was replaced with a real one (
docs/decisions.md:901-910).
📋 Context (does not affect the score)
| Signal | Value |
|---|---|
| Contributors / commits / repo age | 1 author (NullNetDev <[email protected]>), 23 commits in the depth-50 clone, repo created 2026-09-28 |
| AI-assisted commits | the repo carries a CLAUDE.md engineering-rules file; the decisions log reads as AI-assisted. Not deducted |
| Relaunch, rename or fork lineage | none; written from scratch, sources named |
| Independent audit | no — docs/audit.md is an internal review, self-labelled as not independent |
| Launch-stage items not deducted | no checkpoints (params.md:33), no DNS seeds, five unnamed seeds, no listing, no liquidity, no live chain, single author — recorded, not scored |
| Public RPC or explorer reachable | no chain yet |
| Market data | not relevant at this stage |
| Hashrate, power cost, profitability | out of scope |
❓ What I could not check
- 🔍 Circuit soundness. Only a formal verification or an external audit closes it; the project names this as the largest residual risk.
- 🔍 On-chain supply — no chain exists. There is no supply endpoint, so it can never be observed on a shielded chain; only the per-transaction balance can be checked.
- 🔍 The difficulty rule's tolerance for a 7,200 s max future drift on 120 s blocks was not read in detail. Check
crates/chain/src/difficulty.rsagainst LWMA timestamp-manipulation literature. - 🔍 Custody of the development fund — the code does not implement it, so it cannot be verified.
🧭 Bottom line
The best-engineered money path in this group: issuance is a pure function of height, recomputed inside validation, proven by the binding signature, and the premine is tested to pay exactly its stated amount. What keeps it in MIXED is that a shielded chain cannot show its supply, and that 3 % of it sits under one key with no timelock or multisig. To reach MIXED in the scored areas: add the MAX_MONEY bound as a circuit constraint so the cap is proven rather than parsed, put the development fund under a timelock or a multisig, and get the circuit formally verified. The project already lists all three as launch requirements.
Files we opened
crates/protocol/src/consensus.rs:58-65
crates/chain/src/validate.rs:295
crates/protocol/src/validate.rs:56-60
crates/circuit/src/gadgets/range.rs:28-43
crates/protocol/src/amount.rs:14-33
crates/chain/src/genesis.rs:51-58
docs/decisions.md:894
crates/chain/src/validate.rs:72-97
crates/protocol/src/consensus.rs:181-200
crates/wallet/src/wallet.rs:73-77
Every file listed here was opened by us during the audit. Nothing here is a guess.