A privacy-preserving, regulation-aware stablecoin built in Leo for the Aleo network. The system implements a fungible token (USDCx) that supports both public and private transfers while retaining the compliance controls a regulated issuer needs: role-based administration, a freeze/deny list, an investigator audit trail, a pausable transfer switch, and a Circle-CCTP-style bridge for cross-chain minting and burning. Program upgrades are gated behind a reusable k-of-n multisig.
Privacy and compliance are usually at odds. This design reconciles them with zero-knowledge Merkle proofs: a user can prove their address is not on the freeze list without revealing which address they are, and every private transfer simultaneously emits an encrypted ComplianceRecord that only a designated investigator can read.
Developed together with Sealance Technology by Forte.
The system is composed of five Leo programs. Each is independently deployable, and dependencies are wired through program.json. The arrows below show the import/dependency direction.
merkle_tree.aleo
▲
│ (non-inclusion proofs)
│
multisig_core.aleo ◄── freezelist_program.aleo
▲ ▲ ▲
│ │ │
│ └──── stablecoin_program.aleo (USDCx)
│ ▲
│ │
└──────── stablecoin_bridge.aleo
| Program | Purpose | Dependencies | License |
|---|---|---|---|
merkle_tree.aleo |
Reusable Merkle inclusion / non-inclusion proof verification. | — | MIT |
multisig_core.aleo |
k-of-n multisig supporting both Aleo and ECDSA (Ethereum) signers; governs program upgrades. | — | GPL |
freezelist_program.aleo |
Compliance freeze/deny list with a public mapping and a Merkle root for private checks. | merkle_tree, multisig_core |
GPL |
stablecoin_program.aleo |
The USDCx token: public + private balances, roles, minting/burning, transfers, allowances, pausing. | freezelist_program, multisig_core |
GPL |
stablecoin_bridge.aleo |
Mints / burns USDCx against Circle CCTP-style attestations bridged from Ethereum. | stablecoin_program, freezelist_program, multisig_core |
GPL |
A small, dependency-free library of Merkle-proof primitives shared across the system.
verify_inclusion(addr, proof)— proves an address is a leaf in the tree and returns the computed root.verify_non_inclusion(addr, [proof; 2])— proves an address is absent from a tree whose leaves are sorted ascending by address-as-field. It takes the two adjacent leaves that bracket the missing address and asserts the target falls strictly between them (with dedicated handling for the left-most and right-most edges).- Hashing uses Poseidon4, with a domain-separation prefix (
0fieldfor internal nodes,1fieldfor leaves) to prevent second-preimage attacks across tree levels. MAX_TREE_DEPTH = 15, so proofs carry up to 16 sibling entries. A0fieldsentinel in the siblings array marks the end of a shorter path.
This program is marked @noupgrade — it is immutable once deployed.
A general-purpose, reusable k-of-n multisig that the other programs rely on to authorize upgrades. Notable properties:
- Mixed signer types. A wallet can mix native Aleo signers and ECDSA/Ethereum signers (up to four of each). ECDSA signatures are verified on-chain against an EIP-191 (
"\x19Ethereum Signed Message:\n32") digest, with a per-operation nonce to prevent replay. - Signing operations. A signing op is identified by
hash(wallet_id, signing_op_id). Signers confirm independently until the wallet threshold is reached, at which point the op is recorded incompleted_signing_ops. Operations carry an expiry (in blocks) and a round counter so an expiredsigning_op_idcan be safely reused without old signatures leaking into the new round. - Admin operations. Change-threshold, add-signer, and remove-signer are themselves multisig-gated operations.
- Upgrade governance.
ProgramSettingsholds anallow_upgradeskill-switch and anupgrader_address. Other programs checkcompleted_signing_opsin theirconstructorbefore permitting an upgrade (see below). - Wallet-creation guard. Optionally (
guard_create_wallet), creating new wallets itself requires a completed multisig op, so the public cannot squat wallet IDs the deployer cares about.
This program is adapted from a general-purpose multisig implementation; its own
README.mdreferenced in the source header describes the standalone scheme in more depth.
The compliance backbone. It maintains the set of frozen/denied accounts in two complementary forms:
- A public mapping
freeze_list: address => boolfor transparent, public-transfer checks. - A Merkle root (
freeze_list_root) over the sorted set of frozen addresses, enabling private non-inclusion proofs — a user proves they are not frozen without revealing their address.
Key mechanics:
- Root rotation window. When the list changes, the current root becomes the previous root and
root_updated_heightis stamped. A configurableblock_height_windowkeeps the previous root valid for a grace period, so in-flight transactions built against the old root don't fail during an update. update_freeze_list(...)adds or removes an account, maintaining a packed index (freeze_list_index) and guarding against double-freezing, double-unfreezing, and accidental root overwrites (it requires the caller to pass the expectedprevious_root).verify_non_inclusion_pub/verify_non_inclusion_privexpose public and private compliance checks for other programs and clients.- Access is role-gated: a
MANAGER_ROLEassigns roles, and aFREEZELIST_MANAGER_ROLEmutates the list and the block-height window.
The token contract. It implements an ERC-20-style public interface plus Aleo-native private records.
State & metadata
TokenInfoholds name, symbol, decimals, supply, and a hardmax_supplycap enforced on every mint.- Public balances live in
balances: address => u128; allowances inallowanceskeyed byhash(account, spender). - Private holdings are
Tokenrecords (owner,amount), splittable and joinable viasplit/join.
Roles (bit-masked, so one address can hold several)
MINTER_ROLE,BURNER_ROLE,PAUSE_ROLE,MANAGER_ROLE. The manager assigns roles viaupdate_role; a manager cannot strip their own manager bit by accident.
Lifecycle & transfers
initialize(deployer-only, once) sets metadata and the initial admin.- Mint / burn:
mint_public,mint_private,burn_public,burn_private. - Public transfers:
transfer_public,transfer_public_as_signer,transfer_from_public(+approve_public/unapprove_public). - Cross-visibility transfers:
transfer_public_to_private,transfer_from_public_to_private,transfer_private,transfer_private_to_public. set_pause_status(pause-role) halts all transfers in an emergency; every transfer/mint/burn checks the pause flag.
Compliance integration
- Every public transfer checks both sender and recipient against the public freeze list.
- Every transfer that touches private records verifies the sender's non-inclusion in the freeze-list Merkle tree (accepting the current or recently-rotated previous root).
- Every private movement emits a
ComplianceRecordowned by theINVESTIGATOR_ADDRESS, recording amount, sender, and recipient — an encrypted audit trail readable only by the investigator. get_credentialsissues a reusableCredentialsrecord proving freeze-list non-inclusion, so a user can make repeated private transfers (transfer_private_with_creds) without re-proving each time.
Bridges USDCx to/from an external chain using Circle's CCTP-style attestation format.
mint_private/mint_publicparse a 240-byte deposit payload, validate a magic value, version, domain, non-zero token & recipient addresses, and amount, then verify a Circle attester ECDSA signature over the Keccak-256 digest of the payload before minting. Anullifiermapping prevents replaying the same deposit.- Minting also enforces the freeze list (private Merkle non-inclusion for private mints, the public mapping for public mints) and the bridge pause switch.
burn_publicburns USDCx and records the native destination domain + recipient for Circle compliance, subject to a configurableminimum_burn_amount. (The legacyburnentrypoint is intentionally disabled with an always-failing assertion.)- Admin keys:
USDCX_PAUSE_KEYcontrols pausing;USDCX_MANAGER_KEYsets the Circle attester and the minimum burn amount.
Every upgradeable program (stablecoin_program, freezelist_program, stablecoin_bridge) uses the same pattern in its constructor:
- Edition 0 (initial deployment): no checks — the program deploys freely.
- Edition > 0 (upgrade): the program derives
signing_op_id = hash(checksum, edition)and requires a completed multisig operation for it inmultisig_core.aleo/completed_signing_ops. Binding the signing op to both the code checksum and the edition number means an upgrade must be explicitly approved and cannot be downgraded or replayed against a different build.
merkle_tree.aleo is @noupgrade and multisig_core.aleo governs its own upgrades through its ProgramSettings.
| Capability | How it's achieved |
|---|---|
| Private balances & transfers | Aleo Token records; amounts and owners stay off the public ledger. |
| "Prove I'm not sanctioned" without doxxing | Zero-knowledge Merkle non-inclusion proof against the freeze-list root. |
| Regulator visibility into private flows | ComplianceRecord records encrypted to a fixed INVESTIGATOR_ADDRESS. |
| Sanctions enforcement on public flows | Direct freeze_list mapping checks on sender and recipient. |
| Emergency stop | pause flag honored by every value-moving transition. |
| Safe list updates | Previous-root grace window so in-flight proofs don't break mid-update. |
| Reusable compliance | Credentials record lets a vetted user transact privately without re-proving each time. |
.
├── merkle_tree/ # Merkle proof verification library (immutable)
├── multisig_core/ # k-of-n multisig (Aleo + ECDSA), upgrade governance
├── freezelist_program/ # Compliance freeze/deny list + Merkle root
├── stablecoin_program/ # USDCx token (public + private)
├── bridge_program/ # stablecoin_bridge.aleo — Circle CCTP-style bridge
├── LICENSE.md # GNU General Public License
└── README.md
Each program directory contains a program.json manifest and src/main.leo.
These programs target the Leo toolchain version 3.4.0.
# Install Leo (see https://docs.leo-lang.org for the latest instructions)
# Build a single program (e.g. the token), with its local dependencies resolved:
cd stablecoin_program
leo buildBecause of the dependency graph, programs must be deployed bottom-up:
merkle_tree.aleomultisig_core.aleofreezelist_program.aleostablecoin_program.aleostablecoin_bridge.aleo
Before deploying: the
DEPLOYER_ADDRESS/MULTISIG_DEPLOY_KEY/INVESTIGATOR_ADDRESSand the bridge'sUSDCX_*andETH_PUBLIC_KEYconstants are currently set to test values. Replace them with production keys before mainnet deployment. The multisig source notes this explicitly: "IMPORTANT! Remember to change this before deployment!"
- Arithmetic safety. Underflow/overflow on balances and supply is caught by the Aleo VM; the code relies on this (e.g. burning more than a balance reverts).
- Domain separation. Merkle hashing prefixes leaves vs. internal nodes; multisig hashes wrap IDs in verbosely-named structs to avoid cross-context collisions.
- Replay protection. The bridge uses a per-deposit nullifier; ECDSA multisig signatures use per-operation nonces.
- Sorted-tree assumption. Non-inclusion soundness depends on the freeze-list Merkle tree being correctly sorted ascending by address; off-chain tooling that builds the tree must preserve this invariant.
Unless noted otherwise in an individual program.json, the programs are released under the GNU General Public License (see LICENSE.md). merkle_tree.aleo is licensed under MIT.
Developed together with Sealance Technology by Forte.