Rule
duplicate-shielded-mint
Identical canonical shielded mint inputs produce a duplicate coin commitment in one transaction.
Specimen 05 of 96 · Audit-default ordinary rules
- Severity
- medium
- Confidence
- varies
- Maturity
- experimental
- Selection
- Audit default
- Policy intent
- not required
- Release
- v0.1.1
- Engine facts used
resolved-callscfgsuccessful-predicatescanonical-mint-observations
Selection and scope#
Selected by the default audit profile, so an ordinary scan runs it.
./.nightshade/node_modules/.bin/nightshade ./contracts --detectors duplicate-shielded-mint
./.nightshade/node_modules/.bin/nightshade detector show duplicate-shielded-mintThe first command runs only this rule; --detectors takes a comma-separated list. The second prints the rule's metadata from your installed version.
Severity is review priority. It does not express exploitability or impact. Confidence is detector metadata, not a probability. All built-in rules are experimental.
Compiler scope: the engine supports the pinned profiles 0.28.0, 0.29.0, 0.30.0, 0.31.1, 0.33.0-rc.2, 0.34.0, 0.35.0-rc.1 and 0.35.0, with ZKIR2 by default. Support for this rule can be narrower; the rule documentation below states any narrower scope.
Verified example#
Two mint calls use identical token, amount, nonce and recipient inputs on the same path. From the proof run of the released CLI; the examples page also has the lookalike and the files.
// Owned synthetic duplicate-mint regression.
import CompactStandardLibrary;
export circuit rewardTwice(nonce: Bytes<32>, recipient: ZswapCoinPublicKey): [] {
const to = left<ZswapCoinPublicKey,ContractAddress>(disclose(recipient));
mintShieldedToken(pad(32,"sdk:reward"),1,disclose(evolveNonce(0,nonce)),to);
mintShieldedToken(pad(32,"sdk:reward"),1,disclose(evolveNonce(0,nonce)),to);
}$ nightshade vulnerable.compact --config config.json --compiler /path/to/compactc
MEDIUM — Duplicate shielded mint commitment [duplicate-shielded-mint]
vulnerable.compact:7:3
7 | mintShieldedToken(pad(32,"sdk:reward"),1,disclose(evolveNonce(0,nonce)),to);
| ^
rewardTwice invokes mintShieldedToken twice with provably identical token domain, amount, nonce and active recipient on one modeled successful history. The pinned library derives the same coin commitment in this contract context; duplicate commitments are rejected by the ledger. This is a transaction availability defect, not proof of token theft or a generic commitment-opening vulnerability.
Source: vulnerable.compact:6:3
Call path: rewardTwice → mintShieldedToken
Remediation: Use a distinct nonce for each minted output, for example evolveNonce with a distinct index. Review freshness across separate transactions independently.
Semantic profile: compact-0.34.0-zkir2.
Baseline analysis: complete; selected rules: complete.
Rule coverage: 1 selected rule(s); 0 with incomplete entries.
1 finding(s), 0 suppressed, 0 baseline; 1/1 entrypoint(s) completed; analysis complete. Compiler validation passed.Corrected: Give the second mint a distinct nonce. Changed lines are marked.
// Owned synthetic duplicate-mint regression.
import CompactStandardLibrary;
export circuit rewardTwice(nonce: Bytes<32>, recipient: ZswapCoinPublicKey): [] {
const to = left<ZswapCoinPublicKey,ContractAddress>(disclose(recipient));
mintShieldedToken(pad(32,"sdk:reward"),1,disclose(evolveNonce(0,nonce)),to);
mintShieldedToken(pad(32,"sdk:reward"),1,disclose(evolveNonce(1,nonce)),to);
}$ nightshade corrected.compact --config config.json --compiler /path/to/compactc
Semantic profile: compact-0.34.0-zkir2.
Baseline analysis: complete; selected rules: complete.
Rule coverage: 1 selected rule(s); 0 with incomplete entries.
0 finding(s), 0 suppressed, 0 baseline; 1/1 entrypoint(s) completed; analysis complete. Compiler validation passed.The command line is shortened from the recorded run to what you would type; the output below it is verbatim, with absolute file paths shortened to the example folder. Long lines wrap. A quiet result shows that this rule stopped firing, not that the contract is secure.
Corpus checks#
Synthetic corpus checks for this rule in the 0.1.1 proof run. A check counts only with compiler acceptance and complete analysis.
| Positive detected | Corrected quiet | Lookalike quiet | Other negative quiet | Incomplete |
|---|---|---|---|---|
| 1/1 | 1/1 | 1/1 | 0/0 | 0 |
No deviations were recorded for this rule.
These are behavior checks on synthetic fixtures, not a precision or recall estimate. See evaluation.
Rule documentation#
From docs/detectors/duplicate-shielded-mint.md at v0.1.1.
Availability: default-on, medium severity, high confidence for operand equality.
Description, example and scenario#
mintShieldedToken(domain, 1, nonce, recipient);
mintShieldedToken(domain, 1, nonce, recipient);Two canonical library calls on the same modeled successful transaction history have provably identical token domain, amount, nonce and active recipient. The contract address is unchanged within that history. The library derives the same coin commitment, which the ledger rejects as a duplicate. This is a transaction availability defect; the rule does not establish theft or runtime feasibility.
Recommendation#
Use a different nonce for each minted output. evolveNonce derives nonces from an index and a prior nonce; use distinct indices for different outputs. Review freshness across separate transactions independently.
Evidence and known false positives#
Resolved canonical mint calls, immutable operand identities/constants and enforced successful-path equalities establish equality. Re-evaluating evolveNonce with identical inputs also establishes equality. A saved opaque computed nonce retains an observer-only immutable snapshot; separately computed unknown values do not become equal through shared dependencies. Inactive Either payloads are ignored; active tags and addresses are retained. Separate histories, rejected paths, independent unknown witnesses, changing ledger revisions, different amounts and domains, and application namesakes do not create duplicates.
Intentionally rejected test transactions may be review matches. Generic repeated persistentCommit openings are not diagnosed as a vulnerability. Cross-transaction nonce freshness, custom mint/commitment implementations and symbolic reasoning are outside scope. Unknown operands do not prove a duplicate; exhausted observation or equality budgets are rule/entry-scoped incomplete, preserve baseline findings and keep the overall scan incomplete.
Suppress a match#
If a match is intentional, suppress it on the line before with the exact rule ID and a reason of at least eight characters:
// nightshade-disable-next-line duplicate-shielded-mint -- Reason this match is intentionalDiagnostics cannot be suppressed, and an unmatched directive for a selected rule makes analysis incomplete. See configuration for fingerprint suppressions and baselines.