Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICRC-107: standard for fee collector #117

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

ICRC-107: standard for fee collector #117

wants to merge 26 commits into from

Conversation

bogwar
Copy link
Collaborator

@bogwar bogwar commented Feb 17, 2025

Fee collection in ICRC-based ledgers (e.g., ckBTC) is inconsistently implemented. While transfer transactions often incur fees, approve transactions typically do not. However, there is no standardized way to:

  • Define fee collection rules—who receives fees, which operations are charged, and whether fees are burned.
  • Record fee collection settings directly on-chain in ledger blocks.
  • Provide consistent semantics for wallets, explorers, and other integrations to interpret fee structures.

ICRC-107 extends ICRC-3, adding semantics for fee collection while ensuring full compatibility with the existing block format. This proposal eliminates reliance on off-chain metadata, simplifies integration with wallets and explorers, and ensures full transparency in fee handling.

Copy link
Member

@mbjorkqvist mbjorkqvist left a comment

Choose a reason for hiding this comment

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

Thanks for writing this up, @bogwar! I left a few clarifying questions/comments.


### 2.1 Overview

For each block added to the ledger, some party must pay a fee. The amount and payer of the fee depend on the specific transaction type.
Copy link
Member

Choose a reason for hiding this comment

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

The fee may be set to 0, in which case no party is paying a fee. Also, what if the fee collector is set to the minting account? Should that explicitly be forbidden, or should the standard say that setting the fee collector to the minting account is effectively a way to go back from collecting fees, to burning them, i.e., a way to unset the fee collector (which is otherwise currently not possible)?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the minting account section of icrc1 would assume that the minting account would be burned: https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md#minting-account-

There probably is no harm in mentioning it here as that would be expected behavior from icrc1

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, good suggestion (to allow setting the fee collector to the minting account). I was thinking how to deal with going back to burning and this is the best idea.


This ensures that existing tools and explorers relying on the current `fee_col` format remain functional.

A block **MAY** include the following fields to define or update fee collection settings:
Copy link

Choose a reason for hiding this comment

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

Why do we need 3 fields? Couldn't we just say if fee_col is set, it receives the fee, otherwise fee is burned?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For backwards compatibility: we have to deal with the case where fee_col is set but we only collect for transfers.

Copy link

Choose a reason for hiding this comment

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

How about announcing the block number ranges which follow the standard in the icrc10_supported_standards URL? Something like https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-107.md#blockRange=[[123,)] would mean that all blocks >= 123 would follow the standard. I think this would simplify implementation for clients because all you need to determine if a fee was burned or collected for a given block are the block ranges. You don't have to lookup fee collector settings from previous blocks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This would not allow to accurately calculate the balance of fee collectors (if they were set prior to the standard)

Copy link
Member

Choose a reason for hiding this comment

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

@bogwar can you elaborate again on this a little bit? I kind of understand where we are coming from, but this seems super weird for somebody who is not aware of the history.

are there already past blocks that define the fee collector? does this apply for transfers and approvals?

is the full col_ops list expected to be provided for each block type (transfer, approval, ...)? if so, it seems a little bit weird to provide this information in each block.

we should keep in mind that fee collection was not clearly specified in a standard before and that the behavior could generally differ among different implementations.

I would prefer the this standard to not rely on implementation specifics of the reference implementation. wouldn't it be better to just use the field fee_col as @tmu0 initially proposed (ignoring the history), but document the historical behavior for the rust reference implementation?

Copy link
Member

Choose a reason for hiding this comment

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

and document the semantics for legacy fields per Thomas' suggestion.

the semantics for the legacy fields would be documented in the ledger implementation, right? or also in this standard? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll keep it part of the standard, otherwise it's going to be folklore that will get lost at some point. Do you see an issue with doing so?

Copy link
Member

Choose a reason for hiding this comment

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

I definitely think it is important to be documented. I am not sure if this standard is the best place for that, but I also do not have a strong opinion on it. it just kind of feels wrong here.

