N-06 Lack Of Two-Step Ownership Transfer Modules
Severity: Note
Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)
ZOwnablePK implements ownership by storing an _ownerCommitment and requiring privileged circuits to call assertOnlyOwner. This function recomputes an owner identifier from ownPublicKey() and the witness wit_secretNonce(), and compares it against that commitment. A valid identifier is therefore a hash preimage, since _computeOwnerId returns persistentHash over the public key and the nonce.
Ownership transfer is a single step. transferOwnership accepts any newOwnerId other than the type default and commits it, with no step in which the recipient demonstrates that a preimage exists. For an arbitrary 32-byte value, no such preimage is findable, so once this value is committed, no caller can satisfy assertOnlyOwner and every owner-gated circuit becomes permanently unreachable. This includes the two circuits that might otherwise recover the situation, since a further transferOwnership and renounceOwnership both begin by calling assertOnlyOwner. Similarly, Ownable.compact too implements the single-step ownership transfer.
Consider implementing a two-step transfer module in which transferOwnership records a pending identifier and a separate acceptance circuit requires the recipient to prove knowledge of a public key and secret nonce reproducing it before _ownerCommitment is updated, similar to the Ownable and Ownable2Step contracts of the OpenZeppelin Solidity Library.
N-06 Lack Of Two-Step Ownership Transfer Modules
Severity: Note
Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)
ZOwnablePKimplements ownership by storing an_ownerCommitmentand requiring privileged circuits to callassertOnlyOwner. This function recomputes an owner identifier fromownPublicKey()and the witnesswit_secretNonce(), and compares it against that commitment. A valid identifier is therefore a hash preimage, since_computeOwnerIdreturnspersistentHashover the public key and the nonce.Ownership transfer is a single step.
transferOwnershipaccepts anynewOwnerIdother than the type default and commits it, with no step in which the recipient demonstrates that a preimage exists. For an arbitrary 32-byte value, no such preimage is findable, so once this value is committed, no caller can satisfyassertOnlyOwnerand every owner-gated circuit becomes permanently unreachable. This includes the two circuits that might otherwise recover the situation, since a furthertransferOwnershipandrenounceOwnershipboth begin by callingassertOnlyOwner. Similarly,Ownable.compacttoo implements the single-step ownership transfer.Consider implementing a two-step transfer module in which
transferOwnershiprecords a pending identifier and a separate acceptance circuit requires the recipient to prove knowledge of a public key and secret nonce reproducing it before_ownerCommitmentis updated, similar to the Ownable and Ownable2Step contracts of the OpenZeppelin Solidity Library.