Thoughts on the token standard

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!!

2 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!

2 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!

12 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.

1 Like

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

These are all great thoughts. I’ll add a few to the mix:

Re: Subscribe - Please don’t call it subscribe! Use notify. We will soon want to support subscriptions(the ability to take a certain amount of tokens every (x) time period and it will get really confusing. see Subscription Services on the Blockchain: ERC-948 | ConsenSys

Re: Notify(instead of subscribe) - This is a great solution in as much as a canister can notify another canister with some metadata, it may be nice to be able to return something more than a true/false.

Re: OpenCan - I’ve seen @dostro pushing us over there a good bit. Perhaps if you gave some instructions, best practices, rollout plan, etc. Right now it just looks like a github site that you translate into a static site and push to the IC? Are we supposed to just use the github issue features? Who controls the github site? How does publishing work? This forum gets a good bit of attention and a lot of people are directed here. It has some great community and notification features. I’m all for moving onto an IC based system or documenting our findings on OpenCan, but until it has the features that we have here it may not be time yet.

Finally, I’ll propose that we don’t really need giant standards. Little pieces of candid interface seem to work well. We could add a meta layer on top as well.

Given the following type:

//this is the DIPs type file
module {
    type Meta = actor{
       dip_supports: query (dip_feature: Text) -> async Bool;
    }
    type MetaData = [byte];
    type Payment_Notifiable = actor{
       notify_of_payment: (amount: Nat, metadata: ?MetaData) -> async ?Payment_Notifiable_Response;
    }

    type Payment_Notifiable_Response = ?MetaData
}

Then you can do something like the following

public shared(msg) func transfer(recipient : Principal, amount : Nat) : async Bool {
  //handle transaction stuff
  //....

  let metaActor : DIP.Meta = actor.fromPrincipal(recipient);

  //figure out if the principal is a subscriber
  let bWantsNotification : Bool = wantsNotification(principal);
    
  if(bWantsNotification and metaActor.dip_supports('DIP_Payment_Notifiable') == true){
    let notifiable : DIP.Payment_Notifiable = actor.fromPrincipal(recipient);
    let response: DIP.Payment_Notifiable_Response = notifiable.notify_of_payment(amount, metadata);
    //do something with the response
  };
}

The canister requesting notification will need a “dip_supports” function that returns true for the string “DIP_Payment_Notifiable”. I think you’d generally want to say that this should not be a dynamic set of supports, but hardcoded into your canister.

2 Likes

Yup, this is just a super short term restriction, we’re just rolling everything out in steps hence the restricted and unrestricted subnets.

If you want to start developing, I’d advise just deploying a new ledger canister to a subnet and disabling the whitelist. The ledger doesn’t do anything NNS specific so it runs fine anywhere.

1 Like

You can fill this DIP module with other ‘standards’ that the community comes up with:

I don’t think that Motoko supports multiple inheritance right now, but it would be nice if it did so that we could do the following and get compile-time checks:

 //this is the DIPs type file
    module {
        type Meta = actor{
           supports: query (dip_feature: Text) -> async Bool;
        }
        type MetaData = [byte];
        type Payment_Notifiable = actor{
           notify_of_payment: (amount: Nat, metadata: ?MetaData) -> async ?Payment_Notifiable_Response;
        }

        type Payment_Notifiable_Response = ?MetaData

        type Token_Metadata = actor {
          name: query ()  -> async Text;
          symbol: query () -> async Text;
          decimals: query () -> async Nat;
          totalSupply: query () -> async Nat;
          balanceOf: (address: Principal) -> async ?Nat;
        }

        type Token_Transferable = actor {
          transfer: (recipient: Principal, amount: nat) -> async Bool
        }
type Token_Allowances = actor {
      transferFrom: (sender: Principal, recipient: Principal, amount: nat) -> async Bool;
      approve: (spender : Principal, amount : Nat) : async Bool);
      allowance: (owner : Principal, spender : Principal) : async ?Nat;
    }

    type Token_Mint_And_Burnable = actor {
      mint: (to: Principal, amount: Nat) -> async Bool;
      burn: (amount : Nat) : async Bool);
      canMint: query (controller: Principal) -> async Bool;
      canBurn: query (controller: Principal) -> async Bool;
    }
       updateSubscriptionAddress: (
        subscriptionId: Hash,
        payeeAddress: Principal
      ) -> Bool
      //etc see erc948
    }

I don’t think that Motoko supports multiple inheritance right now, but it would be nice if it did so that we could do the following and get compile-time checks:

shared(msg) actor class CoolCoin(_name : Text, _symbol : Text, _decimals : Nat): async DIP.Token_Transferable, DIP.Token_Allowances, DIP.Token_Metadata {}

What is a short term restriction? That canisters can’t send ICP? This will go away eventually?

Yup and any tokens they’ve recieved will become accessible to the canister.

3 Likes

Another idea I’ve had, but not explored too much yet is that what if the token canister didn’t actually hold any balances and was just a router to wallet actors that hold the balances? This way the token canister needs far fewer cycles and people can be in charge of funding their own infrastructure?

2 Likes

If you make a canister that does delegated identities you can do more than that, you can pick what user or users holds a certain account.
It’s the same feature that the Identity Provider uses to share identities on stuff like the NNS UI.

2 Likes

The compacting GC is about to get merged to master as a command line option to the compiler. I don’t think it solves the issue that dmd rightly points out, but will give access to more heap.

Doing less aggressive GC should be possible but we haven’t investigated it yet. That isn’t a fundamental problem with Motoko the language, just our current GC implementation.

Motoko will, I think, be open source in a matter of days or small number of weeks. The main issues are degree of history preservation, sorting our CI dependencies so they are publically available and, ideally, ensuring the same experience for Dfinity and external contributors.

12 Likes

I’m not sure I understand why we can’t implement this interface in Motoko.

As this example illustrates, we can share functions among actors to allow dynamic calling.

3 Likes

This sounds amazing and I don’t know that we have a real example of what is happening under the hood with this. Any good examples that would help us understand this paradigm better?

Yes please do this. I would love to know exactly how icp is broken down and how I can set up my own sun governance system
On my own project

3 Likes

Thanks for the enlightenment!

PrincipalId vs AccountIdentifier

Using AccountIdentifiers could run into composability issues. My token in subaccount 0 doesn’t know about tokens in subaccount 1. Slightly better privacy, but at the cost of convenience. I’m used to the Ethereum ecosystem where one account = one account, representing all my assets and transaction history, which can be plugged into any application.

In general, having both PrincipalId and AccountIdentifier increases friction for users and devs - why do I have two IDs? Why can’t I just use one? Why does this one have dashes but this one looks like an eth address? and so on…

I believe we should use PrincipalId as the one and only identifier. Would it be possible to upgrade the ICP ledger to support this? The existing entries will have to be migrated and subaccount data could be lost, but I think the tradeoffs are worth it.

Alternatively, if SubAccounts are here to stay, then we’ll want a registry to lookup PrincipalId from AccountIdentifier. Or, maybe we can change the implementation of AccountIdentifier to remove hashing.

14 Likes