Skip to content

Commit 9e7a1bd

Browse files
committed
Cleanup
1 parent aaa0fb9 commit 9e7a1bd

3 files changed

Lines changed: 26 additions & 67 deletions

File tree

ARC-20-22-SUMMARY.md

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ARC-20 & ARC-22: Executive Summary
22

3-
Two standards. ARC-20 is the base fungible token. ARC-22 adds compliance (freeze lists, audit records, roles, pause).
3+
Two standards. ARC-20 is the base fungible token. ARC-22 adds compliance (freeze lists, audit records).
44

55
---
66

@@ -89,7 +89,7 @@ program my_exchange.aleo {
8989
}
9090
```
9191

92-
### Private transfer with type-erased records
92+
### Private transfer with dynamic records
9393

9494
```leo
9595
fn deposit_private(
@@ -141,7 +141,7 @@ interface ARC20Compliant {
141141
fn unapprove_public(public spender: address, public amount: u128) -> Final;
142142
fn transfer_from_public(public owner: address, public recipient: address, public amount: u128) -> Final;
143143
144-
// Private -- sender must prove non-inclusion in freeze list
144+
// Private -- sender must prove non-inclusion in freeze list; emits ComplianceRecord to investigator
145145
fn transfer_private(recipient: address, amount: u128, input_record: Token,
146146
proofs: [freezelist.aleo/MerkleProof; 2u32]) -> (ComplianceRecord, Token, Token, Final);
147147
fn transfer_private_to_public(public recipient: address, public amount: u128,
@@ -170,22 +170,6 @@ record Metadata { owner: address, sender: address }
170170

171171
`owner` is always `INVESTIGATOR_ADDRESS` (hardcoded, changeable only via multisig-gated upgrade).
172172

173-
### Roles (bitmask)
174-
175-
```
176-
MINTER_ROLE = 1u16 BURNER_ROLE = 2u16
177-
PAUSE_ROLE = 4u16 MANAGER_ROLE = 8u16
178-
```
179-
180-
```leo
181-
// Check role
182-
let role: u16 = address_to_role.get(caller);
183-
assert(role & MINTER_ROLE == MINTER_ROLE);
184-
185-
// Assign combined role (MINTER + MANAGER)
186-
address_to_role.set(addr, 9u16);
187-
```
188-
189173
### Freeze list
190174

191175
Sender proves non-inclusion via two adjacent Merkle leaf proofs showing a gap. Windowed root updates keep previous proofs valid for `BLOCK_HEIGHT_WINDOW` blocks.
@@ -202,7 +186,8 @@ let root: field = verify_non_inclusion(input_record.owner, sender_merkle_proofs)
202186
// Public functions work dynamically
203187
ARC20Compliant@(token_id)/transfer_public(recipient, amount);
204188
205-
// Private functions need Merkle proofs -- call directly or via static import
189+
// Private functions also work -- caller must pass Merkle proofs
190+
ARC20Compliant@(token_id)/transfer_private(recipient, amount, token, merkle_proofs);
206191
```
207192

208193
### On-chain dependencies
@@ -213,7 +198,7 @@ ARC20Compliant@(token_id)/transfer_public(recipient, amount);
213198

214199
| Program | What it does |
215200
|---------|-------------|
216-
| `compliant_token_template.aleo` | Full ARC20Compliant: roles, freeze list, pause, multisig upgrades |
201+
| `compliant_token_template.aleo` | Full ARC20Compliant: freeze list, multisig upgrades |
217202
| `freezelist.aleo` | Sorted Merkle tree of frozen addresses with windowed root updates |
218203

219204
---

arc-0020/README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ created: 2026-03-18
1010

1111
## Abstract
1212

13-
ARC-20 defines a fungible token standard for Aleo, supporting both public and private balances through Aleo's dynamic dispatch (interface) system. Tokens implementing the ARC-20 interface can be called by any program at runtime without compile-time knowledge of the specific token implementation.
13+
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.
1414

1515
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`.
1616

17-
For regulated tokens requiring freeze lists, compliance records, and pause functionality, see [ARC-22](../arc-0022/).
17+
For regulated tokens requiring freeze lists and compliance records, see [ARC-22](../arc-0022/).
1818

1919
## Motivation
2020

21-
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 program ID at runtime, with no recompilation or redeployment required.
21+
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.
2222

2323
ARC-20 supersedes the previous ARC-20 draft (2023), which was written in Aleo instructions without interface support.
2424

@@ -75,10 +75,10 @@ record Token {
7575
}
7676
```
7777

78-
**Metadata** -- Emitted by `transfer_private_to_public`. The sender's identity is private (consumed from a Token record), but the recipient and amount are public inputs. The Metadata record gives the sender a private receipt linking them to the transfer:
78+
**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:
7979
```leo
8080
record Metadata {
81-
owner: address, // the sender (record owner = sender, so only they can decrypt)
81+
owner: address, // set to the sender's address
8282
sender: address,
8383
}
8484
```
@@ -94,7 +94,7 @@ record Metadata {
9494

9595
### Dynamic Dispatch
9696

97-
ARC-20 interfaces are consumed 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.
97+
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.
9898

9999
Leo provides two ways to make dynamic calls:
100100

@@ -120,7 +120,7 @@ ARC20@('my_token')/transfer_public(recipient, amount);
120120
ARC20@('my_token', 'aleo')/transfer_public(recipient, amount);
121121
```
122122

123-
The compiler resolves function names and validates argument types against the interface definition. No manual function selector encoding is needed. `Final` values returned by dynamic calls are run as `dynamic.future`s.
123+
The compiler resolves function names and validates argument types against the interface definition. No manual function selector encoding is needed.
124124

125125
#### Example: Swap
126126

@@ -152,7 +152,7 @@ program my_exchange.aleo {
152152

153153
#### Example: Private Transfer with `dyn record`
154154

155-
For private token operations, `dyn record` provides type-erased records that work with any ARC20 token:
155+
For private token operations, `dyn record` provides dynamic records that work with any ARC20 token:
156156

157157
```leo
158158
fn deposit_private(
@@ -214,7 +214,7 @@ Tests use Jest with a local devnode and Leo CLI execution.
214214

215215
## Rationale
216216

217-
**Additive approvals**: `approve_public` increases the existing allowance rather than replacing it. This matches Aleo's finalize model where concurrent transactions can both succeed -- two `approve_public` calls in the same block will both add to the allowance rather than one overwriting the other.
217+
**Additive approvals**: `approve_public` increases the existing allowance rather than replacing it. This avoids race conditions -- two `approve_public` calls in the same block will both succeed and add to the allowance, rather than one silently overwriting the other.
218218

219219
**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.
220220

@@ -323,7 +323,7 @@ For developers familiar with Ethereum's ERC-20 standard:
323323

324324
## Security Considerations
325325

326-
**Approval model**: ARC-20 uses additive approvals -- `approve_public` increases the allowance, and `unapprove_public` decreases it. This differs from ERC-20's replace semantics. Front-running `approve_public` can increase exposure; users should `unapprove_public` before setting a new allowance if needed.
326+
**Approval model**: ARC-20 uses additive approvals -- `approve_public` increases the allowance, and `unapprove_public` decreases it. This differs from ERC-20's replace semantics. To change an allowance from 100 to 50, call `unapprove_public(50)` rather than setting a new value. Calling `approve_public` without first reducing the existing allowance will add to it.
327327

328328
**Arithmetic overflow/underflow**: Leo's `u128` arithmetic aborts the transaction on underflow/overflow (enforced by the Aleo VM). No explicit checks are needed in implementations.
329329

arc-0022/README.md

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,16 @@ created: 2026-03-18
1010

1111
## Abstract
1212

13-
ARC-22 defines a compliant fungible token interface for Aleo. It extends [ARC-20](../arc-0020/) with freeze-list enforcement, compliance records, role-based access control, and pause functionality 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.
13+
ARC-22 defines a compliant fungible token interface for Aleo. It extends [ARC-20](../arc-0020/) 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.
1414

1515
## Motivation
1616

1717
ARC-20 provides a minimal token standard but lacks regulatory compliance features required by many real-world token deployments. Regulated tokens need:
1818

1919
1. **Freeze lists** to block sanctioned or compromised addresses from transacting
20-
2. **Audit trails** for private transfers, enabling authorized investigators to review token movements without breaking sender privacy for the public
21-
3. **Role-based minting/burning/pausing** to support operational controls required by regulated issuers
20+
2. **Audit trails** for private transfers, enabling authorized investigators to review token movements without exposing sender identity to the public
2221

23-
ARC-22 adds these capabilities while preserving Aleo's privacy guarantees through Merkle non-inclusion proofs. Private transfers remain private to the public, but produce compliance records visible only to a designated investigator address.
22+
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.
2423

2524
## Specification
2625

@@ -103,7 +102,7 @@ The freeze list prevents sanctioned or compromised addresses from transacting. I
103102
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:
104103

105104
1. The freeze list is maintained as a sorted Merkle tree of frozen addresses
106-
2. To prove non-inclusion, the sender provides two adjacent leaf proofs showing that their address falls in a **gap** between two consecutive frozen addresses (or before the first / after the last)
105+
2. To prove non-inclusion, the sender provides Merkle proofs for two adjacent leaves in the tree, showing that their address falls in the **gap** between them (or before the first / after the last frozen address)
107106
3. The proof verifies against the current (or previous) Merkle root stored on-chain
108107

109108
#### Windowed Root Updates
@@ -135,19 +134,6 @@ For `transfer_private_to_public`, a lighter `Metadata` record is emitted instead
135134

136135
The investigator address is hardcoded as the `INVESTIGATOR_ADDRESS` constant in `compliant_token_template.aleo`. It can only be changed by deploying a new edition of the program, which is gated by `multisig_core.aleo` signing operations. This ensures that changes to the investigator require multi-party approval.
137136

138-
### Role-Based Access Control
139-
140-
Roles are assigned using a bitmask system, allowing a single address to hold multiple roles:
141-
142-
| Role | Bitmask | Description |
143-
|------|---------|-------------|
144-
| `MINTER_ROLE` | `1` | Can mint new tokens (public and private) |
145-
| `BURNER_ROLE` | `2` | Can burn tokens (public and private) |
146-
| `PAUSE_ROLE` | `4` | Can pause/unpause all transfers |
147-
| `MANAGER_ROLE` | `8` | Can assign roles to other addresses |
148-
149-
Role checks use bitwise AND: `role & REQUIRED_ROLE == REQUIRED_ROLE`. A MANAGER can assign any role combination to any address, but cannot remove MANAGER from themselves (the `update_role` function requires that if `caller == new_address`, the new role must still include MANAGER).
150-
151137
### Dynamic Dispatch
152138

153139
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:
@@ -160,11 +146,7 @@ ARC20Compliant@(token_id)/transfer_from_public(owner, recipient, amount);
160146

161147
See [ARC-20 Dynamic Dispatch](../arc-0020/#dynamic-dispatch) for the full syntax reference, `_dynamic_call` intrinsic details, and examples.
162148

163-
Private functions (`transfer_private`, `unshield`, etc.) require Merkle proof parameters specific to the freeze-list state, making them impractical for generic dynamic dispatch. These are typically called directly by the token owner or through a program that imports the specific compliant token.
164-
165-
### Pause Mechanism
166-
167-
`set_pause_status(bool)` requires `PAUSE_ROLE` and blocks all transfer functions (public, private, shield, unshield) while paused. Minting and burning are also blocked during pause. This provides an emergency stop mechanism for regulated tokens.
149+
Private functions (`transfer_private`, `unshield`, etc.) can also be called dynamically -- the caller must pass the Merkle non-inclusion proofs as additional arguments.
168150

169151
## Test Cases
170152

@@ -180,18 +162,14 @@ Tests use Jest with a local devnode and Leo CLI execution.
180162
- `transfer_private`: Private transfer with freeze-list proof and ComplianceRecord
181163
- `transfer_private_to_public`: Returns `Metadata` record (not `ComplianceRecord`) with investigator as owner; validates sender address
182164
- `shield`: ComplianceRecord contains correct investigator owner and sender
183-
- `mint_public (positive)`: Minter increases recipient balance
184-
- `mint_private (positive)`: Minter creates private Token with ComplianceRecord
185-
- `burn_public (positive)`: Burner decreases owner balance
186-
- `burn_public (negative)`: Non-burner is rejected
187-
- `mint_public (negative)`: Non-minter is rejected
188-
- `set_pause_status (negative)`: Non-pauser is rejected
189-
- `update_role (negative)`: Non-manager is rejected
190-
- `pause/unpause`: `set_pause_status` blocks all transfers while paused
165+
- `mint_public`: Minter increases recipient balance; non-minter is rejected
166+
- `mint_private`: Minter creates private Token with ComplianceRecord
167+
- `burn_public`: Burner decreases owner balance; non-burner is rejected
168+
- `pause/unpause`: `set_pause_status` blocks and unblocks transfers
191169

192170
## Reference Implementations
193171

194-
- [`compliant_token_template/`](./compliant_token_template/) -- Full ARC20Compliant implementation with freeze list, role-based minting/burning/pausing, Merkle proof non-inclusion verification, and multisig-gated upgrades
172+
- [`compliant_token_template/`](./compliant_token_template/) -- Full ARC20Compliant implementation with freeze list, Merkle proof non-inclusion verification, and multisig-gated upgrades
195173
- [`freezelist/`](./freezelist/) -- On-chain freeze list using a Merkle tree with windowed root updates for proof validity across blocks
196174

197175
## Dependencies
@@ -214,10 +192,6 @@ ARC-22 is a new standard and has no backwards compatibility concerns. Programs i
214192

215193
**Metadata records**: `transfer_private_to_public` emits a lighter `Metadata` record instead of `ComplianceRecord`, since the amount and recipient are already visible as public inputs. The `Metadata` record owner is set to the investigator address so the investigator can identify the sender.
216194

217-
**Pause mechanism**: Token admins with the `PAUSE_ROLE` can halt all transfers via `set_pause_status`. This is an emergency mechanism -- prolonged pauses should be avoided as they freeze all user funds.
218-
219-
**Role-based access**: Minting, burning, pausing, and freeze-list management are gated by role bitmasks. A MANAGER can assign any role to any address but cannot remove MANAGER from themselves, preventing accidental self-lockout.
220-
221195
**Upgradability**: `compliant_token_template.aleo` and `freezelist.aleo` gate program upgrades behind `multisig_core.aleo` signing operations, ensuring that code changes require multi-party approval.
222196

223197
## Testing

0 commit comments

Comments
 (0)