Thoughts on the token standard

approve / transferFrom, transferAndCall, pub/sub are not interchangeable and can be satisfied with different scenarios, which can expose different problems in atomicity. As transferAndCall has difficulty handling Callback failures, which is bad for IC‘s without atomicity support, we drop transferAndCall in favor of approve / transferFrom, pub/sub.

Hm… but pub/sub also involves a token canister calling external canisters (i.e. subscribers). How does pub/sub avoid the atomicity problems that transferAndCall suffers from? From my perspective, it seems like pub/sub would be even harder to make atomic, since there can be an arbitrary number of subscribers who need to be notified.

We will provide some use cases at a subsequent time.
You guessed right! ICLighthouse is working on a wallet and will provide Defi Dapps later.

Awesome—looking forward to it!

1 Like

transferAndCall is often used in synchronous calls.
pub/sub is often used for asynchronous message consumption.
In the IC development environment, pub/sub is more friendly to programmers.
In terms of functionality pub/sub can override transferAndCall.

Why is pub/sub more friendly to programmers if it’s not done atomically?

What type of use case were you envisioning developers using pub/sub for instead of approve / transferFrom?

It supports both pub/sub and approve/transferFrom

Dfinity Fungible Token Standard v0.1.3 Released

Dfinity Fungible Token Standard Change Logs

v0.1.4 Changes

v0.2.0

v0.2.1

Let me introduce the major change in v0.2

Nonce: approve/transfer/transferFrom/burn/mint support optional nonce to avoid submitting the same transaction repeatedly
Remove notify & call : why remove?

Talking to untrustworthy canisters can be risky, for the following (likely incomplete) reasons:

The other canister can withhold a response. Although the bidirectional messaging paradigm of the Internet Computer was designed to guarantee a response eventually, the other party can busy-loop for as long as they are willing to pay for before responding. Worse, there are ways to deadlock a canister.
The other canister can respond with invalidly encoded Candid. This will cause a Motoko-implemented canister to trap in the reply handler, with no easy way to recover. Other CDKs may give you better ways to handle invalid Candid, but even then you will have to worry about Candid cycle bombs that will cause your reply handler to trap.

Fee struct : the old logic

The fee rate has a default decimals 8, this one is undocumented, just a default setting that confuses developers. Now rate_decimals is added to Fee Struct to clearly identify the decimal places of rate, which is more clear

type Fee = record { rate : nat; rate_decimals : nat8; minimum : nat };

Tx Record: DFT Standard compatible with AccountId and Principal, if the caller in the TxRecord use principal, the original Principal of the accountid will be exposed. For privacy reasons, the caller of the tx record is changed from Principal to TokenHolder to avoid privacy leakage.

4 Likes

v0.4.0 is released, this release contains a large number of automated integration test cases to verify the reliability of the code. As the infrastructure of DEFI, reliability and security are the most important things

2 Likes

v0.4.0 is released

Is this standard supported / maintained by Dfinity the org?

No, the Dfinity Foundation is not involved with Deland-Labs’s project.

Hmmmm. Perhaps there should be at least some talk about how to guide teams in using the dfinity brand name in their projects. I understand that a lot of projects would benefit from the exposure, and we’re still early in ecosystem adoption, but at some point having dfinity in their name will start back-firing. At least ask the teams to clearly differentiate between IC / dfinity and state their affiliation or lack thereof …

The name has been changed to fungible-token-standard :slight_smile:
v0.5.0 is released
Major Updates.

  1. Blockchain support for Token, integration of the ledger design, interface optimization design
  2. TokenHolder type changed to AccountIdentifier to ensure that Principal receipts and default AccountIdentifier are consistent
  3. Use bincode serialization to optimize the size of stored data
  4. Auto-scaling failure and archiving failure of the fallback policy optimization
  5. Remove nonce, use transaction hash to do better anti-duplication
  6. Auto-scaling storage optimization
1 Like

I had some issues with the ICP ledger interface and with your design, too, which I outlined in a reply to this thread:

Maybe you could incorporate something like this into your design?

Please read Question 8 and Question 9 that were considered when designing https://dft.delandlabs.com/

8. TransferAndCall vs Receiver Notify

* Question: which option is more suitable
* Consideration:
 * Notify can meet the basic notification needs. Although it cannot support better flexibility, it is sufficient to meet the transfer scenario
 * TransferAndCall provides better flexibility, but it depends on the transfer caller to fully understand the method and parameters corresponding to the call, which is not needed for most transfer scenarios
 * For security reasons, For security reasons, do not call the canister of the location inside the canister,[why?Inter-canister calls](https://www.joachim-breitner.de/blog/788-How_to_audit_an_Internet_Computer_canister)
* Solution:
 * Neither is supported

9. ApproveAndCall VS TransferAndCall

* Question: We compare ApproveAndCall and TransferAndCall. ApproveAndCall and TransferAndCall are two sets of non-atomic operations, there is no difference essentially. Which one should be retained?
* Consideration: In some scenarios, when multiple Tokens need to be transferred at the same time, TransferAndCall can not meet such needs. After approval, execute transferFrom in the final call to pay multiple tokens at once
* For security reasons like Q8
* Solution: call is not supported

We are very familiar with ERC20 and its optimized version. In the initial DFT Standard design, we also referred to these (in the v0.1.0 version, there was a design of notify), and for security reasons, all these designs were finally deleted.

Why we remove it? please check it here Inter-canister calls

I get that it’s hard to correctly handle inter-canister calls, but in some instances, they just have to be made, if you want to have any functionality that doesn’t rely on users issuing multiple transactions to achieve a single task. Especially if you want a service-side guarantee that a multi-step process will finish once it started, you need inter-canister calls. But if you really don’t want to support those, then your token standard is not appropriate for our purposes.

Hi RmbRT,

I’m leading the team responsible for the ICP Ledger and I can explain to you why there is a notification mechanism. Before I start though, I want to emphasise that most standards on the IC don’t have a notification mechanism. That is for a very good reason.

Let me explain why this is the case. There are several ways to notify a Principal about a transaction on the IC. The approach you propose is the more automatic one, which requires the Ledger to notify when a transaction has been successful. With this approach, the Ledger must be sure that the notification arrives at the receiver canister. On the IC, this requires the receiver to answer to the on_ft_received call with either a simple ack in case the handler is implemented on the receiver side, or with an error in case the handler is not implemented. What is the problem then? Well, there are two problems with this: 1) the Ledger could not get any answer from the receiver canister when it calls on_ft_received and 2) the Ledger receives a different error than the one saying the receiver canister misses the on_ft_received endpoint. Let’s talk about each one of these.

