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 - alpha, not audited

The library is currently at version v0.0.1-alpha.1. Per the project's own README, it has never been audited or thoroughly reviewed for security vulnerabilities, and the maintainers explicitly state: DO NOT USE IT IN PRODUCTION.

Use these contracts for prototyping, learning, and Testnet development only.

Prerequisites​

Before using the library, ensure that you have:

Compatibility​

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

ComponentVersionNotes
Library versionv0.0.1-alpha.1Alpha; 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

OpenZeppelin's library currently pins compiler 0.29.0, while the official Midnight Toolchain has shipped a newer release. The pragma >= 0.21.0 keeps source-level compatibility, but verify your local compact compile --version before reporting issues.

Modules​

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

Access control · src/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<ZswapCoinPublicKey, 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.Not yet on OpenZeppelin's docs site - see source.
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 · src/security/​

These modules enforce lifecycle and operational guards on your contract.

ModulePurposeOpenZeppelin docs
InitializableOne-time initialization guard. Prevents a contract from being initialized more than once. Most other modules depend on this.Guide · API
PausableEmergency pause switch. Gates circuit execution behind assertNotPaused(). Wrap _pause() and _unpause() in your own authorization logic.Guide · API

Tokens · src/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 · src/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 currently ships as a Git submodule. The OpenZeppelin team plans to publish it on npm, as noted in the project's README.

To add the library to a new project, clone it as a submodule and compile the contracts:

mkdir my-project && cd my-project
git init
git submodule add https://github.com/OpenZeppelin/compact-contracts.git
nvm install
yarn
SKIP_ZK=true yarn compact

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 "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/access/Ownable"
prefix Ownable_;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/security/Pausable"
prefix Pausable_;
import "./compact-contracts/node_modules/@openzeppelin/compact-contracts/src/token/FungibleToken"
prefix FungibleToken_;

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

export circuit transfer(
to: Either<ZswapCoinPublicKey, 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();
}
Why import through node_modules

The OpenZeppelin team recommends importing through compact-contracts/node_modules/@openzeppelin/compact-contracts/... rather than the submodule path, to avoid state conflicts between shared dependencies. For details, see the project's README.

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:

Compiling 3 circuits:
circuit "pause" (k=10, rows=125)
circuit "transfer"(k=11, rows=1180)
circuit "unpause" (k=10, rows=121)
Overall progress [====================] 3/3

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: Most modules import Initializable and require an explicit initialize call from your constructor. Initializable_assertNotInitialized() and Initializable_assertInitialized() enforce this. Calling other circuits before initialize fails at runtime.

  • Contract-address recipients are restricted: Compact does not yet support contract-to-contract calls, so every module's safe path (e.g., Ownable.transferOwnership, AccessControl.grantRole, FungibleToken.transfer) 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 Compact supports contract-to-contract calls.

  • Caller authorization comes in two flavors: Ownable and AccessControl use ownPublicKey() directly to check the caller. 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. The privacy-preserving variants are the recommended choice when the contract should not reveal who its owners or role-holders are.

  • Either<ZswapCoinPublicKey, 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.