Thoughts on the token standard

Thinking about atomicity
Atomicity is a very important matter, but in the traditional distributed development environment, there are two solutions:

  1. Distributed transaction, similar to 2-phase commit (or 3-phase commit)
  2. sagas

Distributed transactions require each participant to support 2-phase commit, but sagas does not have such a requirement. Based on this situation, saga can reduce the complexity of a single canister implementer and complete the consistency requirements independently of a single canister.

So I think sagas is a better consistency solution

3 Likes

Why is fee designed like this?

type Fee = record { lowest: nat; rate :nat32 };

Dfinity should consider the cost of DDOS attacks. The cost design does not appear in the ERC20 standard, but Dfinity is necessary.
The cost design should first consider the minimum handling fee for each update operation to prevent ddos attacks, and some services may be charged according to the rate. The two cost logics are integrated into [lowest + rate] to support different scenarios.

  1. Only minimum charge is required x
    fee= record {lowest: x, rate:0}

  2. Charged at rate y%, minimum charge x
    fee= record {lowest: x, rate:y%}

1 Like

Why do we need approve?

Approve can improve the possibility of repeated payments. In most payment scenarios, post-payment operations, such as shopping, will be followed by order processing, such as transactions, and there will be exchange operations. In these scenarios, approve is better than transfer:

Approve can actually charge through transferFrom when the next specific operation is performed, but transfer must complete the transfer before the next operation. If the user has multiple transfers, it may lead to repeated payments. Approve x can only transfer x, which can be eliminated Repeat payment.

At the same time, based on approve, many innovations were born, such as superfluid . Dfinity needs the approve interface to open the window to accept innovations from Ethereum

2 Likes

We have made some effort to implement some token canister templates.
About 3, currently we have implemented built-in tx storage and separate canister tx storage, ultimately I think we need an auto-scale storage solution for tx history storage.

About 4, fee logic is indeed needed, pay a fixed amount of token for each update call is reasonable.

About 5, I think you mean account id and principal id, here is a picture explains the different, principal id is the unique and native identity on the ic, we choose to use principal in the implementation.

About 6, I think aaaaa-aa can be used as the blackhole address, its the ic-management canister id, not an actual canister, just an abstraction of some system level APIs.

4 Likes

thanks for your feedback. @ccyanxyz
When I designed this token standard, your code was one of my reference codes, thank you for you and your team’s work.

