Voting results for the ICRC-1 Fungible Token Standard and next steps

Hi,

The vote on the ICRC-1 Fungible Token Standard is closed. The vote was largely positive with 19 votes in favor and 8 votes against:

The results of the vote can be found in this spreadsheet. I removed the comments for people that voted to not share their information.

Although the vote shows broad community support for the standard, it is also important to acknowledge the negative votes, comments, and suggestions for improvement.

I propose to postpone the motion proposal and spend a bit more time in the working group to address the constructive criticism received. On the one hand we can and should amend the ICRC-1 repository with the (valid) proposed improvements, and on the other we can discuss and clarify some of the broader points made. This should lead to an improved standard with even stronger community support.

Let’s go through the comments to the negative votes here and then talk about them during the next working group.

Q1: We should treat Account consistently

Agreed and we merged a PR that addresses exactly that

Q2: I don’t believe the creation of a single token standard by way of a technical working group is the way to move forward.

ICRC-1 won’t be a single token standard. DFINITY won’t promote it as the token standard but as a reliable standard for the Internet Computer.

The main difference between ICRC-1 and other standards such as ICP is that ICRC-1 has been a joint effort between the community and DFINITY and as such it should be a good standard for future services. This joint effort was possible only because of the working group. This is also why ICRC-1 gets voted.

Q3: Uncertainty about the extensions to the standard

ICRC-1 is a base standard that will be enriched with extensions. It works as a common layer for interoperability. The extensions can add anything, from a payment flow to a way to notify. Extensions can yield a diversity of solutions in the ecosystem and the ability to experiment with new APIs without breaking support for the base standard.

Some extensions are deemed important by the working group. Those extensions are labeled as Core Extension. The Core Extensions are a set of recommended extensions for ICRC-1 that a ledger should support to be compatible with most services. This set is decided by the working group. DFINITY will develop a ICRC-1 ledger that is compatible with all the core extensions.

Q4: The ICRC-1 Payment Flow is not reliable

The ICRC-1 Payment Flow is certainly reliable. It is by far one of the most tested flows on the Internet Computer; it is used every day by many services (e.g. neuron creation).

Other payment flows offer different tradeoffs but with similar levels of reliability. We will provide additional payment flows in future in the form of Standard Extensions to ICRC-1 some of which will be Core extensions. The ICRC-1 payment flow doesn’t preclude them.

The reliability of the ICP Payment Flow is not the reason why multiple standards exist. The common reason stated is that ICP was not standard, so other standards were made. DIP-20 was developed to be like ERC-20 but on the IC (useful if you have ETH experience) and IS-20 was developed to fix issues with cycle draining. None of those standards exist because of reliability issues with the ICP Ledger Payment Flow.

I’m happy to answer any questions about the reliability of the most used flow on the Internet Computer here in the forum. Feel free to reply to this with more details about your concerns.

3 Likes

I’ll speak for this one as I think I can relate. I think reliable is the wrong word. I don’t think anyone is questioning if the ledger can reliably deliver payments. I think the proper issue is that The Payment Flow has poor useability in a couple of contexts.

User flow - Having different formats for principal and accountID(subaccount) can be confusing. It is one more bit of data that an already confused new user has to keep in their head.

Canister flow - If the user has to send your canister their tokens and then something goes sideways, you have their tokens and they don’t have their services. Unless they really understand the block and/or sub-account schema the idea of where their tokens are is opaque. A canister owner has a significant lift to keep the user from ending up in this situation. Sub account is a great way to keep the funds separate, but it still requires the canister owner to create a reprocess(hard_to_find_info) or a withdraw() function and a user pathway to invoke those options.

Approve/Transfer has its own issues, but at least the tokens don’t move until a canister is assured have accounted for the service.

Transfer/Notify covers most cases unless you are upgrading and miss the notification.

Providing an in-canister search by sender/receiver could help alleviate this problem, but it requires indexes on the canister that take up more storage. Ultimately it is just awkward to move tokens in a separate step from providing the service. It creates useability issues.

If you don’t use sub-accounts then all your canister funds end up in the same account and it is likely a security nightmare due to the upgradeability of the IC. On ETH you can’t upgrade the contract so there is more care taken before you click ‘publish’ on a contract. With upgradeable contracts, I’ve seen a trend of moving faster and breaking things, and this makes comingled funds much more of a risk than it would be on a more strict platform. I’m developing an option that canisters not using sub-accounts is a severe security vulnerability and users should be wary of using canisters that comingle their funds. In some cases, this can be difficult because you can’t just send to multiple sub-accounts with one transaction (although I think a batch payment extension could fix this) and you can run into cycle limits if you need to pay out to an array of sub-accounts. Again this is a useability issue that requires more work to do well.


