diff --git a/ARCs/arc-1410.md b/ARCs/arc-1410.md new file mode 100644 index 000000000..a8690b9ad --- /dev/null +++ b/ARCs/arc-1410.md @@ -0,0 +1,170 @@ +--- +arc: 1410 +title: Partitioned Token Balances +description: Partitioned sub-ledger model for security tokens on Algorand +author: Ludovit Scholtz (@scholtz) +discussions-to: https://github.com/algorandfoundation/ARCs/discussions +status: Draft +type: Standards Track +category: Interface +sub-category: Application +created: 2025-09-03 +requires: 88, 200 +replaces: +--- + +# ARC-1410: Partitioned Token Balances + +## Abstract + +ARC-1410 defines partitioned balances for Algorand security tokens. Partitions allow segregating holdings (e.g., unrestricted, locked, escrow, tranche A/B) while preserving a single ARC-200 token supply. Each partition acts as a logical sub-ledger with rules influencing transfer eligibility. + +## Motivation + +Regulated assets often require lock-ups and categorization of units. Instead of deploying multiple ARC-200 tokens, partitions provide granular control, improved UX, and simplified corporate actions (single total supply). Ethereum's ERC-1410 informs this ARC; we adapt to Algorand state constraints and atomic group mechanics. + +## Specification + +### Partition Identifier + +A partition is identified by an ARC-4 `address` (not a 32-byte arbitrary hash). Divergence from ERC-1410: Ethereum permits `bytes32` (or shorter labels hashed); ARC-1410 fixes the identifier to an address to enable: + +- Native authorization (partition controller / escrow account as identifier) +- Reuse of address indexing infra +- Clear zero-address semantics for the default/unrestricted partition + +Implementations MAY map human-readable labels to partition addresses via a registry box `partition-labels` storing tuples `(label -> address)`. + +### State Representation + +Two recommended models: + +1. Local State Mapping: Each account's local state stores per-partition balances under truncated keys (e.g., first 8 bytes of partition address) when partition count is small (<8 due to schema limits). +2. Boxes: Each holder has a box named `p--` storing a `uint64` balance (msgpack encoded). Preferred for scalable partition counts. + +A global box `partitions` MAY store an indexed list of active partition addresses and optional metadata (label, flags). + +### Invariants + +For every account: `Σ partitionBalances == ARC-200 balance`. Contract logic MUST enforce this on issuance, redemption, and transfers. Discrepancies MUST trigger failure. + +### Balance Semantics + +Clarifications relative to ERC-1410 behavior: + +- Moving units between partitions (e.g., release from locked to unrestricted) MUST NOT change the holder's total ARC-200 balance; it's a reclassification only. +- A partition move operation MUST debit the source partition balance and credit the destination partition balance in the same atomic group as (or instead of) any ARC-200 unit movement so the invariant holds every step. +- Plain ARC-200 (ARC-200 standard) transfers that occur without partition context MUST be defined by implementation policy. Recommended rule: they operate on the default/unrestricted (zero-address) partition ONLY. If the sender does not have enough units in that default partition, the transfer MUST fail (rather than implicitly draining other partitions) to avoid silent regulatory bypass. +- Implementations MAY forbid bare ARC-200 transfers entirely (forcing `arc1410_transfer_by_partition`) by having the ARC-200 logic route through the application; this MUST be clearly documented if chosen. +- Receiving units via a plain ARC-200 transfer credits only the default/unrestricted partition under the recommended rule above. It MUST NOT auto-spread across other partitions. +- Partition-aware issuance/redemption (`arc1410_issue_by_partition`, `arc1410_redeem_by_partition`, `arc1410_operator_redeem_by_partition`) directly adjust both the ARC-200 total and the specified partition balance. + +### Methods (ABI Recommendations) + +(All snake*case with ARC-4 types; `partition: address`. ARC-1410 methods are namespaced with `arc1410*`.) + +- `arc1410_balance_of_partition(holder: address, partition: address) -> (amount: uint256)` +- `arc1410_partitions_of(holder: address, page: uint64) -> (partitions: address[])` (MAY paginate if large. if not, page = 0) +- `arc1410_transfer_by_partition(partition: address, to: address, amount: uint256, data: bytes) -> (receiverPartition: address)` +- `arc1410_can_transfer_by_partition(from: address, partition: address, to: address, amount: uint256, data: bytes) -> (code: uint8, status: bytes, receiverPartition: address)` +- `arc1410_operator_transfer_by_partition(from: address, partition: address, to: address, amount: uint256, data: bytes) -> (receiverPartition: address)` +- `arc1410_is_operator(holder: address, operator: address, partition: address) -> (flag: uint64)` (1 = authorized; zero partition = any) +- `arc1410_authorize_operator(holder: address, operator: address, partition: address)` (partition = zero address to authorize all) +- `arc1410_revoke_operator(holder: address, operator: address, partition: address)` +- `arc1410_authorize_operator_by_portion(holder: address, operator: address, partition: address, amount: uint256)` (Sets a remaining transferable allowance for operator limited to the specified partition.) +- `arc1410_is_operator_by_portion(holder: address, operator: address, partition: address) -> (flag: uint64)` (1 if remaining allowance > 0 for that partition) +- `arc1410_issue_by_partition(to: address, partition: address, amount: uint256, data: bytes)` +- `arc1410_redeem_by_partition(partition: address, amount: uint256, data: bytes)` +- `arc1410_operator_redeem_by_partition(from: address, partition: address, amount: uint256, data: bytes)` +- `arc1410_set_partition_flag(partition: address, flag_key: bytes, value: uint64)` + +Portion operators provide granular delegation without granting full operator rights. If both a full operator authorization and a portion allowance exist, full authorization takes precedence and does not consume the allowance. Portion allowance is decremented on each successful `arc1410_operator_transfer_by_partition` / `arc1410_operator_redeem_by_partition` that relies on it. + +#### arc1410_can_transfer_by_partition + +Purpose: Off-chain (readonly) preflight check returning a compact code and human-readable status string WITHOUT mutating state. + +Return tuple fields: + +- `code: uint8` machine-readable status +- `status: bytes` UTF-8 short description (MAY be truncated client-side if large) +- `receiverPartition: address` partition that would receive the units (may differ if recipient lacks the specified partition and fallback/default is used) + +Error / status codes (draft allocation separate from ARC-1594 codes space): + +- `0x51` success (transfer allowed) +- `0x50` partition_not_exists (sender partition missing / zero balance structure absent) +- `0x52` insufficient_partition_balance +- `0x57` invalid_receiver (zero address or disallowed) +- `0x58` operator_not_authorized (caller not permitted for given holder / partition OR portion allowance insufficient) +- `0x5A` receiver_partition_restricted (future use; e.g., flagged destination partition) OPTIONAL +- `0x5B` partition_flag_blocked (flag indicates non-transferable) OPTIONAL +- `0x5F` internal_error (unexpected condition; SHOULD NOT occur in normal path) + +Implementations MAY extend codes >= `0x60`; clients MUST treat unknown non-zero codes as failure. + +### Transfer Semantics + +`arc1410_transfer_by_partition` MUST internally call or replicate ARC-1594 validation logic including partition-specific flags: + +- Transferable flag absent or zero -> return code `12` PartitionRestricted. +- Check sender partition balance. +- Adjust sender + recipient partition balances atomically with ARC-200 movement (inner transaction or paired ARC-200 transfer). + +If `partition` is the zero address it denotes the default/unrestricted partition when such a concept is used. + +`arc1410_operator_transfer_by_partition` and `arc1410_operator_redeem_by_partition` MUST succeed if either: + +1. Caller is a fully authorized operator for the partition (or global) OR +2. Caller has a sufficient portion allowance (`arc1410_authorize_operator_by_portion`) for that partition covering the requested amount. + +If path (2) is used, the allowance MUST be decremented atomically by the transferred / redeemed amount. If both apply, do not decrement the allowance. + +Redeeming (burning) by partition for end users uses `arc1410_redeem_by_partition` without a `from` parameter; the caller (`Txn.sender`) is implicitly the holder. The earlier draft signature including `from` is DEPRECATED and MUST NOT be implemented by conforming contracts. Operator-driven redemption for another holder MUST use `arc1410_operator_redeem_by_partition` which retains the explicit `from` parameter. + +### Discovery + +Wallets & indexers discover partitions via: + +1. Reading global `partitions` box. +2. Querying `arc1410_partitions_of`. + +### Events (Logs) + +- Tag 0x11 arc1410_partition_transfer | from | to | partition(address) | amount +- Tag 0x12 arc1410_partition_issue | to | partition(address) | amount +- Tag 0x13 arc1410_partition_redeem | from | partition(address) | amount +- Tag 0x14 arc1410_operator_authorized | holder | operator | partition +- Tag 0x15 arc1410_operator_revoked | holder | operator | partition +- Tag 0x16 arc1410_operator_portion_authorized | holder | operator | partition | amount (OPTIONAL) + +### Merging / Splitting + +Optional methods MAY allow migrating balances between partitions with issuer authorization (e.g., releasing locked units). + +### Cost Considerations + +Use boxes to avoid local state schema inflation for large partition sets. Batch operations (e.g., multi-partition redemption) can be structured as multiple app calls in a group. + +## Security Considerations + +- Prevent creation of unauthorized partitions (maintain an allowlist of partition addresses). +- Avoid address reuse for unrelated semantic partitions without proper migration flows. +- Ensure merging logic cannot bypass lock-ups (validate destination partition flags). +- Distinguish clearly in UI between full operator and portion operator grants to prevent overestimation of delegated power. + +## Reference Implementation + +[arc1410.algo.ts](https://github.com/scholtz/arc-1400/blob/main/projects/arc-1400/smart_contracts/security_token/arc1410.algo.ts) + +## Backwards Compatibility + +Applications not aware of partitions can treat the zero address partition as canonical, reading ARC-200 balance minus sum of restricted partitions if exposed. + +## Rationale + +Using address identifiers unifies partition identity with potential escrow / controller addresses and simplifies tooling reuse. + +## Copyright + +CC0 1.0 Universal.