You should join the working groups. We were just discussing multi-canister tokens last week! I’ve have collaborated on and drafted various iterations of ICRC-80 over the last 18 months. (Why it has been sitting idle for 18 months is a long story that I’ll not relay here)
In reality, there has been much progress as significant infrastructure to support all of this has been necessary, and thus the work on making orchestration, installation, independence, verifiability, and DAO oversight-without-burden. But here are the current high-level drafts. ICRC-80 is also fundamentally intertwined with ICRC-8(Market Intents), which should see the light of day soon as the collaborating organization has the code in the can for a marketplace with all the React goodies for whitelabeling a roll-your-own token or NFT marketplace built on interoperable standards.
TLDR - 80 is compatible with 1+2 in that no actual change has to be made to wallets for it to “work”. The backend change is that the subaccount becomes the token ID and you can’t co-mingle across subaccounts. Ie, if Principal X sends tokens at subaccount Y to Principal Z at subaccount J, the canister should trap. So in this sense, wallets may initially be confused, but you shouldn’t be able to lose funds through accident.
You do lose subaccounts in ICRC-80, but I think that is ok in this sense given that canisters can still transact with source canisters and the advantages of them working over ICRC-1/2 give speed to uptake.
1. Introduction
ICRC-80 defines the foundational architecture for multi-token canisters on the Internet Computer, using the extended Subaccount mechanism known as TokenPointer to uniquely represent and manage multiple token types within a single canister. This standard establishes account, transaction, and metadata models adapted for token multiplexing.
All block type schemas are specified in [ICRC-80-Blocks], named token association in [ICRC-80-Named-tokens], and escrow/deposit/withdraw mechanisms are governed exclusively by [ICRC-Escrow]. This document provides only the core protocol and behavioral definitions — implementers must refer to associated standards for these auxiliary topics.
2. Data Representations
2.1 TokenPointer
TokenPointer generalizes the concept of subaccount (as in ICRC-1), allowing each account to refer to a specific token within the canister or to an external token reference. Under ICRC-80, every transfer, balance inquiry, and approval is determined by matching both the principal (owner) and TokenPointer (subaccount), which functions as the token identifier.
TokenPointer Structure:
-
Length Byte: The first byte indicates the length of the data, including the flag byte.
-
Data: Principal bytes or numeric token ID. Last byte in this segment is the flag.
-
Flag Byte: Specifies the pointer type:
-
0x01 — External ICRC-2 principal token.
-
0x7f — Local (native) canister token.
-
0x7d — External ICRC-80 principal token (with ID).
-
0x7e — External ICRC-7 principal token (NFT).
-
0x7c — Named token (see ICRC-80-Named-Tokens).(Intents?)
-
Padding: Pad with zeros to 32 bytes.
-
Default: 0x00..00 (32 bytes) or null for the canister’s primary/native token.
Example TokenPointer Usages
-
Transfer of local token id 123 (flag 0x7f) between two owners:
-
Both sides must use identical TokenPointer in subaccount field.
-
Holding external ICRC-2 token (flag 0x01):
-
TokenPointer encodes the target canister principal with 0x01 flag and zero-padding.
For schema, construction, and textual presentation rules, see above. To construct TokenPointers, use icrc8_construct_token_id (see below).
3. Main Functions
3.1 Overview
All ICRC-1, ICRC-2, and ICRC-4 methods are implemented to operate over accounts where each token type is segregated via its TokenPointer (subaccount) value. Unless otherwise specified, all references to ‘subaccount’ below mean TokenPointer.
Mandatory Subaccount (TokenPointer) Matching
Operations involving transfer of value (via icrc1_transfer, icrc2_transfer_from, or batch transfer) must verify that the source and destination TokenPointer match exactly. If not, the canister must return:
type GenericError = record { error_code : nat; message : text };
3.2 ICRC-1, ICRC-2, and ICRC-4 Method Adaptations
icrc1_transfer
Transfer a quantity of a specified token by subaccount.
-
If source and destination TokenPointers/subaccounts do not match, reject as above.
-
Fees (if any) are implementation defined per-token.
icrc2_approve/icrc2_transfer_from
Authorize and execute third-party spending for a target token.
-
Approvals are TokenPointer-specific.
-
Spender may use any subaccount they control, but execution must match TokenPointer rigorously.
icrc4_transfer_batch
Batch process multiple transfers.
- Each transfer entry must specify appropriate TokenPointer and all must individually pass subaccount matching.
Query Methods
-
icrc1_balance_of — Returns the balance for a specific TokenPointer/principal pair.
-
icrc2_allowance — Returns allowance set for a TokenPointer.
-
icrc4_balance_of_batch — Batch balance queries for multiple TokenPointer/principal pairs.
Metadata Extensions
ICRC-80 provides additional token-specific metadata endpoints:
icrc80_name_by_id : (TokenPointer) -> (text) query;
icrc80_symbol_by_id : (TokenPointer) -> (text) query;
icrc80_decimals_by_id : (TokenPointer) -> (nat8) query;
icrc80_fee_by_id : (TokenPointer) -> (nat) query;
icrc80_total_supply_by_id : (TokenPointer) -> (nat) query;
icrc80_metadata_by_id : (TokenPointer) -> (vec record { text; Value }) query;
icrc80_maximum_query_batch_size_by_id : (TokenPointer) -> (opt nat) query;
icrc80_maximum_update_batch_size_by_id : (TokenPointer) -> (opt nat) query;
Other Utility Methods
-
icrc80_tokens(prev: opt TokenPointer, take : opt nat) -> (vec record { TokenPointer; nat }) query
-
Returns a list of all tokens in the canister and their supplies.
-
icrc8_construct_token_id(vec TokenIdentifierRequest) -> (vec opt TokenPointer) query
-
Request construction of TokenPointers for local or ICRC-2 tokens.
-
icrc80_balance_of(BalanceOfRequest, prev: opt TokenPointer, take: opt nat) -> (vec record { TokenPointer; nat }) query
-
Retrieve all token balances for a principal, optionally filtered.
3.3 Fee Handling
Fees for token transfer and other operations are defined at the token implementation level. All fee schedules are token-specific and accessible via metadata methods.
4. Error Handling
All state-changing methods must strictly verify subaccount/TokenPointer match as described. On violation, a GenericError with error code 80 and message Incompatible Subaccount must be returned. Permission and allowance errors must conform to ICRC-1/2 standards.
5. References to Related Standards
-
Block Schemas: All block definitions, remote transfer schemas, and related fields are specified in [ICRC-80-Blocks], not this document.
-
Named Tokens: See [ICRC-80-Named-Tokens] for methods and models for named tokens and mapping to external assets.
-
Escrow, Deposit, Withdrawal: Functionality for escrow, remote deposit, and withdrawal is covered in the [ICRC-Escrow] standard. No methods for these operations appear in ICRC-80 other than the requirements to reference them as needed.
6. Examples
Example: Account Creation and TokenPointer Use
Suppose Alice holds three tokens in a canister:
- The canister’s native token (default TokenPointer):
{ owner = alice_principal; subaccount = null }
- An external ICRC-2 token ICPT (principal
ryjl3-tyaaa-aaaaa-aaaba-cai):
{ owner = alice_principal; subaccount = ?("0x0A...01") } // Constructed using TokenPointer rules
- A local token for this canister (id = 5; flag 0x7f):
{ owner = alice_principal; subaccount = ?("0x02...7f") }
Example: Transfer Attempt with Mismatched TokenPointer
If a user tries to transfer from { owner: A, subaccount: T1 } to { owner: B, subaccount: T2 } where T1 ≠ T2:
- The method must fail with:
Err(GenericError { error_code = 80; message = "Incompatible Subaccount" })
Example: Querying Token Metadata
To obtain the name/symbol/fee etc of a token, pass the correct TokenPointer to the respective icrc80_X_by_id method. If the TokenPointer is not a valid token in the canister, errors follow ICRC metadata conventions.
7. Out of Scope
-
Block definitions/formats — Provided in [ICRC-80-Blocks]
-
Named tokens, mapping, bridging — See [ICRC-80-Named-Tokens]
-
Escrow, deposit, withdrawal mechanisms — Exclusively managed in [ICRC-Escrow]; this standard only references their use.
This draft is subject to change pending final review and related standard maturity.
ICRC-80-Escrow
ICRC-80-Escrow: Standard Escrow Interface for Multi-Token Ledgers
| ICRC |
Title |
Author(s) |
Discussions/Issue |
Status |
Type |
Category |
Created |
| Escrow |
Escrow Canister Standard for Multi-Token Ledgers |
DFINITY Community |
GitHub · Where software is built |
Draft |
Standards Track |
Financial |
2024-XX-XX |
Abstract
ICRC-80-Escrow defines a standardized escrow mechanism for ICRC-80 (and similar multi-token) ledgers on the Internet Computer. It provides a common interface for securely holding, releasing, and withdrawing digital assets, enabling use cases such as cross-canister transfers, conditional payments, and bridging to external systems. Deposit and withdrawal features are explicitly not governed by the core ICRC-80 standard and must follow this Escrow specification.
1. Motivation and Scope
The ICRC-80-Escrow standard:
- Is designed for ledgers which manage multiple token types using ICRC-80.
- Separates escrow, deposit, and withdrawal logic from the base token/trade methods.
- Defines safe, interoperable ways for tokens to be held conditionally and released upon predetermined outcomes (e.g., bridging, delayed delivery, or dispute resolution).
- Is referenced by ICRC-80 and other token standards for all escrow-related operations.
Not in scope: Non-token assets, custom dispute resolution processes, or non-deterministic smart contract flows.
2. Key Actors and Definitions
- Escrow Canister: Implements the ICRC-80-Escrow interface, responsible for securely holding tokens on behalf of parties.
- Depositor: Principal or canister placing tokens under escrow.
- Beneficiary: Target recipient of tokens upon escrow completion.
- Arbiter/Resolver: Optional third-party with authority to settle, dispute, or resolve escrowed funds.
- EscrowID: Unique identifier for an escrow instance.
- Escrow State: Life cycle phases—
Initialized, Funded, Released, Reverted, Expired.
3. Overview of Escrow Mechanisms
Typical Escrow Workflow
- Escrow Created: An escrow intent is registered by the depositor. Parameters (token type, amount, beneficiary, conditions, timeouts) are set.
- Deposit/Fund: Escrow canister receives tokens (typically via an
icrc2_transfer_from or similar call) from the depositor. Escrow state becomes Funded.
- Release/Withdrawal: Upon meeting release conditions (manual approval, cross-chain event, timeout, etc.), beneficiary (or escrow logic) calls to release funds. Tokens are sent to the beneficiary’s account via ledger interaction.
- Reversion/Expire: If conditions fail or escrow is canceled, funds are returned to the depositor.
Integration note: All deposit and withdrawal for ICRC-80 tokens must be routed through the standard ICRC-80-Escrow workflow.
4. Method Specifications and Types
Type Definitions
type EscrowID = nat;
type TokenPointer = blob; // As per ICRC-80
type Account = record { owner: principal; subaccount: opt blob };
type EscrowState = variant { Initialized; Funded; Released; Reverted; Expired };
type EscrowTerms = record {
token: TokenPointer;
amount: nat;
depositor: Account;
beneficiary: Account;
arbiter: opt principal;
release_condition: opt text; // Implementation-dependent
timeout: opt nat64; // UTC timestamp in seconds
};
type EscrowRecord = record {
id: EscrowID;
terms: EscrowTerms;
state: EscrowState;
created_at: nat64;
updated_at: nat64;
};
type EscrowError = variant {
NotFound;
NotAuthorized;
AlreadyFunded;
NotFunded;
InvalidState;
Timeout;
ReleaseConditionNotMet;
GenericError: record { error_code: nat; message: text };
};
Methods
1. escrow_create
Registers a new escrow intent and returns an EscrowID.
escrow_create: (EscrowTerms) -> (variant { Ok: EscrowID; Err: EscrowError });
2. escrow_fund
Performs the transfer of tokens (typically by calling an underlying icrc2_transfer_from or similar method), moving tokens into escrow. May only be called after escrow_create.
escrow_fund: (EscrowID) -> (variant { Ok: (); Err: EscrowError });
3. escrow_release
Releases the escrow to the beneficiary after confirmation or release conditions are met. Can be limited to the arbiter or determined by canister logic.
escrow_release: (EscrowID) -> (variant { Ok: (); Err: EscrowError });
4. escrow_revert
Reverts/unwinds the escrow in error or if terms are not met (state is not released/finalized). Funds return to the depositor.
escrow_revert: (EscrowID) -> (variant { Ok: (); Err: EscrowError });
5. escrow_status
Returns details for a given escrow instance.
escrow_status: (EscrowID) -> (variant { Ok: EscrowRecord; Err: EscrowError }) query;
6. escrow_list
Returns a paginated list of escrow records (e.g., by user, status, or all).
escrow_list: (owner: principal, prev: opt EscrowID, take: opt nat) -> (vec EscrowRecord) query;
5. Integration with ICRC-80
- ICRC-80-compliant ledgers must remove direct deposit and withdrawal methods, and instead reference ICRC-80-Escrow for such flows.
- When a remote deposit or withdrawal is required (e.g., cross-canister transfer or bridging), ICRC-80 ledgers shall invoke the relevant
escrow_create and escrow_fund methods to initiate the escrow, and escrow_release or escrow_revert for completion.
- ICRC-80 blocks referring to remote deposit/withdrawal should reference the corresponding EscrowID and EscrowRecord for audit and reconciliation.
6. Security Considerations
- Escrow canisters must carefully validate state transitions to avoid double release or re-entrancy.
- Only authorized actors (depositor, beneficiary, arbiter) should be able to trigger sensitive callbacks.
- Timeouts and release logic should be deterministic and auditable.
- Major security boundaries include correct accounting for token pointers and integration with underlying ledger for actual token movement.
7. Example Flows
Example: Cross-Chain Bridging
- User initiates an escrow (escrow_create) for a cross-chain deposit.
- Escrow canister holds tokens, waiting for cross-chain event.
- Upon proof or event, escrow_release is triggered to release tokens to the target beneficiary/subaccount.
- If rejected or timeout, escrow_revert returns tokens.
Example: Conditional Payout (e.g., marketplace)
- Buyer deposits funds into escrow via escrow_create + escrow_fund.
- Upon delivery confirmation (or arbiter approval), escrow_release transfers tokens to seller.
8. Standard Metadata
Escrow canisters should expose metadata, such as supported standards:
icrc_standards: () -> (vec record { name: text; url: text }) query;
9. Changelog
- 2024-XX-XX: Initial draft.
ICRC-80-Escrow-Blocks
Note: ICRC-80-Escrow has a significant overlap with ICRC-84 and the two may/should be combined.
1. Introduction
ICRC-80-Blocks formally defines the standard block types (schemas and structures) for multi-token canisters following [ICRC-80]. This document exclusively governs:
- The concrete structure and field requirements for blocks representing remote deposit and remote withdrawal actions within the ICRC-80 multi-token context.
- Block schema serialization and validation rules for these block types (not for canister methods or canister business logic).
- Context: Blocks are used for ledger/chain records, cross-canister, and cross-chain settlement.
SCOPE:
- This standard does not cover canister methods, business logic, TokenPointer definition, named tokens, or escrow mechanisms. It exclusively specifies block structure and block serialization for remote deposit/withdraw.
- All canister-side deposit, withdrawal, and escrow protocols are defined in [ICRC-Escrow]. TokenPointer structure and multi-token account logic is defined in [ICRC-80].
2. Block Format and Conventions
Blocks in this standard follow the general ICRC-3 style: a map-based structure encoded via variant/vec record pairs, with a btype text key to identify the block type and a tx map (record) containing type-specific fields. All blobs, nats, and arrays are to be encoded as in ICRC-3 and ICRC-1 standards.
Block Identifiers
- The field
btype (variant { Text }) MUST be present and exact:
- “80ERemoteDeposit” for remote deposit blocks
- “80ERemoteWithdrawal” for remote withdrawal blocks
Account Representation
- All account fields (
from, to) MUST encode a principal (as Blob, 29 bytes) and a subaccount (as Blob, typically 32 bytes), forming a tuple: Array(vec {Blob = principal; Blob = subaccount}). See [ICRC-80] for TokenPointer format.
3. Block Type: 80ERemoteDeposit
Purpose
Represents tokens deposited (minted to) an ICRC-80 canister as a result of approved cross-canister or remote ledger transfer.
Schema
btype (Required): Must be the string “80RemoteDeposit”
tx (Map) Required fields:
from (Required): Array(vec {Blob (principal), Blob (subaccount)}); account sending funds from source ledger
to (Required): Array(vec {Blob (principal), Blob (subaccount)}); account receiving tokens inside ICRC-80
canister (Required): Blob; principal of the source canister/ledger or external reference
amount (Required): Nat; amount transferred
tokenId (Optional): Blob or Nat;
- If the token is an ICRC-80/ICRC-7/ICRC-NFT,
tokenId specifies the token, otherwise omitted. Can be Nat (recommended) or Blob format where relevant.
Example (ICRC-3 Encoded Pseudocode)
variant { Map = vec {
record { "btype"; variant { Text = "80RemoteDeposit" }};
record { "tx"; variant { Map = vec {
record { "from"; variant { Array(vec {Blob = <principal_bytes>, Blob = <subaccount_bytes>})}};
record { "to"; variant { Array(vec {Blob = <principal_bytes>, Blob = <subaccount_bytes>})}};
record { "canister"; variant { Blob = <principal_bytes> }};
record { "amount"; variant { Nat = <amount_value> }};
record { "tokenId"; variant { Nat = <token_id_value> }}; // only if applicable
}}};
}};
Minimum fields: btype, from, to, canister, amount
Example (Sample Values)
variant { Map = vec {
record { "btype"; variant { Text = "80RemoteDeposit" }};
record { "tx"; variant { Map = vec {
record { "from"; variant { Array(vec {Blob = principal_A; Blob = subA})}};
record { "to"; variant { Array(vec {Blob = icrc80_canister; Blob = icrc80_token_pointer})}};
record { "canister"; variant { Blob = remote_ledger_canister }};
record { "amount"; variant { Nat = 1000 }};
}}};
}};
4. Block Type: 80ERemoteWithdrawal
Purpose
Represents tokens withdrawn (burned or exported) from an ICRC-80 canister to an external ledger or destination, commonly for bridging, burn-and-mint, or remote settlements.
Schema
btype (Required): The string “80RemoteWithdrawal”
tx (Map) Required fields:
from (Required): Array(vec {Blob (principal), Blob (subaccount)}); account sending tokens inside ICRC-80
to (Required): Array(vec {Blob (principal), Blob (subaccount)}); remote account to receive tokens
canister (Required): Blob; principal or identifier of remote canister/ledger
amount (Required): Nat; amount withdrawn/transferred
tokenId (Optional): Blob or Nat; as above, present for non-default tokens
Example (ICRC-3 Encoded Pseudocode)
variant { Map = vec {
record { "btype"; variant { Text = "80RemoteWithdrawal" }};
record { "tx"; variant { Map = vec {
record { "from"; variant { Array(vec {Blob = <principal_bytes>, Blob = <subaccount_bytes>})}};
record { "to"; variant { Array(vec {Blob = <principal_bytes>, Blob = <subaccount_bytes>})}};
record { "canister"; variant { Blob = <principal_bytes> }};
record { "amount"; variant { Nat = <amount_value> }};
record { "tokenId"; variant { Nat = <token_id_value> }}; // only if applicable
}}};
}};
Minimum fields: btype, from, to, canister, amount
Example (Sample Values)
variant { Map = vec {
record { "btype"; variant { Text = "80RemoteWithdrawal" }};
record { "tx"; variant { Map = vec {
record { "from"; variant { Array(vec {Blob = principal_A; Blob = subA})}};
record { "to"; variant { Array(vec {Blob = principal_B; Blob = subB})}};
record { "canister"; variant { Blob = remote_ledger_canister }};
record { "amount"; variant { Nat = 200 }};
}}};
}};
5. Guidelines and Validation
- All conformant implementations MUST verify required fields are present and of correct type.
tokenId is strictly optional but must be included (and of the correct type) for non-default/non-native tokens as indicated by ICRC-80.
- All variant and record structures must follow encoding rules in ICRC-1/ICRC-3; field order is not guaranteed but labels must match exactly.
- These block types must not contain custom fields outside the prescribed schema for interoperability.
6. Relations and References
- ICRC-80: This standard is referenced in the Core multi-token canister (ICRC-80), which emits, consumes, or queries these blocks for remote transfer actions.
- ICRC-80-Escrow: Deposit, withdrawal, and escrow protocols are defined elsewhere; this document only describes the on-chain/off-chain record format.
- ICRC-80-Named-Tokens: No content here relates to named tokens except as may influence field assignment in “tokenId”.
- See ICRC-3 for encoding/serialization patterns and detailed variant/map conventions.
7. Revision History
- v0.1 Initial Draft – Defines 80ERemoteDeposit, 80ERemoteWithdrawal block types strictly for ICRC-80.
ICRC-8-Named-tokens
Pending draft, but this allows named tokens, chainfusion tokens, basically extensibility.