I think we’ve been clear that we’re addressing these with Core Extensions and that is enough for me to move past the useability issue and vote yes, but I understand those that voted no and want that fixed before voting.

4 Likes

In fact…I think I convinced myself that the following might be essential for any ledger that wants to support proper canister security and fund handling:


type TransferArgs = record {
    from_subaccount: opt Subaccount;
    to: Account;
    amount: nat;
    fee: opt nat;
    memo: opt nat64;
    created_at_time: opt Timestamp;
};

icrc1_transfer_batch : ([TransferArgs]) -> async ([variant { Ok: nat; Err: TransferError; }]);

This would allow a canister to handle fees differently for batch transactions as well which might be a nice feature.

2 Likes

That’d be a nice extension and would fix most of the issues I have with subaccounts atm. The only scenario that’d be left out is the case of services frequently moving funds around “internally”, on the canister level it’s as expensive as the cycles needed to run the instructions, with subaccount the ledger must be contacted, batching isn’t always possible and paying fees is unavoidable.

Hard to explain in a short post on mobile, but I’m with you on those scenarios. I still think trying to go with a minimum viable “box” around funds is a best practice. You can move things around in that small box between uses, but if you have a bug, your losses are limited to what is in the box and not the whole canister account. Hmmmm…example…maybe you need to do hourly shifting of assets based on current state between two parties(collateral based on mark to market). You don’t want to do a ledger trx each time because the fees will add up…at least keep each “contract” in a separate sub account with internal accounting against that sub account instead of all contracts on the canister in one big pile.

1 Like

I think the way some services use principal is contributing a lot to the confusion. In reality the idea that a single person has multiple accounts is nothing new nor groundbreaking. It’s how everything works. I would argue that the alternative scenario where you have multiple principals is much weirder. It works well for advanced users but not so much for normal users.

I have to disagree. The payment flow using transfer and subaccount is based on two steps: 1) transfer funds, 2) notify service. The user is in full control of this all the time. If the user hasn’t done a transfer then it needs to do the transfer. If the user has done the transfer then it needs to retry the notification to the service until that goes through.

The user does not have to know about subaccounts. In the general scenario, you have an invoice with a destination address. It doesn’t matter for the user if that address is a principal or an account, for them it’s just the destination of the funds.

The sequence diagram could be something like this:

From the user perspective, the flow with all the error cases look like this:

Let’s see all the steps:

  1. The user calls getInvoice
    1… there is an error => go to step 1
    2… the user gets the invoice => go to step 2
  2. The user calls balance_of on the ledger for the invoice
    1… there is an error => go to step 1
    2… the user gets back 0 => go to step 3
    3… the user gets back the amount of the invoice => go to step 4
  3. The user calls transfer on the ledger for the invoice
    1… there is an error => go to step 1 (double spending is avoided by step 2)
    2… there is no error => go to step 4
  4. The user notifies the service
    1… the is an error => go to step 1 (double spending is avoided by step 2)
    2… there is not error => all good, transaction done

There is a small window in between the notify_payment has been sent where a user can potentially getInvoice again for the same invoice. This could trigger a double spending if the service has moved the funds to its main account before the user reaches point 2. This a non-problem for two reasons:

  1. the service should not return the same invoice multiple times to the same user in a small window of time. Every service should just have a window of time in which the same invoice cannot be returned to avoid ddos attacks.
  2. In the worst case scenario where a disaster happened and a notify got stuck then a refund can be issued but this is rare. Note that the service doesn’t have to pay the fee for refund neither, it could just subtract the fee from the amount in the subaccount. Of course this depends on the type of service that is offered.

Also a final note about the general scenario is that I’m working with a user client that is stateless, so a retry starts always from the beginning. If the client has state then the flow is simpler.

Bottom line is that I don’t think there is poor usability involved in the ICP Ledger.

@skilesare I don’t want to downplay the pain with the ICP Payment Flow but I think it’s fair to listen to everybody else and there are a lot of people that had issues with the ERC-20 approve/transferFrom flow. It’s hard to compare because there are way more services on other chains that use ERC-20 than services using ICP Payment Flow but it’s still important to keep in mind that ERC-20 caused a lot of pain. This doesn’t mean approve/transferFrom is a bad flow, it’s a candidate for a core extension after all, it just means that some code was not up to the task of using the flow correctly or explaining the user what was going on.