About3, I agree with you about auto-scale storage, I choose sudograph as separate canister tx storage(sudograph can provide richer query support, thanks for the work of the sudograph team @lastmjs .

we need an auto-scale storage solution for tx history storage

Yes, fixed fee can meet the needs of most tokens.
A common fee model is a fixed fee or rate.
type Fee = record {lowest: nat; rate :nat32 };
Can take care of the above two types of needs.

About 4, fee logic is indeed needed, pay a fixed amount of token for each update call is reasonable.

Yes, I mean account id and principal id. Before designing the token standard, I saw this picture.
I don’t know which is the best, and nobody can give a perfect answer, so compatible with both may be a better choice .

About5 ,I think you mean account id and principal id

Yes,I have considered this address, but can official developers call this address to perform operations? I did not find a clear answer, so I gave up this choice. Burn has a similar implementation in ERC20, which is a good choice.

About 6, I think aaaaa-aa can be used as the blackhole address, its the ic-management canister id, not an actual canister, just an abstraction of some system level APIs.

4 Likes

Just added a token canister template with auto-scale history transaction storage, haven’t been thoroughly tested yet, just for reference: ic-token/motoko/auto-scale-storage at main · dfinance-tech/ic-token · GitHub, welcome feedback.

3 Likes

For example, in a scenario, my canisters call the token function: transferfrom(); At this point, my canisters are abnormal. How do I know if my call is successful? Therefore, the standard should provide the ID corresponding to the transaction before sending a transfer。
use transaction ID, we can query the transaction details afterwards. In addition, it is necessary to provide a transaction (index: nat64): record query interface and a current index (nonce similar to ETH) index (CID: Principal): nat64 query interface;

First of all, dfinity is not eth, which means that your experience in eth cannot be 100% copied to dfinity.

Please learn about the atomicity of dfinity from here

Secondly, Canister’s current largest storage is 4G, so the production environment should store transaction history separately. Token Standard implemented by Deland implement separate storage as default:

I try to understand your question:
Scenario: You call transferFrom in your own canister. After calling transferFrom, you deliberately set a trap to make your canister call fail
Question: I can’t confirm whether what you want to know is whether the transferFrom call was successful, or whether your own canister method was successfully called?
My answer is:
Once you call transferFrom and the returned result contains TransactionID, it means that your call was successful. If it is unsuccessful, an error message will be returned.

Even if there are exceptions in the execution of other logic of your canister, transferFrom will not be rolled back because of these exceptions, that is, in this case, transferFrom is still successful.

If you want to obtain whether the transferFrom is successful, or want to obtain the details of the transferFrom transaction in the future, you can obtain it in the following way in the Token Standard implemented by Deland:

  1. Get Token’s external storage canister Id: tokenGraphql: () → (principal) query;

  2. Get your tx details through sudograph query: “graphql_query”: (text, text) → (text) query;

for example: dfx canister call graphql graphql_query ‘(“query { readTx(search:{ txid:{eq:“your transcation id”} }) { id,txid,txtype,from,to,value,fee,timestamp} }”, “{}”)’

1 Like

Why choose sudograph as separate canister tx storage?

Sudograph can provide richer query support.

You can learn more from Sudograph book

3 Likes

let result = await canister.transferFrom(from, to, amount);
let b = 100/0; or other exception occurs here…I’ll never get transaction ID.

Whether a function should be provided, hashID:(from, to, amount) → txid; This txid is equal to the ID returned by transferfrom().

The above code should be written as follows:

let txID = hashID(from, to, amount);
writeToRecord(txID); //record transaction id
let result : Bool = await canister.transferFrom(from, to, amount);
let b = 100/0; exception occurs here,but I got txID。
Then through the query interface, I can determine whether my transaction is successful

As far as I know, the answer is no.

Whether a function should be provided, hashID:(from, to, amount) → txid; This txid is equal to the ID returned by transferfrom().

You can do it like this:

let transferResult =canister.transferFrom(from, to, amount). await;
match transferResult {
   TransferResult::Ok(txid, inner_errors_opt) => {writeToRecord(txid); },
   _=>{}
};
let b = 100/0;  // exception occurs here,but you can got txID。

The above code should be written as follows:
let txID = hashID(from, to, amount);
writeToRecord(txID); //record transaction id
let result : Bool = await canister.transferFrom(from, to, amount);
let b = 100/0; exception occurs here,but I got txID。
Then through the query interface, I can determine whether my transaction is successful

let b = 100/0;Just think of an example, What I want to express is: will my own canisters suddenly break the link with other canister or the cycle is suddenly stopped due to lack of cycles. if so ,the following code has no chance to be executed
match transferResult {
TransferResult::Ok(txid, inner_errors_opt) => {writeToRecord(txid); },
_=>{}
};

Let’s replace these Web2 social platforms with IC Native equivalents:

  // Return all of the extend data of a token.
  // Extend data show more information about the token
  // supported keys:
  // OFFICIAL_SITE
  // OFFICIAL_EMAIL
  // DESCRIPTION
  // BLOG
  // DSCVR
  // OPENCHAT
  // DISTRIKT
  // WEACT
  // NUANCE
  // ETC…
  // GITHUB
  // DISCORD
  // WHITE_PAPER

Good idea, thanks for your suggestion.
I will add the IC Native equivalent.

We have added DSCVR, OPENCHAT, DISTRIKT, WEACT to the list of keys supported by extend info.

Improve: add IC native social media(DSCVR,OPENCHAT,DISTRIKT,WEACT)

But I didn’t find any information about NUANCE, so I haven’t added it yet. If I find information about NUANCE, I would like to add NUANCE to the list, and any PRs are welcome.

Thanks @ililic for the suggestion again.

The token standard is an important basic standard in the dfinity ecosystem. The establishment of a complete standard requires everyone to participate. Anyone is welcome to comment and submit a PR

2 Likes

The motoko implementation has been completed, but the call of approve and transfer has not been implemented for this reason.

how to test Motoko implementation?

git clone from here , then run

make test_motoko
3 Likes

Hey everyone! Consider checking out Non Fungible Token (NFT) Standard [Community Consideration] this thread if you’re interested in some new ideas around token standards.

3 Likes

DeLand Labs DFT latest update:

  1. Dropped Dfinity Self Describing Standard

Motoko canister support default method __get_candid_interface_tmp_hack to get did, if rust canister implement this method ,Developer can check canister’s interface through __get_candid_interface_tmp_hack .

candid::export_service!();

#[query(name = "__get_candid_interface_tmp_hack")]
#[candid_method(query, rename = "__get_candid_interface_tmp_hack")]
fn __export_did_tmp_() -> String {
    __export_service()
} 
  1. Rust implementation has removed supportedInterface(text) -> (bool)

Deland Labs will continue to improve DFT.Come on with us to improve it,any comments, PR are welcome.

2 Likes

Updates summary:


  owner : () -> (principal);
  setOwner : (principal) -> (bool);
  setExtend : (vec KeyValuePair) -> (bool);
  setFee : (Fee) -> (bool);
  setFeeTo : (text) -> (bool);
  setLogo : (vec nat8) -> (bool);

  allowancesByHolder : (text) -> (vec record { TokenHolder; nat }) query;
  tokenInfo : () -> (TokenInfo) query;

  lastTransactions : (nat64) -> (TxRecordsResult) query;
  transactionById : (text) -> (TxRecordResult) query;
  transactionByIndex : (nat) -> (TxRecordResult) query;

The repository is here , Leave your comments & advices to make it better , and any PR are welcome..

Updates summary:

  • add burnable & mint extension , and example code
  • refactor code

How to use rust to create a fungible token with 1 line of code?

check it out at here