Thoughts on the token standard

Yea, this looks nice. I agree.

It would be cool to have something like Principal::empty().

1 Like

Agree it’s better if we have this alias in SDK.

But even without one, we can still use Principal::from_str("aaaaa-aa") for rust and Principal.fromText("aaaaa-aa") for motoko.

3 Likes

With a few changes to what I proposed it’s looking like:

type AccountIdentifier = Text;
type Balance = Nat;
type SubAccount = [Nat8];
type Metadata = {
  name : Text;
  symbol : Text;
  decimals : Nat8;
};
//Principal of sender, SubAccount of sender, Balance of transfer
type Callback = shared (Principal, ?SubAccount, Balance) -> async Bool;

type Receiver = {
  #account : AccountIdentifier;
  #principal : Principal;
  #canister : {
    principal : Principal;
    callback : Callback;
  };
};

type Token = actor {
  totalSupply: query () -> async Balance;

  metadata: query () -> async Metadata;
  
  balanceOf: query (who : AccountIdentifier) -> async Balance;

  transfer: shared (subaccount : ?SubAccount, recipient : Receiver, amount : Balance) -> async Bool;
};

It’s hard because some think we should stick with Principals for token holder IDs, and I would agree if the IPC Ledger also did it this way. But it becomes really hard if the addresses used for sending ICP is different to the addresses used for sending tokens built on the IC.

I ended up going with a callback submitted by the sender of a tx. A way it could work to stop funds being stuck in canisters is this:

  1. Alice initiats a transfer with a callback
  2. Token canister validates the tx, and puts the balance of the tx “on hold” (so Alice can’t spend while we await the callback)
  3. Await the callback - canister has to return true or false. If the callback fails because it doesn’t exist it will drop the state change anyway, if it proceeds and triggers the await then we continue. This stops funds being locked in canisters (which happens with ethereum)
  4. Canister responds with true or false. On true, the balance is added to the receivers account. If false, it is returned to the sender.
  5. Transfer responds with a bool as well so the sender knows what happened. Prob best to change this to a proper response type in future though

This to me seems very simple, allowing canisters to decide how they want to treat txs and giving them the ability to reject them if they wish. Is there a group chat/discussion where developers can chat about this? A unified standard is something I think should have some priority and I’m happy to contribute, but hesitant to continue building without something that is somewhat standardized.

EDIT: Actually could probably drop the subaccount from a #canister receiver as it’s unlikely that a canister dev will want to use multiple subaccounts…

2 Likes

@senior.joinu Thanks for sharing your thoughts. Your discussion with @hackape and @stephenandrews is a masterclass for someone like me who is an absolute beginner on blockchain tech. I picked up on ethereum and solidity a month ago with no real dapp programming experience. I will be following this thread and reading it multiple times :slight_smile:

4 Likes

It’s hard because some think we should stick with Principals for token holder IDs, and I would agree if the IPC Ledger also did it this way. But it becomes really hard if the addresses used for sending ICP is different to the addresses used for sending tokens built on the IC.

This is what I said earlier in this thread. I believe, we should not follow the same design decisions, which Dfinity took implementing ICP. Why?

Dfinity provided us with two default tokens on the IC: cycles and ICP.
Cycles are a stable coin which is by design used as a fuel for computations.
ICP is a volatile coin which is by design used as voting power on NNS.

None of them are designed to transfer value. They both have their own purpose. And this is very important. They have their own tasks and do them well. They do not follow any standard or convention, because solving the problem correctly is the first priority thing.

We’re, on the other hand, speaking about the standard for tokens which should transfer value and do it well. We’re solving another problem here.
So, in my opinion, we should do something that fits our case and serves our tasks and not to try treat any token the same way, like you want to do with ICP.

4 Likes

Welcome onboard! Good luck on you journey!

1 Like

Yep fair point. I think using something like the Receiver type could be a way that caters for both. SubAccount is optional, so if one was to completely ignore SubAccounts and only use the Principal as the identifier it would still work, but it does have backward (or I guess more sideways) compatibility with ICP. e.g.:

type Receiver = {
  #account : AccountIdentifier;
  #principal : Principal;
  #canister : {
    principal : Principal;
    callback : Callback;
  };
};

