Skip to main content
For the complete documentation index, see llms.txt

OpenZeppelin contracts for Compact

OpenZeppelin Contracts for Compact is a library of reusable smart contract building blocks written in Compact for Midnight. It mirrors the structure of OpenZeppelin's Solidity contracts library, providing token standards, access-control patterns, and security primitives that compose into custom contracts.

The library is modular: instead of inheriting a base contract, you import individual modules into your own .compact source with a prefix and call their circuits as building blocks.

Experimental, not for production

The library is currently at version 0.2.0. The project's own README calls the code highly experimental, and the maintainers explicitly state: DO NOT USE IT IN PRODUCTION. The repository's audits folder holds OpenZeppelin's audit report for version 0.1.0. Version 0.2.0 includes changes made after that audit.

Use these contracts for prototyping, learning, and development on Preview and Preprod only.

Prerequisites​

Before using the library, ensure that you have:

Compatibility​

The table below lists the Compact compiler and language versions the library targets. Verify these against your local toolchain before integrating.

ComponentVersionNotes
Library version0.2.0The release that yarn add @openzeppelin/compact-contracts installs. Rapid iteration expected.
Compact compiler0.29.0Pinned by the library's CI badge.
Compact language>= 0.21.0Per pragma in every source file.
Token amountsUint<128> (not Uint<256>)Midnight encoding limit; Uint<256> is not supported by the Compact compiler.
Midnight NetworkPreprod and Preview onlyNot for Mainnet.
Compiler version mismatch

Version 0.2.0 needs compiler 0.29.0 or later. OpenZeppelin builds and tests it with 0.29.0, while the official Midnight Toolchain has shipped newer releases. The pragma >= 0.21.0 keeps source-level compatibility: the example on this page also compiles with compilers 0.31.1, 0.34.0, and 0.35.0.

The compiler also decides which ledger version the compiled contract targets. Compiler 0.29.0 targets ledger version 7, and 0.31.1 targets ledger version 8. Compilers 0.34.0 and 0.35.0 target ledger version 9. To deploy, compile with the toolchain that the compatibility matrix lists for your target network. Verify your local compact compile --version before reporting issues.

The npm beta tag and the repository's main branch carry prereleases that target compiler 0.34.0 and pragma language_version >= 0.26.0. This page does not cover them.

Modules​

The library organizes its modules into four families. Most modules import Utils (zero-address and Either helpers) for composition.

Access control · access/​

These modules control who can call specific circuits in your contract. They are listed from simplest to most complex: single-owner first, then role-based, then their privacy-preserving variants.

ModulePurposeOpenZeppelin docs
OwnableSingle-owner authorization. Stores the owner as Either<Bytes<32>, ContractAddress>. Supports ownership transfer, renunciation, and safe/unsafe variants for contract-address owners.Guide · API
AccessControlRole-based authorization with admin hierarchy. Roles are Bytes<32> identifiers. Each role has a configurable admin role that controls grants and revocations.Guide · API
ShieldedAccessControlPrivacy-preserving role-based access control. Stores role commitments in a MerkleTree<20> and revokes via nullifiers, so role membership can be proven in zero knowledge.Guide · API
ZOwnablePKPrivacy-preserving single-owner authorization. Stores the owner as a commitment hash instead of a public key, so on-chain data does not reveal who the owner is.Documented alongside Ownable

Security · security/​

These modules enforce lifecycle and operational guards on your contract.

ModulePurposeOpenZeppelin docs
InitializableOne-time initialization guard. Stops a contract from initializing more than once. No other module in the library imports it: modules that need initialization track their own flag. OpenZeppelin advises using it only in your top-level contract.Guide · API
PausableEmergency pause switch. Gates circuit execution behind assertNotPaused(). Wrap _pause() and _unpause() in your own authorization logic.Guide · API

Tokens · token/​

These modules implement standard token interfaces adapted for Compact.

ModulePurposeOpenZeppelin docs
FungibleTokenERC-20-style fungible token. Supports transfer, approve, transferFrom, _mint, and _burn. Balances use Uint<128> (not Uint<256> - see Compatibility). Includes safe and unsafe transfer variants.Guide · API
NonFungibleTokenERC-721-style non-fungible token. Tracks ownership, approvals, operator approvals, and token URIs. Token IDs use Uint<128>.Guide · API
MultiTokenERC-1155-style multi-token. Manages multiple token types in a single contract. Does not support batched operations (Compact lacks dynamic arrays).Guide · API

Utilities · utils/​

Helper functions for type safety and input validation.

ModulePurposeOpenZeppelin docs
UtilsPure helpers for working with Either<ZswapCoinPublicKey, ContractAddress> values. Includes zero-checks, equality checks, and canonicalize which prevents crafted-input attacks.Guide · API

Installation​

The library ships as the npm package @openzeppelin/compact-contracts. The latest tag installs version 0.2.0, the release this page describes.

To add the library to a new project, create the project and install the package:

mkdir my-project && cd my-project
yarn init -y
yarn add @openzeppelin/compact-contracts

The package contains the Compact source of each module in the access/, security/, token/, and utils/ folders.

Yarn Plug'n'Play

The imports in your contract point to files in node_modules. Yarn 2 and later use Plug'n'Play by default, which does not create that folder. If your project has no node_modules folder after the install, run yarn config set nodeLinker node-modules, then run yarn install. Alternatively, install the package with npm install @openzeppelin/compact-contracts.

Compose a contract​

