Repository navigation
Replies: 3 comments 2 replies
Comments
interface ARC22Metadata: ARC22 {
fn decimals() -> u8;
fn totalSupply() -> u128;
fn name() -> identifier;
fn symbol() -> identifier;
}
|
0 replies
|
Also, we're missing the |
0 replies
|
Here's my updated proposal: Latest Update: 4/4/26 IARC22 (lib.leo)// The 'IARC22' library.
const MAX_TREE_DEPTH: u32 = 15u32;
// ZERO_ADDRESS as field == 0field
const ZERO_ADDRESS: address = aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc;
// EMPTY_ROOT == hash of H(ZERO_ADDRESS, ZERO_ADDRESS)
const EMPTY_ROOT: field = 3642222252059314292809609689035560016959342421640560347114299934615987159853field;
// Permission Level: None
const NONE_ROLE: u16 = 0u16;
// Permission Level: TODO
const MINTER_ROLE: u16 = 1u16;
// Permission Level: TODO
const BURNER_ROLE: u16 = 2u16;
// Permission Level: can pause/unpause transfers
const PAUSE_ROLE: u16 = 4u16;
// Permission Level: assign roles
const MANAGER_ROLE: u16 = 8u16;
// Permission Level: update the freeze list
const FREEZELIST_MANAGER_ROLE: u16 = 16u16;
struct MerkleProof {
siblings: [field; MAX_TREE_DEPTH + 1],
leaf_index: u32,
}
//=============================================================
// MERKLE TREE HELPER FUNCTIONS
//=============================================================
// Calculates the hash of two sibling nodes in a Merkle tree.
// The order of the siblings depends on the index bit (0 = left, 1 = right).
// Uses Poseidon hash to compute the result.
fn calculate_hash_for_nodes(sibling1: field, sibling2: field, indexbit: u32) -> field {
let poseidon_params: [field; 3] = indexbit == 0u32 ? [0field, sibling1, sibling2] : [
0field,
sibling2,
sibling1,
];
return Poseidon4::hash_to_field(poseidon_params);
}
fn calculate_hash_for_leaves(sibling1: field, sibling2: field, indexbit: u32) -> field {
let poseidon_params: [field; 3] = indexbit == 0u32 ? [1field, sibling1, sibling2] : [
1field,
sibling2,
sibling1,
];
return Poseidon4::hash_to_field(poseidon_params);
}
// Calculates the Merkle root and the depth of a Merkle proof path.
// Iteratively hashes the sibling path based on the leaf index to reconstruct the root.
// Stops when a zero field is encountered in the siblings array, indicating the end of the valid path.
// Returns the calculated root and the actual depth reached.
fn calculate_root_depth_siblings(merkle_proof: MerkleProof) -> (field, u32) {
let root: field = calculate_hash_for_leaves(
merkle_proof.siblings[0u8],
merkle_proof.siblings[1u8],
merkle_proof.leaf_index % 2u32,
);
for i: u32 in 2u32..MAX_TREE_DEPTH + 1u32 {
if (merkle_proof.siblings[i] == 0field) {
return (root, i - 1u32);
}
root = calculate_hash_for_nodes(
root,
merkle_proof.siblings[i],
(merkle_proof.leaf_index / (2u32 ** (i - 1u32))) % 2u32,
);
}
return (root, MAX_TREE_DEPTH);
}
// Verifies that the given address is included in a Merkle tree.
// Asserts that the address matches the first sibling (leaf node).
// Computes the Merkle root and returns it for comparison against a known root.
fn verify_inclusion(addr: address, merkle_proof: MerkleProof) -> field {
assert_eq(addr as field, merkle_proof.siblings[0u32]);
let (root, depth): (field, u32) = calculate_root_depth_siblings(merkle_proof);
return root;
}
// Verifies non-inclusion of an address in a Merkle tree sorted in ascending order by address (as field).
// Accepts two Merkle proofs representing the neighboring leaves around the missing address.
// Returns the common Merkle root if the address is proven to be outside the tree.
// If the tree is not sorted correctly, this function may return incorrect results.
fn verify_non_inclusion(addr: address, merkle_proofs: [MerkleProof; 2]) -> field {
let (root1, depth1): (field, u32) = calculate_root_depth_siblings(merkle_proofs[0u32]);
let (root2, depth2): (field, u32) = calculate_root_depth_siblings(merkle_proofs[1u32]);
// Ensure the roots from the merkle proofs are the same
assert_eq(root1, root2);
// Ensure the depth of the merkle proofs is the same
assert_eq(depth1, depth2);
let addr_field: field = addr as field;
if (merkle_proofs[0u32].leaf_index == merkle_proofs[1u32].leaf_index) {
// Ensure that if the address is the most left leaf, it is less than the first sibling
if (merkle_proofs[0u32].leaf_index == 0u32) {
assert(addr_field < merkle_proofs[0u32].siblings[0u32]);
} else {
// Ensure that if the address is the most right leaf
let last_index_leaf: u32 = 2u32 ** depth1 - 1u32;
assert_eq(merkle_proofs[0u32].leaf_index, last_index_leaf);
// Ensure that the address is bigger than the first sibling
assert(addr_field > merkle_proofs[0u32].siblings[0u32]);
}
} else {
// Ensure the address is in between the provided leaves
assert(addr_field > merkle_proofs[0u32].siblings[0u32]);
assert(addr_field < merkle_proofs[1u32].siblings[0u32]);
// Ensure that the leaf indexes are not greater than the last possible leaf index
let last_index_leaf: u32 = 2u32 ** depth1 - 1u32;
assert(merkle_proofs[1u32].leaf_index <= last_index_leaf);
// Ensure the leaves are adjacent
assert_eq(merkle_proofs[0u32].leaf_index + 1u32, merkle_proofs[1u32].leaf_index);
}
return root1;
}
interface ARC22Freezelist {
// Indicates whether an address is frozen.
mapping freeze_list: address => bool;
// Mimics an array of frozen addresses by storing them at sequential indexes.
mapping freeze_list_index: u32 => address;
// Stores the highest index used in the freeze_list_index mapping.
storage freeze_list_last_index: u32;
// Stores the current Merkle root of the freeze list
storage current_freeze_list_root: field;
// Stores the previous Merkle root of the freeze list
storage previous_freeze_list_root: field;
// Stores the block height when the root was updated.
storage root_updated_height: u32;
// Defines the number of blocks during which the previous root is still considered valid.
storage block_height_window: u32;
// Initializes the freeze list and the admin. Sets the block height window, the initial empty Merkle root,
fn initialize(public admin: address, public blocks: u32) -> Final;
// Freezes or unfreezes an account based on the is_frozen flag (true = freeze, false = unfreeze).
fn update_freeze_list(
public account: address,
public is_frozen: bool,
public frozen_index: u32,
public previous_root: field,
public new_root: field,
) -> Final;
// Verifies that an account is not currently frozen.
fn verify_non_inclusion_pub(public account: address) -> Final;
// Verifies that an account is not included in the freeze list Merkle tree.
fn verify_non_inclusion_priv(account: address, merkle_proof: [MerkleProof; 2u32]) -> Final;
}
interface ARC22 {
mapping balances: address => u128;
mapping allowances: field => u128; // hash([owner, spender]) => amount
storage pause_status: bool;
storage total_supply: u128;
storage max_supply: u128;
record Token;
record ComplianceRecord {
owner: address, // investigator address
amount: u128,
sender: address,
recipient: address
..
}
record Credentials {
owner: address,
freeze_list_root: field,
..
}
//=============================================================
// COMPLIANCE FUNCTIONS
//=============================================================
fn set_pause_status(public status: bool) -> Final; // Stops or resumes all token transfers.
fn get_credentials(sender_merkle_proofs: [MerkleProof; 2u32]) -> (Credentials, Final);
//=============================================================
// APPROVAL FUNCTIONS
//=============================================================
fn approve_public(public spender: address, public amount: u128) -> Final;
fn unapprove_public(public spender: address, public amount: u128) -> Final;
//=============================================================
// TRANSFER FUNCTIONS
//=============================================================
fn transfer_public(public recipient: address, public amount: u128) -> Final;
fn transfer_private(
recipient: address,
amount: u128,
input_record: Token,
sender_merkle_proofs: [MerkleProof; 2u32],
) -> (ComplianceRecord, Token, Token, Final);
fn transfer_private_to_public(
public recipient: address,
public amount: u128,
input_record: Token,
sender_merkle_proofs: [MerkleProof; 2u32],
) -> (ComplianceRecord, Token, Final);
fn transfer_public_to_private(recipient: address, public amount: u128) -> (ComplianceRecord, Token, Final);
fn transfer_from_public(public owner: address, public recipient: address, public amount: u128) -> Final;
fn transfer_from_public_to_private(
public owner: address,
recipient: address,
public amount: u128,
) -> (ComplianceRecord, Token, Final);
fn transfer_public_as_signer(public recipient: address, public amount: u128) -> Final;
fn transfer_private_with_creds(
recipient: address,
amount: u128,
input_record: Token,
credentials: Credentials,
) -> (ComplianceRecord, Token, Token, Credentials, Final);
//=============================================================
// SHIELD/UNSHIELD FUNCTIONS
//=============================================================
fn shield(public amount: u128) -> (ComplianceRecord, Token, Final);
fn unshield(
public amount: u128,
input_record: Token,
) -> (ComplianceRecord, Token, Final);
//=============================================================
// JOIN/SPLIT FUNCTIONS
//=============================================================
fn join(input_1: Token, input_2: Token) -> Token;
fn split(input: Token, amount: u128) -> (Token, Token);
//=============================================================
// MINT/BURN FUNCTIONS
//=============================================================
fn mint_public(public recipient: address, public amount: u128) -> Final;
fn mint_private(recipient: address, amount: u128) -> (ComplianceRecord, Token, Final);
fn burn_public(public owner: address, public amount: u128) -> Final;
fn burn_private(input: Token, amount: u128) -> (ComplianceRecord, Token, Final);
}
interface ARC22Metadata: ARC22 {
fn decimals() -> u8;
fn name() -> identifier;
fn symbol() -> identifier;
}
|
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
arc: 22
title: ARC-22 Compliant Fungible Token Standard
authors: The Aleo Team hello@aleo.org
discussion: #125
topic: Application
status: Draft
created: 2026-03-23
Abstract
ARC-22 defines a compliant fungible token interface for Aleo. It extends ARC-20 with freeze-list enforcement and compliance records for regulated token issuers (stablecoins, security tokens). ARC-22 preserves Aleo's privacy guarantees while enabling regulatory oversight through Merkle non-inclusion proofs and investigator-visible compliance records.
Motivation
ARC-20 provides a minimal token standard but lacks regulatory compliance features required by many real-world token deployments. Regulated tokens need:
ARC-22 adds these capabilities while preserving Aleo's privacy guarantees through Merkle non-inclusion proofs. Private transfers remain hidden from the public, but produce compliance records visible only to a designated investigator address.
Specification
ARC20Compliant Interface
The compliant token interface adds freeze-list enforcement and compliance reporting to ARC-20's transfer primitives:
Record Types
Token -- Represents a private token balance:
ComplianceRecord -- Emitted to the investigator address during private transfers. Contains the full transfer details for compliance auditing:
Metadata -- Emitted by
transfer_private_to_publicinstead ofComplianceRecord. Sinceamountandrecipientare already public inputs visible on-chain, a lighter record is used:Freeze-List Mechanism
The freeze list prevents sanctioned or compromised addresses from transacting. It uses a Merkle tree to enable privacy-preserving verification.
Merkle Non-Inclusion Proofs
Private transfers require the sender to prove they are not on the freeze list without revealing their identity publicly. This is accomplished through non-inclusion proofs:
Windowed Root Updates
When the freeze list is updated, the Merkle root changes. A
BLOCK_HEIGHT_WINDOWmechanism prevents race conditions:BLOCK_HEIGHT_WINDOWblocks after a root updatefreezelist.aleoStatefreeze_listaddressboolfreeze_list_indexu32addressfreeze_list_last_indexboolu32freeze_list_rootu8(1 or 2)fieldroot_updated_heightboolu32block_height_windowboolu32Compliance Records
All private transfers emit a
ComplianceRecordwithownerset toINVESTIGATOR_ADDRESS. This means only the investigator can decrypt the record and view the transfer details (sender, recipient, amount).For
transfer_private_to_public, a lighterMetadatarecord is emitted instead, since the amount and recipient are already visible as public inputs on-chain. TheMetadatarecord'sowneris set toINVESTIGATOR_ADDRESS.Investigator Address
The investigator address is hardcoded as the
INVESTIGATOR_ADDRESSconstant incompliant_token_template.aleo. It can only be changed by deploying a new edition of the program, which is gated bymultisig_core.aleosigning operations. This ensures that changes to the investigator require multi-party approval.Dynamic Dispatch
ARC20Compliant is declared as a Leo
interface, so its public functions (transfer_public,approve_public,transfer_from_public) can be called dynamically using Leo's interface-enforced syntax:See ARC-20 Dynamic Dispatch for the full syntax reference,
_dynamic_callintrinsic details, and examples.Private functions (
transfer_private,unshield, etc.) can also be called dynamically -- the caller must pass the Merkle non-inclusion proofs as additional arguments.All reactions