If you only know someone’s address used for sending ICP, you can still transfer tokens to them. If you want to transfer to a principal, you can do that as well (and it is just credited to the principals default subaccount which is 0). You could use the entire token canister and only use Principals and it would work (might need to adjust the balanceOf query though to accept Principals).

1 Like

@senior.joinu - Seriously love where your head is at! I went on the warpath advocating for subscriptions / events a few days ago - we should absolutely be leveraging the powerful parts of the IC.

Couple of thoughts for consideration:

  1. For subscription I think the candid type candid::Func should be used. I’ve harped about this on twitter - but It facilities exactly what you have envisioned. I have some rust and motoko examples of that here: tweak notify by SuddenlyHazel · Pull Request #1 · SuddenlyHazel/token-standard · GitHub

  2. For the primary spec I’m going to toss my voice towards using using Principals. I see too much room for error on the developer side using the Ledger spec. I will say the community should provide an extension format to facilitate playing nicely with the ledger for those who might need to, but I dont see that in the critical path.

  3. I dont care what the scheme looks like, but we need some concept of account operators or allowances. Without this users would only be able to act on their tokens from a single identity, likely the one generated from some token UI. How does this sound : only allow adding operator accounts that have full control of the users account. Why? Because this would encourage development of wallets / side contract canisters, likely with their own standards, that could encapsulate all the more funky logic.

4 Likes

Wait no scratch that - we might not even need operators at all - I guess this hypothetical “wallet”, “account manager” could just be the account controller. Then, canisters wishing to execute a payment could hit some wallet canister method, and then wait for the callback from the “safe” token account.

:boom: - More Dfinity Like

1 Like

The above flow might be made a bit more simple if send accepted something like

public type Metadata = // not sure;
public type Callback = {
  recipients : [shared (Metadata) -> ()]; // allow alerting multiple canisters from one call
  metadata : Metadata;
};

So, if we wanted to buy an NFT say:

  1. NFT Frontend redirects to this fictitious wallet canister frontend. Wallet frontend extracts payment info from url query param.
  2. User executes the transaction on the wallet.
  3. Wallet hits the token canister.
  4. Token canister does the transfer magic, and forwards the metadata to the accounts listed on the Callback Object
  5. One of those is our NFT Backend, it peeks at the metadata, verifies the transaction occurred (or maybe we just attach the needed info on the Callback Object) and does its own application magic.

Boom pretty seamless checkout flows on the IC. :white_check_mark:

Note : the Metadata passed by the wallet should be treated as unsecure. It should just be a hint to the recipient who can then check to make sure everything was valid.

1 Like

Yeah I agree with func's for callbacks - I know in Rust you have more flexibility, but it’s easier for Motoko to use the func type. I do the same with what I proposed and have used it effectively with the initial wrapped_cycles work.

I’ve adjusted what I proposed a few days ago, and it’s probably my ideal proposition: GitHub - Toniq-Labs/ic-fungible-token: Fungible/Stackable Token Standard for the IC - the main takeaways:

  1. Still supports Account Identifiers, but can be used to only work with Principals so it’s essentially sideways compatible. I’m not too concerned if this isn’t what the standard uses as I think Principal’s are easier to work with anyway
  2. Uses a subscription model, but not as open as @senior.joinu proposed with filters - I believe this should be handled by the receiving canister not the token canister. A simple callback with a bool return type allows for this
  3. Allows for the use of callbacks being defined in the transaction as well, but I have loosely defined some rules around how this works but open to discussion. It may make sense to remove this from the transaction args and only allow subscribers to receive callbacks?

Hm lots to think about :slight_smile:

@stephenandrews - I might propose your callback accepts TransactionMetadata so the entire flow is truely reactive. My motivation is we dont just want to tell a canister a transfer happened, we want to tell a canister a transfer happened, and give it a hint “why” is happened so devs can build truly reactive flows.

Continuing the discussion from Thoughts on the token standard:

@Hazel @stephenandrews @senior.joinu @geokos @hackape @harrison @ICVF

What do you think of having conversations on standards and improvement proposals take place on OpenCan?

