Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7dec143
ICRC-107 initial draft
bogwar Jan 14, 2025
f45dbed
bool -> text for metadata
bogwar Jan 15, 2025
63bff20
bool -> text for metadata
bogwar Jan 15, 2025
b5bae30
option 1 from the discussion
Feb 4, 2025
ef25f9e
new versions
Feb 7, 2025
fc15a30
pass
bogwar Feb 12, 2025
d2a50c3
final draft icrc-107
bogwar Feb 17, 2025
b24b937
deleted old version of icrc107
bogwar Feb 17, 2025
88617a3
first pass over Thomas' comments
bogwar Feb 25, 2025
d58a821
implemented WG decisions
bogwar Feb 27, 2025
19764d9
minor edits
bogwar Feb 28, 2025
023326e
minor edits
bogwar Feb 28, 2025
d33d2af
eliminated fee_burn
bogwar Mar 3, 2025
199280b
minor changes
bogwar Mar 3, 2025
86b676a
clarified what fee_ops = [] means
bogwar Mar 3, 2025
e6e4122
new summary
bogwar Mar 6, 2025
4654feb
included examples, legacy fee collection logic, compliance reporting
bogwar Mar 7, 2025
06e6d5d
one more pass
bogwar Mar 10, 2025
eb7d6a4
further clarifications on legacy behaviour
bogwar Mar 10, 2025
d3accbf
typos
bogwar Mar 10, 2025
12eb7ca
typo
bogwar Mar 10, 2025
9582f01
Update ICRCs/ICRC-107/ICRC-107.md
bogwar Mar 18, 2025
0944115
Update ICRCs/ICRC-107/ICRC-107.md
bogwar Mar 18, 2025
100b532
addressed comments
bogwar Mar 20, 2025
597195c
rewrite legacy part
bogwar Mar 20, 2025
1cef2a6
rewrite
bogwar Mar 20, 2025
1f30ad3
introduced tx structure
bogwar Jun 27, 2025
aeac056
fixed an error type
bogwar Jun 27, 2025
92436c8
fixed example
bogwar Jun 30, 2025
b81f013
final(?) pass
bogwar Jun 30, 2025
dd064ab
alignment with ICRC-3 refinements
bogwar Sep 11, 2025
fcd51d3
clarified the sematnics and aligned wiht ICRC-3
bogwar Sep 29, 2025
589b4bd
addressed comments from revies & added clarifications
bogwar Sep 29, 2025
34ef399
restructured the semantics section for icrc107_set_fee_collector
bogwar Sep 29, 2025
463fdbf
updated examples
bogwar Sep 29, 2025
b87d0db
minor typos
bogwar Sep 29, 2025
968e4f1
icrc107_set_fee_col --> 107set_fee_col in blocks and icrc107_fee_col …
bogwar Oct 1, 2025
9e178d3
standards overview
bogwar Nov 5, 2025
6a44362
initial table/overview
bogwar Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 385 additions & 0 deletions ICRCs/ICRC-107/ICRC-107.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,385 @@
# ICRC-107: Fee Management

## Introduction & Motivation

Different ICRC-based ledgers (e.g., ckBTC) handle fee collection inconsistently.
Some assign a designated fee collector for transfer operations, while others burn fees for approve operations—even when fee collection details appear in approve blocks.

This inconsistency creates challenges:

- Fee collection rules are not uniformly defined or visible on-chain.
- Wallets and explorers cannot reliably predict where fees go.
- Integration requires off-chain metadata to interpret fee behavior.

**ICRC-107** standardizes fee collection across all ICRC-based ledgers by introducing:

- A **global fee collector configuration** stored in the ledger state.
- A new block type (`107feecol`) for setting the fee collector on-chain.
- A consistent fee application model integrated into the ICRC-3 evaluation model.

This ensures **transparency**, **interoperability**, and **simplified integration**.

ICRC-107 applies to all ICRC-based ledgers ensuring consistent fee collection behavior across transfers and approvals. This standard is backward-compatible with existing ICRC-based ledgers, allowing them to transition gradually to the new fee collection mechanism, while documenting legacy fee collection logic.

## Overview