If on_ft_received doesn’t return then the Ledger canister is effectively stuck. Remember that the workflow relies on the fact that the message is received. The Ledger theoretically could not even stop the call to the receiver canister because then the payment would be lost. Note that it’s very problematic to upgrade a canister when there is an in-flight message and there is no way for the Ledger to do anything about it.

The situation is not better if the ledger receives a different error from the call, e.g. if the canister doesn’t exist or the canister exists but its queue is full. The Ledger doesn’t know and cannot know whether the canister will exist in future and even if it did, there would be no way to tell when the canister will be available and when it will be able to receive messages. The Ledger could retry indefinitely but then we have another issue: the more notification pending, the more space and time the Ledger would need.

Consequently, I believe that a notification mechanism that involves the ledger (directly), inevitably leads to issues at some point.

Needless to say, no safe standard should support notify without a well defined and tested approach.

The Ledger notify method is another approach. It is more secure than the one you proposed because it gives the responsibility to deliver the notification to the sender. Despite this, the notify method is still problematic and less convenient and we are deprecating it.

A final approach to the issues pertaining to notify is the so called fire and forget mechanism where the ledger sends the event to the canister and doesn’t wait for a response. This is possible and theoretically safer but then you end up with the same issue you have without notify. The caller must be able to call this mechanism multiple times in case the first notify doesn’t reach the receiver canister and that means that the client will need to track double-spending related information.

Note that most of the canisters on the IC work with the ICP Ledger. For instance, the CMC canister and the governance canister work just fine.

2 Likes

I have explained above, we want to support, but due to security concerns, we have to give up support.
And the explanation of @mariop also allows us to see why it needs to be dropped.

Oh, wow, I didn’t know the IC architecture had such hard limitations. Especially the stuck calls. In that case, can any canister ever safely call another canister? Because I assume this is a problem for all inter-canister interactions.

Not sure if I am right or wrong here but…
The way I see it - if the canisters you wrote have correct code in them which will always work and has no bugs, then that kind of eliminates any possibility of an async method getting stuck, even though possibility exists ? Or are there any caveats that might happen in the canister that I am not aware of and cause a stuck method, breaking functionality? @mariop

I think Mario possibly gave this example about ledger canister getting stuck if we are “awaiting” execution of a callback method defined in a separate canister to which ledger canister should automatically make a call after each transaction to notify it, because some people might not know they must implement that callback method in their canister if they want to work with ledger canister that has that kind of functionality, which would undoubtedly happen.

But if YOU are writing your own canisters and making inter-canister calls that are async, code works etc, I don’t see how this might happen.

The scenario that @mariop has in mind is not that of a canister which does not implement the callback (in this case the IC will return an error message to the caller) but a malicious canister which never returns an answer to the call of the ledger.
So (as @AnonymousCoder says) notifying trusted canisters should be ok (provided that they are not buggy) but notifying arbitrary canisters (as I’m guessing @RmbRT suggests) is quite problematic.

2 Likes

Correct, this is what I suggested. But extrapolating from that fact, we now can’t have any open composable systems of canisters that can interact with other canisters. The only way to safely implement such an interaction is to output something that the user then has to pass to another canister in another transaction.
However, there are two problems with this:

  • The user is not guaranteed to issue this transaction, so the initial canister cannot possibly rely on an issued action being executed (not even guaranteed to happen eventually).
  • The recipient canister might have to verify that data, and for that, would have to query the source canister for verification. However, as I understand it, if the receiving canister does not explicitly trust the source canister, it cannot even contact it for verification.

Thus, the IC is extremely limited in what kinds of ecosystem you can design in it. You can only create closed groups of canisters that trust each other, but cannot really extend them. Unless you create a developer-authorised dynamic web of trust for canisters, which to me smells like attacking the problem from the wrong side. You have this one issue in the protocol of the IC regarding inter-canister calls, and that hinders all attempts to make a powerful, simple and safe to use token standard. This one technical limitation has now led to limited composability of canisters, needlessly complex transaction workflows, complex logic within canisters, and forced developers to maintain some form of whitelist for canisters to interact with.

Is it impossible to force a canister call to definitely return eventually at the protocol level, or to support making a call that does not expect a return message on the IC (therefore, does not wait for a reply)? If so, why? The second option seems trivially possible to me and I think it would solve a lot of problems regarding inter-canister event propagation. It would allow generic handling and issuing of events without having to make both canisters whitelist each other or forcing users to make multi-step transactions.
I know there is still the problem of a call being rejected or never being executed because of issues with the destination canister’s message queue, but at least a call would no longer have the ability to completely incapacitate the caller canister.

5 Likes