maybe we should at least highlight that the legacy fee handling is specifically referring to the Rust implementation of DFINITY before ICRC-107 was introduced.

Copy link

@tmu0 tmu0 Mar 17, 2025

Choose a reason for hiding this comment

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

@bogwar I read through it again and looks like I misunderstood you.

My preferred solution would be adding a new field icrc107_fee_col to the block and not an entirely new block type for setting the fee collector. The idea is that if the ledger supports ICRC-107 he has to specify the field in each block that has a non-zero fee (even if the new field is empty) and in this case the legacy field fee_col is ignored.

Why is this better IMO?

  • self contained: Clients can determine the fee collector of every block without having to look at other blocks
  • allows implementing flexible policies that scale: Ledgers can implement arbitrary complex fee collection logic, e.g., different fee collector for each trx type, round robin fee collection, etc, without having to worry about scalability (current approach would reuqire adding a 107feecol block every time the FC changes)

Copy link
Collaborator Author

@bogwar bogwar Mar 18, 2025

Choose a reason for hiding this comment

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

There are indeed advantages to have a field per block, but... in practice this would add a lot of redundant data per block for the benefit of very few clients of the ledger. Even in the previous incarnation, we used a field to point to the block where the fee collector was set. The saving was 8bytes vs 61bytes.


- If `icrc107_fee_col` is set to a ledger account, that account collects all subsequent fees.
- If `icrc107_fee_col` is set to the empty account (see below), the ledger burns all subsequent fees.
- The most recent `icrc107_fee_col` setting applies to each block.
Copy link
Member

Choose a reason for hiding this comment

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

Could this be extended to be something like "An icrc107_fee_col block configures the fee collection for all subsequent blocks, until superseded by another icrc107_fee_col block"?

- If `icrc107_fee_col` is set to a ledger account, that account collects all subsequent fees.
- If `icrc107_fee_col` is set to the empty account (see below), the ledger burns all subsequent fees.
- The most recent `icrc107_fee_col` setting applies to each block.
- By default, the ledger burns all block fees until the first `icrc107_fee_col` setting is applied. If `icrc107_fee_col` has never been set, the ledger follows legacy `fee_col` logic (see Section 5).
Copy link
Member

Choose a reason for hiding this comment

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

This currently doesn't seem to cover the case where some legacy fee collection has been configured, and later an icrc107_fee_col block is created - in particular, the fees for blocks before the icrc107_fee_col block may have been collected according to the legacy logic.

Copy link
Member

Choose a reason for hiding this comment

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

@mbjorkqvist do you mean that we should explicitly provide the information that there MIGHT be ledger (custom, non DFINITY) implementations out there where the legacy fee_col logic in section 5 does not apply?

if so, I think we should say the ledger most likely burns all block fees ... and elaborate more on that in section 5

Copy link
Member

Choose a reason for hiding this comment

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

I was thinking about a ledger with the DFINITY legacy fee collector implementation, and then later implements ICRC-107 and adds an icrc107_fee_col block. For this case, I feel this bullet point does not apply (since an icrc107_fee_col block HAS been set), and therefore it's unclear whether section 5 applies to the pre-icrc107_fee_col blocks or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the wording is not good; let me rewrite it.


### 3.1 `icrc107_set_fee_collection`

This method allows a ledger controller to update the fee collection settings. It modifies the `icrc107_fee_col` account, which determines where collected fees are sent. The updated settings are recorded in the next block added to the ledger.
Copy link
Member

Choose a reason for hiding this comment

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

"The updated settings are recorded in the next block added to the ledger."

Since we are now proposing a new block type, it should be added to the ledger immediately as a result of a call to icrc107_set_fee_collection, rather than having the new configuration added to the next (unrelated) block added to the ledger.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point; i'll rewrite to state that it is recorded in a new block type

Copy link
Member

Choose a reason for hiding this comment

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

I second that, but I assume this was just a wording/phrasing/interpretation issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

rewrote


### 5.1 Legacy Behavior (`fee_col`)