ICRC-107 introduces a global on-chain fee collection setting recorded in the ledger via a new block type `btype = "107feecol"`. This setting determines whether the effective fee of fee-charging blocks is credited to a designated account or burned. All changes are recorded on-chain for transparent, interoperable processing by wallets and explorers.


Specifically, ICRC-107 defines:

- A fee collection configuration that specifies the collector account (`fee_collector`), applying to all subsequent blocks.
- A new block type (`btype = "107feecol"`) for setting `fee_collector` to designate the fee collector.
- A backward-compatible mechanism where, until the new mechanism is employed, the legacy collection logic applies.

By embedding fee collection settings entirely on-chain, ICRC-107 ensures **transparency**, **interoperability**, and **simplified integration** for wallets, explorers, and other tools.

### Effective Fee Application

For any block that charges a fee, handle the fee using this **checklist in order**:

1) **Find the current `107feecol` setting for this block**
Look **backwards** from this block’s index to the most recent block with `btype = "107feecol"`.
- If you find one with `tx.fee_col = <Account>`, the current setting is **Account**.
- If you find one with `tx.fee_col = []`, the current setting is **Burn**.
- If you find **none**, then legacy collection logic applies.

2) **Apply the setting**
- **Account found:** credit the **effective fee** to that account.
- **Burn found (`[]`):** burn the effective fee.
- **No 107feecol setting found:** apply **legacy `fee_col` logic (see Legacy Fee Collection Mechanism)**.
- If legacy config designates a collector, credit that account.
- If no legacy collector is configured, burn the fee.

> **Important:** “No block with `btype = "107feecol"` is found is **not** the same as finding one with `tx.feecol = []`.
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammatical error with duplicated 'is'. Should be either 'No block with btype = \"107feecol\" found is not' or 'Finding no block with btype = \"107feecol\" is not'.

Suggested change
> **Important:** No block with `btype = "107feecol"` is found is **not** the same as finding one with `tx.feecol = []`.
> **Important:** No block with `btype = "107feecol"` found is **not** the same as finding one with `tx.feecol = []`.

Copilot uses AI. Check for mistakes.
> `[]` means “we explicitly burn from now on.”
> “No setting found” means “use legacy rules.”

3) **Non-retroactivity**
A `107feecol` block affects only blocks at its index **and after**. Earlier blocks keep whatever applied at their own indices (legacy or prior `107feecol`).


## Common Elements

This standard follows the conventions set by ICRC-3, inheriting key structural components.

- **Accounts**
Represented using the ICRC-3 `Value` type as a
`variant { Array = vec { V1 [, V2] } }` where:
- `V1` = `variant { Blob = <owner_principal_bytes> }` (the account owner),
- `V2` = `variant { Blob = <32-byte_subaccount_bytes> }` (optional).
If no subaccount is specified, the `Array` MUST contain only the owner’s principal.

- **Principals**
Represented as `variant { Blob = <principal_bytes> }`.

- **Timestamps**
Both `ts` and `tx.created_at_time` are expressed in **nanoseconds since the Unix epoch**.
They are encoded as `Nat` in ICRC-3 `Value`, but MUST fit into a `nat64`.
- `ts` is set by the ledger when the block is created.
- `created_at_time` is provided by the caller for deduplication (per ICRC-1).

- **Parent Hash**
Each block includes `phash : Blob`, the hash of its parent block,
unless it is the genesis block (where `phash` is omitted).



## Fee Management






### ICRC-107 Transaction and Block Schema

Each `107feecol` block consists of the following top-level fields:

| Field | Type (ICRC-3 `Value`) | Required | Description |
|-------------------|------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `btype` | `Text` | Yes | **MUST** be `"107feecol"`. |
| `ts` | `Nat` | Yes | Timestamp (in nanoseconds since the Unix epoch) when the block was added to the ledger. |
| `phash` | `Blob` | Yes | Hash of the parent block. |
| `tx` | `Map(Text, Value)` | Yes | Encodes information about the fee collection configuration transaction. See `tx` Field Schema below. |

### Minimal `tx` Schema

The minimal fields required to interpret a `107feecol` block:

| Field | Type (ICRC-3 `Value`) | Required | Description |
|-------------------|------------------------|----------|-------------|
| `fee_col` | `Array` (Account/Empty)| Yes | Target fee collector account, or empty (`[]`) to burn fees. |


## Semantics of `107feecol` Blocks

### Core State Transition

A `107feecol` block updates the ledger’s global fee collector configuration:

- If `tx.107fee_col = []` → all subsequent fees are **burned**.
- If `tx.107fee_col` encodes an account → all subsequent fees are **credited to that account**.

This configuration applies **to all subsequent blocks** until replaced by another `107feecol`.


---

### Fee Application under ICRC-107

For every block that charges a fee:

1. Execute the **pre-fee state transition** (per ICRC-3 evaluation model).
2. Compute the **effective fee** (as defined in ICRC-3).
3. Debit the **effective fee** from the block’s **fee payer**.
4. Determine how the effective fee is handled:

- **If at least one `107feecol` block exists:**
• If the most recent `107feecol` has `tx.fee_collector = []` → burn the effective fee.
• If the most recent `107feecol` has `tx.fee_collector = <Account>` → credit the effective fee to that account.

- **If no `107feecol` block exists yet (legacy mode):**
• Apply the legacy rules described in the *Legacy Fee Collection* section.
• In brief: if `fee_col` is unset, burn all fees; if `fee_col` is set, only transfer fees are credited to that account, and all other fees are burned.

This ensures that fee handling is always well-defined, both before and after the introduction of ICRC-107.

---

**Notes**

- Invariants from ICRC-3 still apply (e.g., `effective fee ≤ amt` for mints; sufficient balance for `amt + effective fee`; allowance reductions include `amt + effective fee` where applicable).
- ICRC-107 does **not** define who the fee payer is — that comes from the semantics of each block type (e.g., ICRC-1/2 legacy rules in ICRC-3).





# ICRC-107: Methods for Setting and Getting Fee Collector Information


### `icrc107_set_fee_collector`

This method allows a ledger controller to update the fee collector. It modifies the `fee_collector` account, which determines where collected fees are sent. The updated settings are recorded in a new block (of type `107feecol`) added to the ledger.

```

type Account = record {
owner: principal;
subaccount: opt blob;
};

type SetFeeCollectorArgs = record {
fee_collector: opt Account;
created_at_time: nat64;
};

type SetFeeCollectorError = variant {
AccessDenied : text; // The caller is not authorized to modify fee collector.
InvalidAccount : text; // The provided account for fee collection is invalid (e.g., minting account, anonymous principal, malformed).
Duplicate : record { duplicate_of : nat }; // A duplicate transaction already exists at position `duplicate_of` in the ledger.
GenericError : record { error_code : nat; message : text };
};

icrc107_set_fee_collector: (SetFeeCollectorArgs) -> (variant { Ok: nat; Err: SetFeeCollectorError });

```

### `icrc107_set_fee_collector` — Semantics

- **Authorization**
The method **MUST** be callable only by a **controller** of the ledger or other explicitly authorized principals. Unauthorized calls **MUST** fail with `AccessDenied`.

- **Effect (on success, non-retroactive)**
The ledger **MUST** append a new block with `btype = "107feecol"`.
**The block’s `tx` is a top-level field of that `107feecol` block** and **MUST** be constructed **exactly** as defined in **Canonical `tx` Mapping** (same keys, types, and value encodings) and embedded in the block.
The value in `tx.fee_collector` becomes the active fee-collection setting **from that block’s index onward**; earlier blocks **MUST NOT** be affected.

- If `fee_collector` is **provided** (`Account`), all effective fees for this and later blocks **MUST** be **credited** to that account, until changed by a later `107feecol`.
- If `fee_collector` is **absent** (`null`), it **MUST** be encoded as an empty array `[]` in `tx`; all effective fees for this and later blocks **MUST** be **burned**, until changed by a later `107feecol`.

- **Return value**
On success, the method **MUST** return `Ok(index : nat)`, where `index` is the block index of the newly appended `107feecol` block. The new configuration **MUST** apply starting **at** `index` (non-retroactive).

