N-08 Owner Commitments Are Not Truly Distinct Over Multiple Contracts
Severity: Note
Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)
Ownership in ZOwnablePK is represented by a public commitment, returned by owner() and derived by _computeOwnerCommitment as a persistentHash over the owner identifier, the _instanceSalt, a counter and a fixed domain tag. Of those four inputs only the salt varies between deployments, the tag being a constant, so the salt alone provides the per-instance namespace the module documents it as providing.
initialize rejects a zero owner identifier but persists the salt without validating it. Two deployments that share an owner identifier and are both initialized with the type default therefore publish identical commitments at the same counter value, and an observer can correlate administrative identity across those contracts by comparing a public getter. Authorization behavior is unchanged, since the commitment still requires a preimage to satisfy the owner check, so the consequence is the loss of the unlinkability the salt exists to provide.
Consider rejecting a salt equal to the type default in initialize, matching the check already applied to the owner identifier on the line above. Consider also folding a contract-unique value such as kernel.self() into the commitment, so that deployments remain namespaced even where a consumer supplies a poor salt.
N-08 Owner Commitments Are Not Truly Distinct Over Multiple Contracts
Severity: Note
Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)
Ownership in
ZOwnablePKis represented by a public commitment, returned byowner()and derived by_computeOwnerCommitmentas apersistentHashover the owner identifier, the_instanceSalt, a counter and a fixed domain tag. Of those four inputs only the salt varies between deployments, the tag being a constant, so the salt alone provides the per-instance namespace the module documents it as providing.initializerejects a zero owner identifier but persists the salt without validating it. Two deployments that share an owner identifier and are both initialized with the type default therefore publish identical commitments at the same counter value, and an observer can correlate administrative identity across those contracts by comparing a public getter. Authorization behavior is unchanged, since the commitment still requires a preimage to satisfy the owner check, so the consequence is the loss of the unlinkability the salt exists to provide.Consider rejecting a salt equal to the type default in
initialize, matching the check already applied to the owner identifier on the line above. Consider also folding a contract-unique value such askernel.self()into the commitment, so that deployments remain namespaced even where a consumer supplies a poor salt.