Repository navigation
Replies: 13 comments 9 replies
|
For public transfers, it may be desirable to include the option to require that a sender's funds originate with the transaction signer via a |
|
Regarding the |
Comments
interface ARC20Metadata {
fn decimals() -> u8;
fn totalSupply() -> u128;
fn name() -> identifier;
fn symbol() -> identifier;
} |
|
Another nit: the receiving address input name is inconsistent in the transfer function ( |
|
I think it's a good idea to include |
|
|
|
Why do |
|
Here's my updated proposal: lib.leo (IARC20)// The 'IARC20' library.
interface ARC20 {
mapping balances: address => u128;
mapping allowances: field => u128;
storage total_supply: u128;
storage max_supply: u128;
record Token {
owner: address,
amount: u128,
..
}
//=============================================================
// 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(input: Token, recipient: address, amount: u128) -> (Token, Token);
fn transfer_private_to_public(input: Token, recipient: address, amount: u128) -> (Token, Final);
fn transfer_public_to_private(recipient: address, public amount: u128) -> (Token, Final);
fn transfer_public_as_signer(public recipient: address, public amount: u128) -> Final;
fn transfer_from_public(public owner: address, public recipient: address, public amount: u128) -> Final;
fn transfer_from_public_to_private(
public owner: address,
public recipient: address,
public amount: u128,
) -> (Token, Final);
//=============================================================
// SHIELD/UNSHIELD FUNCTIONS
//=============================================================
fn shield(public amount: u128) -> (Token, Final);
fn unshield(input: Token, amount: u128) -> (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) -> (Token, Final);
fn burn_public(public owner: address, public amount: u128) -> Final;
fn burn_private(input: Token, amount: u128) -> (Token, Final);
}
interface ARC20Metadata: ARC20 {
fn decimals() -> u8;
fn name() -> identifier;
fn symbol() -> identifier;
}
|
|
Some minor findings from @Antonio95 to sharpen the example implementation:
|
|
(expanding on the first point: |
|
One nit: all these wrappers examples should update the |
|
Discussions about this proposal have been ongoing for nearly two months. I want to remind everyone that this feature is crucial for the market. The official documentation advises against applying the current token registry to new projects, but the new standard has yet to be fully rolled out! PS: Regarding the issue of the official documentation being updated far slower than the actual update schedule and the multi-language support, it's very important to consider assigning a dedicated person to handle the documentation! For beginners, the only way to understand aleo is through the documentation. Don't mention Discord; you know how infrequent the development meetings are—it's not suitable for most people! |
|
This ARC20 standard doesn't give the easiest ways to work around for multisig situations but as previously discussed here, it's better to keep this simple. As such, ZKPuzzle has 3 ways to get multisig on private balances under this standardRecords are signer-addressed (only the transaction signer can consume one it owns; programs have no keys, so they can't hold records or veto their spending — wrapping 1. Split the key — FROST threshold signatures behind one address. The multisig is a normal account; t-of-n parties co-produce its signatures. Verification never sees the difference, so no protocol change. Works for every token including credits.aleo, invisible on-chain; buys only flat t-of-n, and signer rotation is a key ceremony. More work for users for key security + tooling to monitor multisig sessions but is fine. 2. Put policy in the token program like a vault extension. A program-scoped 3. Custody the asset in your own program — for tokens you don't control. Hold the pooled asset as a program-owned public balance, keep entitlements as private records in your program, gate releases on in-circuit user signatures (operator can refuse, never steal or redirect), and disburse privately by consuming an operator-held treasury record inside your transition. Works today on third-party tokens; costs a public aggregate balance acting similar to zcash's turnstile mechanism + trusting the pool operator which can be in charge of things such as a loan management system. Just as a heads up and helpfulness to the community -- number 3 & number 1 can apply to be used on arc22 assets like USDCx and USAD |
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: 20
title: ARC-20 Fungible Token Standard
authors: The Aleo Team hello@aleo.org
discussion: #124
topic: Application
status: Draft
created: 2026-03-23
Abstract
ARC-20 defines a fungible token standard for Aleo, supporting both public and private balances. Programs declare conformance to the ARC20 interface, and any other program can call them at runtime via dynamic dispatch -- without compile-time knowledge of the specific token implementation.
The standard defines ARC20, a minimal interface for public transfers, private transfers, shielding/unshielding, and approvals. An optional MintableToken extension adds mint and burn operations. Reference implementations:
wrapped_credits.aleo,wrapped_token_registry.aleo.For regulated tokens requiring freeze lists and compliance records, see ARC-22.
Motivation
Without a standard interface, every DeFi program must import each token at compile time, making it impossible to build token-agnostic protocols. ARC-20 solves this through Leo's dynamic dispatch: a single AMM, lending protocol, or exchange can interact with any conforming token by name at runtime, with no recompilation or redeployment required.
ARC-20 supersedes the previous ARC-20 draft (2023), which was written in Aleo instructions without interface support.
Specification
Interfaces
ARC20
The minimal fungible token interface. Every ARC-20 token must implement these functions:
MintableToken
Optional extension for tokens backed by deposits (e.g. wrapped credits, wrapped registry tokens):
Record Types
Token -- Represents a private token balance:
Metadata -- Emitted by
transfer_private_to_public. The recipient and amount are already visible as public inputs, but the sender's identity is hidden. The Metadata record is owned by the sender, giving them a private receipt of the transfer that only they can decrypt:Programs
token_registry.aleowrapped_credits.aleocredits.aleo. Deposits Aleo credits and issues 1:1 wrapped tokens implementing the standard interface.wrapped_token_registry.aleotoken_registry.aleotoken ID. Demonstrates wrapping registry tokens behind the standard interface.dummy_exchange.aleotransfer_from_publicandswapto interact with any ARC20 token by program identifier at runtime.Dynamic Dispatch
ARC-20 tokens are called via dynamic dispatch, which resolves the target program at runtime. This allows a single program (e.g. an AMM or lending protocol) to interact with any ARC20-implementing token without importing it at compile time.
Leo provides two ways to make dynamic calls:
_dynamic_callintrinsic -- a low-level escape hatch that bypasses interface checking. The caller manually encodes function selectors asfieldconstants and is responsible for passing correct arguments. Useful when working below the interface abstraction or with programs that don't declare interface conformance.Interface-Enforced Syntax
The target is an
identifiervariable, anidentifierliteral (single-quoted program name), or afieldvalue:The compiler resolves function names and validates argument types against the interface definition. No manual function selector encoding is needed.
Example: Swap
A DeFi program that accepts any ARC20 token by program identifier:
Example: Private Transfer with
dyn recordFor private token operations,
dyn recordprovides dynamic records that work with any ARC20 token:See
dummy_exchange.aleofor a working dynamic dispatch example, and the AMM reference implementation for a full example.Example: Implementing an ARC20 Token
A program declares interface conformance with
: InterfaceName:See
wrapped_credits.aleoandwrapped_token_registry.aleofor complete implementations.Test Cases
Tests use Jest with a local devnode and Leo CLI execution.
ARC20 shared interface tests (
arc20-wrapper-tests.js), run for bothwrapped_creditsandwrapped_token_registry:Rationale
Additive approvals:
approve_publicincreases the existing allowance rather than replacing it. This avoids race conditions -- twoapprove_publiccalls in the same block will both succeed and add to the allowance, rather than one silently overwriting the other.u128 amounts: Future-proofs the standard for high-supply tokens and avoids truncation issues. u64 would limit maximum supply to ~18.4 quintillion base units, which is insufficient for some token designs.
shield/unshieldnaming:shieldconverts the signer's public balance to a private Token record.unshieldconverts a private Token back. The interface also includestransfer_private_to_public, which differs fromunshieldin that it allows specifying a recipient and emits aMetadatarecord linking the private sender to the public transfer.All reactions