Reason I make the suggestion is because:

  1. We need to start diluting the power of today’s two main neurons
  2. We need a long-term solution for where conversations about standards and improvement proposals take place (unity in collaboration will yield the best options)
  3. To be most efficient in regards to taking action on those conversations, we need a process for making proposals to the NNS (decentralized governance will help solve #1 and will yield the fastest innovation)

The current idea is to make OpenCan a developer-governed neuron for collaboration on improvement proposals and standards that, once approved within the dapp, get submitted for final voting to the NNS. As this would be its own neuron, it’s one we could delegate voting power to and therefore start diluting the power of today’s two main neurons.

There are already a couple threads on this token standard, but it will need adoption to work. What do you think of this idea?

2 Likes

Sounds good to me! I don’t really care what tokens look like, but we need callback schemes that are going to facilitate reactivity!!

3 Likes

I might propose your callback accepts TransactionMetadata so the entire flow is truely reactive. My motivation is we dont just want to tell a canister a transfer happened, we want to tell a canister a transfer happened, and give it a hint “why” is happened so devs can build truly reactive flows.

Yes that makes a lot of sense!

3 Likes

I agree, I started a new issue maybe more can be discussed here: Token standard for TIC · Issue #14 · OpenCan-io/opencan · GitHub

2 Likes

I’m not sure I fully understand this yet, but rather than nesting I would suggest introducing a new variant with 3 constructors.

Written in Motoko for convenience:

type Filter = {
  #None;
  #Empty;
  #Exact Principal;
};

or

type Transfer = {
  #Any;
  #CreateOrBurn;
  #Participant Principal;
};
2 Likes

I’m really excited to see you all already working on standardizing token interfaces.

I run the team that designed and built ICP, I hope we can find a standard that can work for you guys and ICP can adopt.

If we agree on a standard, not only will canisters be able to integrate new tokens easily, but also any centralized exchange that currently supports ICP will be able to support any other token with almost no technical work through our Rosetta API node.

I really need to write an article explaining the design of ICP some time because I think it will help people designing their own tokens.

ICP is pretty bare bones in terms of functionality, we put just enough into the contract to allow staking and exchange integration. The idea is that for everything else that you need you should be transferring funds into other canisters, so for example if you want ERC 20 functionality create an ERC 20 canister with the appropriate methods, transfer your tokens to a sub-account of that canister and let it manage them.

One of the big issues when designing a token is the memory limit on a single canister, so you either have to effectively shard your token or work hard to limit the amount of storage your data structures will use. This is why we didn’t follow ERC 20 on the ICP canister. I was concerned that an attacker could use approve to use O(number of accounts^2) storage space on the ICP canister.

The account identifier vs principal ID argument is a good one, we did it mainly to keep the cost of storage down, most of our transaction fee is going to go towards the costs of long term storage of transactions and storing Hash(Principal ID, Subaccount) vs (Principal ID, Subaccount) knocked about 25% off the transaction size. I’m not 100% sure it was the right decision because it makes it much less clear what’s going on in the ledger and makes integrations a bit more complicated with only a small gain in privacy so don’t feel obliged to cargo cult that one. Back of the envelope even on a large subnet ICP transactions should only cost about 0.001c, so 20% on top of that probably isn’t worth worrying about.

Oh and if you’re planning to deploy a token right now, use Rust. Currently the motoko GC has real problems running lots of small update calls on big heaps which is exactly the kind of workloads that tokens have.

I look forward to seeing this develop and let me know if anyone has any questions!

15 Likes

@dmd

Re: Using the ledger_canister: I had considered that we all should just use the ledger format in the past, but I still have the concerns in DeFi with ICP seems crippled(on purpose?) - #9 by skilesare which would be great if you could address. Why can canister’s send ICP? Would they also not be able to send tokens built on the same infrastructure?

Re: Motoko: What is the plan to fix the motoko GC? If we can’t use it for tokens at the moment, why should we learn it? It is hard to adopt something when you don’t know the road map. We also need access to create custom transactions from Motoko to future proof wallets.

2 Likes

I know you all have the compacting GC in the Pipeline, but if I could be so bold as to give a gentle push on it. My highly unscientific survey, suggests most people coming to the IC are looking towards Motoko as the goto language to get hacking with. I’ve intentionally been doing all my little projects in Motoko for this reason. The Dfinity Developer Discord appears to be very Motoko focused as well - rightfully so - it’s a great little purpose built language. Just my 2c :grinning_face_with_smiling_eyes:

also, would love to get access to the source of Motoko too :eyes:

cc @claudio

Edit 1 : haha GC, Pipeline, get it :sweat_smile:

Edit 2 : Maybe a better indicator on Motoko - https://twitter.com/MotokoSchool

2 Likes