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.