- **Multiple updates over time**
If multiple `107feecol` blocks exist, the setting that applies to any block **MUST** be the **last** `107feecol` **at or before** that block’s index.

- **Deduplication & idempotency**
The ledger **MUST** perform deduplication (e.g., using `created_at_time` and any implementation-defined inputs).
If a duplicate is detected, the ledger **MUST NOT** append a new block and **MUST** return `Err(Duplicate { duplicate_of = <index> })`.

- **Error cases (normative)**
The method **MUST** return an error in these cases:
- **AccessDenied** — caller is not authorized to modify the fee collector.
- **InvalidAccount** — provided collector account is invalid (e.g., minting account, anonymous principal, malformed principal/subaccount).
- **Duplicate** — a semantically identical transaction was already accepted (per the ledger’s deduplication rules); response **MUST** include `duplicate_of` with the existing block index.
- **GenericError** — any other failure that prevents constructing or appending a valid `107feecol` block.

- **Clarifications**
`tx.fee_collector = []` (explicit **burn**) is **not** the same as “no `107feecol` has ever been set” (which implies **legacy behavior** applies until the first ICRC-107 setting appears).


---

### Canonical `tx` Mapping (normative and referenced above)

**Scope:** This mapping defines the **exact** content of the **top-level** `tx` field that MUST be embedded in every `107feecol` block created by `icrc107_set_fee_collector` (or by the ledger itself in the system-generated case).

| Field | Type (ICRC-3 `Value`) | Source / Encoding Rule |
|-------------------|-------------------------|-----------------------------------------------------------------------------------------|
| `op` | `Text` | **Constant** `"107set_fee_collector"`. |
| `fee_collector` | `Array` (Account/Empty) | From the `fee_collector` argument. If `null`, **encode as** `Array = []`. If present, encode the Account. |
| `created_at_time` | `Nat` | From the `created_at_time` argument (ns since Unix epoch; **MUST** fit in `nat64`). |
| `caller` | `Blob` | Principal of the caller. |

> **Conformance note:** Hashing uses **representation-independent hashing (RIH)**, so **field order and concrete map encoding do not affect the hash**. What **does** matter is:
> - The **presence** (or absence) of the fields listed above.
> - The **exact values** of those fields, encoded per ICRC-3 `Value`.


#### System-Generated `107feecol` Blocks

For blocks created by the ledger itself (e.g., during an upgrade/migration):

- MUST include top-level `tx.fee_collector` (encoded as above).
- MAY omit `tx.created_at_time` and `tx.caller`.
- If `tx.caller` is included, it SHOULD be the ledger canister principal.



### `icrc107_get_fee_collector`

This method retrieves the currently active fee collector account. Unless changed, these settings apply to the next block added to the ledger.

```
icrc107_get_fee_collector: () -> (variant { Ok: opt Account; Err: record { error_code : nat; message : text } }) query;
```


This method should return the currently active fee collector account:

- If the response is `null`, fees are burned. This corresponds to `fee_collector = variant { Array = vec {}}` on-chain.
- If the response is a valid `Account`, fees are collected by that account. This corresponds to `fee_collector` being set to the ICRC-3 representation of the account on-chain.

This method strictly returns the last explicitly set value of `fee_collector`. It does not infer defaults, and if no fee collector was ever set, it returns `opt Account = null`.







#### Example: set fee collector to a specific account (produced by `icrc107_set_fee_collector`)

Below is an example of a valid **fee collector configuration block** that sets the fee collector to a specific account:

