Introduction
On October 20th, 2025, zkSecurity started a security audit of Sealance’s compliant token system for the Aleo blockchain and a Circle unified reserve bridge transferring funds from the Ethereum network to the compliant Aleo token developed by Provable. The audit lasted one week with one consultant. We reviewed the compliant-transfer-aleo repository at commit 22d1159, as well as the bridge implementation in the CUR_Bridge repository at commit a14f51b.
Scope
We reviewed the following Leo programs implementing privacy-preserving compliance for token transfers on Aleo:
-
compliant_token_template.leo: A compliant token contract template with freeze list enforcement, role-based access control, and support for both public and private token balances. Includes privacy-preserving transfers using Merkle proofs for sanctions screening.
-
sealance_freezelist_registry.leo: A standalone freeze list registry managing sanctioned addresses with dual Merkle root system (current and previous) to enable graceful list updates without disrupting active transactions.
-
merkle_tree.leo: Cryptographic utility library providing Merkle tree operations including verification of inclusion and non-inclusion proofs using Poseidon hashing.
Additionally, we reviewed the Circle unified reserve cross-chain bridge implementation:
bridge_program/src/main.leo (from CUR_Bridge): A bridge contract enabling cross-chain token transfers between Ethereum and Aleo via Circle’s Cross-Chain Transfer Protocol. The bridge validates ECDSA-signed attestations from Circle, extracts deposit parameters from CCTP payloads, enforces freeze list checks, and mints bridged tokens on Aleo.
Overview
Compliant Token System
The compliant token contract provides a privacy-preserving token system on Aleo that enforces sanctions compliance through a freeze list mechanism. The system supports both public and private token balances, enabling four types of transfers:
- Public to public: Standard transparent transfers between public balances
- Public to private: Converts public balance to a private record
- Private to public: Converts a private record to public balance
- Private to private: Privacy-preserving transfers between records
The core compliance invariant is that frozen accounts cannot move funds. This is enforced differently depending on the transfer type. For public transfers, the freeze list is checked directly in the finalize function. For private transfers, the system uses privacy-preserving Merkle non-inclusion proofs to verify the sender is not on the freeze list without revealing the sender’s identity on-chain.
An important design choice is that private transfers can still send to frozen accounts. This represents a deliberate efficiency trade-off: preventing transfers to frozen addresses would require the sender to prove non-inclusion for the recipient as well, doubling the proof overhead. The policy assumes senders performing private transfers understand this risk.
Compliance records.
All private transfers emit a compliance record encrypted for a designated investigator address. These records contain the sender, recipient, and amount of each private transfer, providing an audit trail while maintaining privacy from the general public. Only the investigator with the corresponding private key can decrypt and view these records, enabling regulatory oversight without compromising user privacy.
Allowances and credentials.
The token system includes two additional mechanisms to improve usability. Allowances enable one address to spend tokens on behalf of another, following the standard ERC-20 pattern but adapted for both public and private balances. Credentials are reusable proof records that cache a valid freeze list root, allowing users to perform multiple private transfers without regenerating Merkle proofs for each transaction, significantly reducing the computational overhead for frequent users.
Access control.
The token contract implements a role-based access control system using bitwise flags. The primary roles include:
- Manager Role: Can grant or revoke any role to other addresses, including other managers
- Minter Role: Authorized to mint new tokens (public or private)
- Burner Role: Authorized to burn tokens from circulation
- Pause Role: Can pause token operations in emergency situations
Roles can be combined through bitwise OR operations, allowing a single address to hold multiple roles simultaneously. Token supply is managed through role-based mint and burn operations that respect the maximum supply limit. The token contract supports both mint_public (adding to public balance) and mint_private (creating private records), as well as burn_public (destroying public tokens) and burn_private (destroying private records).
Upgrades.
Both the token contract and bridge implement an edition-based upgrade mechanism. New versions can be deployed with incremented edition numbers, and the constructor enforces that only authorized addresses can deploy upgrades (editions > 0). This allows the contracts to evolve while maintaining security controls over who can publish new versions.
Freeze List and Root Rotation
The freeze list is maintained as an ordered Merkle tree of frozen addresses in sealance_freezelist_registry.leo. To allow smooth updates without disrupting active transactions, the system maintains both a current root and a previous root with a configurable time window.
When the freeze list is updated, the old root becomes the previous root and remains valid for a grace period (defined by block_height_window). This prevents transactions that were created with valid Merkle proofs from failing if the freeze list is updated before the transaction is processed. Transactions can use either the current root or the previous root, as long as the previous root was updated within the time window (updated_height + window > block.height).
Ordered Merkle Trees and Non-Inclusion Proofs
The technique for proving non-inclusion in ordered Merkle trees is well established in the literature on authenticated dictionaries, where non-inclusion proofs are constructed by showing the two bounding keys where the queried key would otherwise belong (Crosby & Wallach, 2011). Modern implementations include Aztec’s Indexed Merkle Tree, which adapts this approach to zero-knowledge circuits.
The freeze list uses an ordered Merkle tree where leaves are sorted by address value. This structure enables efficient non-inclusion proofs: to prove an address is not in the tree, a user provides two Merkle proofs for adjacent leaves that bracket the target address.
For example, to prove address 0x500 is not frozen, a user provides proofs for the two adjacent frozen addresses 0x400 and 0x600. The contract verifies:
- Both proofs resolve to the same Merkle root
- The target address falls between the two proven addresses (
0x400 < 0x500 < 0x600)
- The two proven addresses are adjacent in the tree (consecutive leaf indices)
This approach allows proving non-membership with only two Merkle proofs, regardless of tree size, while maintaining privacy since the verifier only learns about two specific frozen addresses.
Circle Cross-Chain Bridge
The bridge contract enables transferring tokens from Ethereum to Aleo. The bridge validates ECDSA-signed attestations from Circle that authorize minting bridged tokens on Aleo.
The mint operation accepts a 240-byte deposit payload structured as follows:
| Offset (bytes) |
Size |
Type |
Field |
Description |
| 0 |
4 |
uint32 |
magic |
Magic value (0x2bcc5ce7) |
| 4 |
4 |
uint32 |
version |
Protocol version (always 1) |
| 8 |
32 |
uint256 |
amount |
Token amount to deposit |
| 40 |
4 |
uint32 |
remoteDomain |
Destination domain ID (Aleo) |
| 44 |
32 |
bytes32 |
remoteToken |
Aleo token contract identifier |
| 76 |
32 |
bytes32 |
remoteRecipient |
Aleo recipient address |
| 108 |
32 |
address |
localToken |
Ethereum source token address |
| 140 |
32 |
address |
localDepositor |
Ethereum depositor address |
| 172 |
32 |
uint256 |
maxFee |
Maximum fee for destination chain |
| 204 |
32 |
bytes32 |
nonce |
Replay-protection nonce |
| 236 |
4 |
uint32 |
hookDataLen |
Hook data length |
Main validations. The bridge receives three inputs for minting: the deposit payload, an ECDSA signature, and a Keccak256 digest of the payload. The contract first verifies the digest matches the payload hash, then validates the ECDSA signature against Circle’s attestation service public key. After extracting the recipient address and amount from the payload, the bridge verifies the recipient is not on the freeze list using Merkle non-inclusion proofs. The nonce prevents replay attacks by tracking used attestations on-chain.
Minting and burning. Upon successful validation, the bridge calls mint_private on the compliant token contract to create a private token record for the recipient. For the reverse direction, burning tokens to withdraw back to Ethereum uses burn_public, requiring users to first convert any private tokens to public balance. The burn operation also enforces freeze list checks to prevent sanctioned addresses from moving funds out of the system. Note that currently the mint_private operation conveniently creates private Aleo tokens for the recipient, but does not protect the privacy of the transaction, given that the transaction details are public on the Ethereum side.
Access control. The bridge implements a simpler access control model compared to the token contract. It uses two privileged addresses:
- Admin Address: Can pause/unpause bridge operations and update the Circle attester public key
- Emergency Pause Address: Can trigger an emergency pause independently of the admin for rapid incident response
Both pause mechanisms provide circuit breakers to halt bridge operations if issues are detected, with the emergency pause providing an additional layer of security through separation of concerns.
Summary and recommendations
Overall, the code on both token and bridge side was found to be of high quality and well commented and tested. The major issue found had to do with freeze list checks when moving tokens through the bridge (see Freeze list bypass in bridge).
An important invariant of the Merkle tree used to privately prove non-inclusion is that the tree must be properly sorted. For efficiency reasons, the sorting and calculation of the root is done off-chain, while the root and its updates are stored on-chain. The trust assumption here is that the updater uses the correct off-chain code to recompute the tree after modification. This is visible to all network participants who can recompute the tree independently. It is therefore recommended that operationally this root is monitored for correctness to detect erroneous updates early.
Below are listed the findings found during the engagement. High severity findings can be seen as
so-called
"priority 0" issues that need fixing (potentially urgently). Medium severity findings are most often
serious
findings that have less impact (or are harder to exploit) than high-severity findings. Low severity
findings
are most often exploitable in contrived scenarios, if at all, but still warrant reflection. Findings
marked
as informational are general comments that did not fit any of the other criteria.