Import individual modules into your .compact source with a prefix, then call their circuits as building blocks. Note the prefix Pausable_; convention: the prefix itself ends in _. Calling the internally underscored _pause circuit becomes Pausable__pause() - the double underscore is intentional.

pragma language_version >= 0.21.0;

import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"
prefix Ownable_;
import "./node_modules/@openzeppelin/compact-contracts/security/Pausable"
prefix Pausable_;
import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"
prefix FungibleToken_;

constructor(
_name: Opaque<"string">,
_symbol: Opaque<"string">,
_decimals: Uint<8>,
_recipient: Either<Bytes<32>, ContractAddress>,
_amount: Uint<128>,
_initOwner: Either<Bytes<32>, ContractAddress>,
) {
Ownable_initialize(_initOwner);
FungibleToken_initialize(_name, _symbol, _decimals);
FungibleToken__mint(_recipient, _amount);
}

export circuit transfer(
to: Either<Bytes<32>, ContractAddress>,
value: Uint<128>,
): Boolean {
Pausable_assertNotPaused();
return FungibleToken_transfer(to, value);
}

export circuit pause(): [] {
Ownable_assertOnlyOwner();
Pausable__pause();
}

export circuit unpause(): [] {
Ownable_assertOnlyOwner();
Pausable__unpause();
}

export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
return Ownable_computeAccountId(secretKey);
}
Account identifiers and witnesses

The Bytes<32> side of each address holds an account identifier, not a wallet public key. Ownable and FungibleToken derive it as a persistentHash of the caller's secret key, and each exports a pure computeAccountId circuit that computes it. The example wraps the one from Ownable in its own computeAccountId circuit. Your DApp can call pureCircuits.computeAccountId(secretKey) from the compiled output to get the identifiers that the constructor and transfer take. The compiled contract declares the witnesses wit_OwnableSK and wit_FungibleTokenSK, which your DApp implements to return that 32-byte secret key. The package does not include witness implementations. For details, see OpenZeppelin's Ownable guide.

Compile​

Run the Compact compiler to generate TypeScript bindings and ZK circuit artifacts from your contract:

compact compile MyContract.compact artifacts/MyContract

A successful compile lists each circuit with its proving-system size. This output comes from compiler 0.31.1, and row counts differ between compiler versions:

Compiling 3 circuits:
circuit "pause" (k=13, rows=2362)
circuit "transfer" (k=13, rows=3855)
circuit "unpause" (k=13, rows=2359)
Overall progress [====================] 3/3

The pure computeAccountId circuit needs no proof, so the output does not list it.

k is the domain size (the circuit is laid out on 2^k rows), and rows is how many of those rows the circuit uses. Smaller circuits prove and verify faster - see OpenZeppelin's ZK Circuits 101 for a longer explanation.

Patterns to know​

These patterns apply across the library and affect how you structure your contracts.

  • Modular composition by delegation: OpenZeppelin's Module/Contract pattern is the formal framing the library expects you to follow. Each module exports two kinds of circuits: external ones with no leading underscore (like transfer, approve) that are safe to expose as-is, and public ones with a leading underscore (like _mint, _burn) that serve as composable building blocks you wrap in your own contract logic. Internal helpers remain unexported. Your contract imports modules with a prefix and wires their circuits together; it should not call initialize() outside its constructor or re-export circuits raw.

  • Composition through prefixes: The import ... prefix X_; syntax namespaces each module so multiple modules can coexist in a single contract without identifier collisions. Circuits whose names start with _ show up as X__name at the call site.

  • Initialization is explicit: Ownable, ZOwnablePK, ShieldedAccessControl, and the three token modules each track their own initialization flag. Each requires an explicit initialize call from your constructor. Until that call runs, their guarded circuits fail at runtime with a contract not initialized error.

  • Safe circuits reject contract-address recipients: Version 0.2.0 targets compiler 0.29.0, which predates cross-contract calls (they arrived with ledger version 9). Every module's safe path (e.g., Ownable.transferOwnership, AccessControl.grantRole, FungibleToken.transfer) therefore rejects ContractAddress recipients. Each module pairs the safe variant with _unsafe* circuits explicitly named to make the risk visible. The maintainers plan to deprecate the unsafe variants once the library supports cross-contract calls.

  • Caller authorization comes in two flavors: Ownable, AccessControl, and the token modules identify the caller by an account identifier. It is a persistentHash of a secret key that each module reads from its own witness, such as wit_OwnableSK. They do not rely on ownPublicKey(), in line with the derived-identity pattern. Unlike the example in that guide, the hash has no domain separator. Neither uses a per-contract salt, so the identifier is the same in every contract that uses the same key. Use a different key for each contract if the identities must not be linkable. ZOwnablePK and ShieldedAccessControl use a witness-derived secret combined with hashing and instance salts to verify the caller via commitments - never exposing the public key on-chain. ZOwnablePK still hashes ownPublicKey() into its owner identifier, so the secret nonce from its wit_secretNonce witness is what authenticates the caller. The privacy-preserving variants are the recommended choice when the contract should not reveal who its owners or role-holders are.

  • Either<Bytes<32>, ContractAddress> is the canonical address type: The Utils.canonicalize helper zeroes out the unused side of an Either to prevent crafted-input attacks where both sides carry data.

Resources​

For more information, see the following links.

Reporting issues​

For issues with the library code, file on OpenZeppelin's tracker. For security disclosures, email security@openzeppelin.com.

For issues with this documentation page, file on the Midnight docs repository.