```
variant { Map = vec {
// Block type
record { "btype"; variant { Text = "107feecol" }};

// Top-level tx (constructed per Canonical `tx` Mapping)
record { "tx"; variant { Map = vec {
record { "op"; variant { Text = "107set_fee_collector" }};
record { "fee_collector"; variant { Array = vec {
// Account = [owner, subaccount?]
variant { Blob = blob "\00\00\00\00\02\00\01\0d\01\01" }; // owner principal bytes
variant { Blob = blob "\06\ec\cd\3a\97\fb\a8\5f\bc\8d\a3\3e\5d\ba\bc\2f\38\69\60\5d\c7\a1\c9\53\1f\70\a3\66\c5\a7\e4\21" } // 32-byte subaccount
}}};
record { "created_at_time"; variant { Nat = 1_750_951_727_000_000_000 : nat }}; // ns since Unix epoch
record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\00\00\01\01" }}; // caller principal bytes
}}};

// Standard block metadata
record { "ts"; variant { Nat = 1_741_312_737_184_874_392 : nat }};
record { "phash"; variant { Blob = blob "\d5\c7\eb\57\a2\4e\fa\d4\8b\d1\cc\54\9e\49\c6\9f\d1\93\8d\e8\02\d4\60\65\e2\f2\3c\00\04\3b\2e\51" }};
}}
```
If one wanted to set the fee collector to be the default account of principal
identified by `"\00\00\00\00\02\00\01\0d\01\01"`, then the
`"fee_collector"` field within `tx` should be set to:

`variant { Array = vec { variant { Blob = blob "\00\00\00\00\02\00\01\0d\01\01"} } }`


#### Example: explicitly burn all fees from this point onward (produced by `icrc107_set_fee_collector`)


A block that explicitly sets fee burning by removing the fee collector (i.e., all fees are burned from this point onward):

```
variant { Map = vec {
// Block type
record { "btype"; variant { Text = "107feecol" }};

// Top-level tx (constructed per Canonical `tx` Mapping)
record { "tx"; variant { Map = vec {
record { "op"; variant { Text = "107set_fee_collector" }};
record { "fee_collector"; variant { Array = vec { }}}; // [] means "burn from now on"
record { "created_at_time"; variant { Nat = 1_750_951_728_000_000_000 : nat }};
record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\00\00\01\01" }};
}}};

// Standard block metadata
record { "ts"; variant { Nat = 1_741_312_737_184_874_392 : nat }};
record { "phash"; variant { Blob = blob "\2d\86\7f\34\c7\2d\1e\2d\00\84\10\a4\00\b0\b6\4c\3e\02\96\c9\e8\55\6f\dd\72\68\e8\df\8d\8e\8a\ee" }};
}}
```



## Reporting Compliance

Compliance with the current standard is reported as follows.

### Supported Standards

Ledgers implementing ICRC-107 **MUST** indicate their compliance through the `icrc1_supported_standards` and `icrc10_supported_standards` methods, by including in the output of these methods:

```
variant { Vec = vec {
record {
"name"; variant { Text = "ICRC-107" };
"url"; variant { Text = "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-107.md" };
}
}};
```

### Supported Block Types

Ledgers implementing ICRC-107 **MUST** report the new block type in response to `icrc3_supported_block_types`. The output of the call must include

```
variant { Vec = vec {
record { "name"; variant { Text = "107feecol" }; "url"; variant { Text = "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-107.md" } };
}};
```

## Legacy Fee Collection Mechanism

The **DFINITY-maintained** ICRC ledgers include a fee collection mechanism which, for completeness, is described below.

Until the first block of type `107feecol`, the ledger follows the following legacy behavior.

- If `fee_col` is not set, all fees are burned.
- If `fee_col` is set in a block, the designated account collects only transfer fees (`1xfer`, `2xfer`). Fees for all other operations (e.g., `2approve`) are burned in DFINITY-maintained ICRC-3 ledgers.


New implementations SHOULD avoid using `fee_col` and instead use `fee_collector` for all fee collection settings. Legacy behavior is provided for backward compatibility only and MAY be deprecated in future versions of this standard.

### Handling Fee Collection for ICRC-1 and ICRC-2 Blocks

To determine who collects the fee in a block:

1. Check for fee collector configuration

- If a previous block set `fee_collector`, the ledger uses the behavior specified by this standard.

2. If no `fee_collector` setting exists (legacy behavior):

- If the block is of type `2approve` then the fee is burned
- If the block is a transfer block, i.e. of type `1xfer` or `2xfer`:
- If `fee_col` is specified in the block the fee is collected by `fee_col`.
- If `fee_col_block` is specified use the `fee_col` from the referenced block index.
- If neither `fee_col` nor `fee_col_block` are specified, then the fee is burned.
Loading