This project is built on / for the Midnight Network, using the Compact smart-contract language.
A small, self-contained Compact contract: a permissioned (KYC-style) token where an owner administers an allowlist, and tokens can only be minted to — or transferred to — accounts that are on that allowlist.
The layout mirrors the eddalabs/midnight-contracts monorepo (a Turbo workspace per contract, with source, witnesses, an in-memory simulator, and a Vitest suite), so it can be read, run, and extended the same way — or dropped into that repo as a new workspace.
allowlist-token-contract/src/allowlist-token.compact implements:
| Circuit | Who can call it | Effect |
|---|---|---|
addToAllowlist(account) |
owner only | Marks an account as allowed to hold/receive tokens. |
removeFromAllowlist(account) |
owner only | Revokes that permission (existing balances are kept). |
mint(to, amount) |
owner only | Creates new tokens for an allowlisted account; bumps totalSupply. |
transfer(to, amount) |
any holder | Moves tokens to an allowlisted recipient. |
isAllowed(account) / balanceOf(account) |
anyone | Read-only views. |
accountId(secretKey) |
anyone (pure) | Off-chain helper to derive an account-id commitment. |
Compact has no implicit msg.sender. A caller proves who they are by knowing a secret key supplied
through the localSecretKey witness (private, never leaves the user's machine). The on-chain
identity is a commitment accountId = persistentHash([domain, secretKey]) — stable and pseudonymous, but it
never reveals the key. This is the same witness-based identity pattern used by the OpenZeppelin
Compact Ownable and AccessControl modules.
The "allowlist:accountid:v1" tag inside the hash is domain separation, and it is
load-bearing rather than decorative. Without it, the same secret key derives the identical
value in any other contract that hashes a key the same way, which would let an observer link
a holder here to their identity in an unrelated project. One tag per purpose per contract.
Midnight's smart contract security
page covers the general rule.
This is an unshielded token: balances, the allowlist, and the account-id commitments are all public ledger state. The only thing kept private is the secret key behind each identity. Value/owner privacy would require a shielded (Zswap) design, which this contract intentionally does not attempt.
-
Node.js v18+ (the upstream repo pins v23+) and npm.
-
Compact developer tools — the
compactcompiler. This workspace pins toolchain+0.31.0(the language pragma is>= 0.23). Install via Midnight's Compact Developer Tools guide and confirmcompactis on yourPATH:compact compile --version
You do not need Docker, a node, a wallet, or a proof server — the tests run entirely in memory.
npm install # install workspace deps
npm run compact # compile the .compact source -> src/managed/allowlist-token/
npm test # run the Vitest suite
# from inside the workspace you can also:
cd allowlist-token-contract
npm run compact-fast # compile with --skip-zk (faster while iterating)src/managed/ is generated build output and is gitignored — regenerate it with npm run compact.
allowlist-token-contract/
src/allowlist-token.compact the contract (the only hand-written Compact)
src/witnesses.ts private-state type + witness implementation
src/test/allowlist-token.test.ts Vitest suite
src/test/simulators/ in-memory CircuitContext harness
docs.md a longer walkthrough
See allowlist-token-contract/docs.md for a guided tour.
Built on / for Midnight with the Compact language. The workspace structure and the
in-memory simulator pattern follow eddalabs/midnight-contracts
(itself crediting the official midnightntwrk/example-counter). The witness-based identity and
access-control approach follows OpenZeppelin/compact-contracts.
Apache-2.0