Honestly I wish the same good faith that is often applied to ERC-20, despite everything that went wrong with it, was applied, even partially, to the ICP Ledger payment flow.

I do think the ICP Payment Flow works well and it’s just not well understood.

3 Likes

@skilesare there is no to_subaccount. Is that intentional?

With this payment flow the invoice.account can be re-used, right? I mean it can be a “customer account” rather than an “invoice account”, i.e. one account into which the customer pays all his invoices. Your notify_payment already has an invoice argument so the service knows which invoice it is about to draw the funds for.

Yes, accounts can be used to represent a pair of (service, user) principals instead of (service, invoice id). When used like that, they look like approvals except for who owns the tokens. On the Ledger they would both be represented as a map from the tuple of principals to the amount of tokens. They even mimic the same ability to move funds to a third party account.

Just the few messages above have shown that subaccounts have different use/intention to different people. That’s probably part of the confusion around them.

For some people it is so that an end user can separate funds into multiple accounts without having to generate new keypairs. The alternative would be to have multiple keypairs but have that fact hidden from the user by the wallet frontend which derives the keys from a single seed.

For some people it is so that a canister can have multiple accounts. Alternative would be that the canister just has one account and does all the accounting internally. Why should we allow the canister to offload the accounting to the ledger?

For some people it is about safety that not all funds in a service are immediately comingled. Though one could also say that’s a false sense of security and the argument also make some assumptions about how the service works and handles funds.

For some people it is a means to develop a payment flow by linking subaccounts to users or invoices. Though one could say that memos are better suited for this purpose than subaccounts. After all memos allow to identify individual transfers whereas subaccounts only balances which could have a complicated history of multiple transfers. Hence, memos should be strictly more powerful for the purpose of identifying and linking payments to outside events.

And then there are hidden things that users never see that have to do with implementation details about what gets archived in blocks etc. (those details are probably the reason we have subaccounts instead of memos).

These multiple independent intentions/use cases, together with the fact that each one has alternative solutions, probably causes a lot of confusion. The concept of subaccounts definitely seems to be conflating multiple things. It is not clear to someone looking at this standard what the main reason is (if there is a main reason).

1 Like

I would be careful in mixing the reason something exists with the flexibility it provides.

The reason for accounts to exist is to support multiple accounts per principal. Period. This is similar to having multiple bank accounts. It’s simple and nobody gets confused by that.

Then we have the flexibility. Something simple can still be flexible and provide different solutions to different problems. That’s not necessarily bad and in fact it can be quite good, provided that complexity doesn’t increase too much.
In the case of accounts, subaccount can help solving many different problems because it’s a 32 arbitrary bytearray. It has space. A good example is what we discussed above: most identifiers can fit subaccounts which means both invoice ids and principals can fit subaccount.

You can generate subaccount from a single seed in the same way you generate principals. The only difference is that you have the same signature for all of them while with different principals you have different keys.
Conversely it would be nice at some point to create an extension to calculate derived subaccounts from seed and principal.

You can still do this. Some services using accounts do their accounting internally. Flexibility doesn’t mean you need to use all the features. I program in rust and I definitely don’t use all the feature it provides :slight_smile:.

Fair enough I guess. This is what I tried to say before though: the flow is fine, the services may not want to use it for different reasons. This is true also for other payment flows such as approve/transferFrom and transfer/notify.

This isn’t true. Memos are useful if you scan the blocks but if you scan the blocks then you also can check the accounts and that provides the same info at the same granularity than memo. The topic of memo vs subaccount is one we should at some point discuss. As far as I’m concerned, memo have the unique advantage that they can be agnostic to the account format. Scanning blocks with the same memo on the ICP Ledger is the same as on the ICRC-1 Ledger and any other ledger with a memo field and this may be useful if we have multiple standards that all provide memos.

1 Like

Hmmm….I copied that from the Icrc draft so we should check it!

This is really well laid out and I hope we can include it in the standard as a best practice. Personally I ended up here at this flow but had to trace through a few less than optimized flows that made my life difficult.

I agree with your points about approve/transfer from(especially the eth flavor). If you can fix a some of the holes it works well for some things. I’m glad we have a platform powerful enough to support multiple payment pathways!

1 Like