- 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`) were always burned in legacy behavior as implemented in Dfinity-maintained ICRC-3 ledgers.
Copy link
Member

Choose a reason for hiding this comment

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

Should we explicitly say that if fee_col is not set, all fees are burned?

Copy link
Member

Choose a reason for hiding this comment

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

I think that applies more to the next line (187) and should be added there

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've reorganised the points to be more explicit. PTAL

- 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`) were always burned in legacy behavior as implemented in Dfinity-maintained ICRC-3 ledgers.
- If `icrc107_fee_col` is not set, the ledger follows this legacy behavior, using `fee_col` only for transfers.

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

Choose a reason for hiding this comment

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

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

bogwar and others added 2 commits March 18, 2025 16:02
Co-authored-by: Mathias Björkqvist <[email protected]>
Co-authored-by: Mathias Björkqvist <[email protected]>
Copy link
Member

@marc0olo marc0olo left a comment

Choose a reason for hiding this comment

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

@bogwar provided some additional comments

@mbjorkqvist thanks for the in-depth review 🙏

- If `icrc107_fee_col` is set to a ledger account, that account collects all subsequent fees.
- If `icrc107_fee_col` is set to the empty account (see below), the ledger burns all subsequent fees.
- The most recent `icrc107_fee_col` setting applies to each block.
- By default, the ledger burns all block fees until the first `icrc107_fee_col` setting is applied. If `icrc107_fee_col` has never been set, the ledger follows legacy `fee_col` logic (see Section 5).
Copy link
Member

Choose a reason for hiding this comment

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

@mbjorkqvist do you mean that we should explicitly provide the information that there MIGHT be ledger (custom, non DFINITY) implementations out there where the legacy fee_col logic in section 5 does not apply?

if so, I think we should say the ledger most likely burns all block fees ... and elaborate more on that in section 5


### 3.1 `icrc107_set_fee_collection`

This method allows a ledger controller to update the fee collection settings. It modifies the `icrc107_fee_col` account, which determines where collected fees are sent. The updated settings are recorded in the next block added to the ledger.
Copy link
Member

Choose a reason for hiding this comment

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

I second that, but I assume this was just a wording/phrasing/interpretation issue.

};
```

This method MUST only be callable by the ledger controller or authorized principals. Unauthorized calls SHOULD result in an error.
Copy link
Member

Choose a reason for hiding this comment

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

Unauthorized calls SHOULD result in an error.

SHOULD or MUST? 🤔

do we need to define the expected error, too?


### 5.1 Legacy Behavior (`fee_col`)

- 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`) were always burned in legacy behavior as implemented in Dfinity-maintained ICRC-3 ledgers.
Copy link
Member

Choose a reason for hiding this comment

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

I think that applies more to the next line (187) and should be added there

Copy link
Collaborator Author

@bogwar bogwar left a comment

Choose a reason for hiding this comment

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

Addressed most of the comments


### 3.1 `icrc107_set_fee_collection`

This method allows a ledger controller to update the fee collection settings. It modifies the `icrc107_fee_col` account, which determines where collected fees are sent. The updated settings are recorded in the next block added to the ledger.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point; i'll rewrite to state that it is recorded in a new block type

- If `icrc107_fee_col` is set to a ledger account, that account collects all subsequent fees.
- If `icrc107_fee_col` is set to the empty account (see below), the ledger burns all subsequent fees.
- The most recent `icrc107_fee_col` setting applies to each block.
- By default, the ledger burns all block fees until the first `icrc107_fee_col` setting is applied. If `icrc107_fee_col` has never been set, the ledger follows legacy `fee_col` logic (see Section 5).
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the wording is not good; let me rewrite it.


### 3.1 `icrc107_set_fee_collection`

This method allows a ledger controller to update the fee collection settings. It modifies the `icrc107_fee_col` account, which determines where collected fees are sent. The updated settings are recorded in the next block added to the ledger.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

rewrote


### 5.1 Legacy Behavior (`fee_col`)

- 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`) were always burned in legacy behavior as implemented in Dfinity-maintained ICRC-3 ledgers.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've reorganised the points to be more explicit. PTAL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants