Thoughts on the token standard

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?

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

13 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

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

15 Likes

This is a hurdle for users, I thought the same. I’ll second that.

3 Likes

It was explained by dmd earlier:

“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.”

1 Like

Yeah my preference is Principal - it’s a lot easier to work with. Upgrading ledger icp to work with principal would be a good idea.

1 Like

FYI, the datastruct used in ledger canister.


14 Likes

Disclaimer: I’ve not read all the past discussions, please bear with me if this point is already discussed.

An important difference between the canister messaging model and ethereum/EVM call stack is that the former has a finer granularity on atomicity: it only rolls back one canister state where an exception happens, not the complete call stack like in EVM.

This has consequences. Consider the classical example of transactional behavior: I plan to take a train to see a concert. I’ll only go if I can successfully purchase both a train ticket and a concert ticket. If one succeeds and the other fails, I ended up wasting money (unless there is a way to refund, but let’s ignore that for a sec). So the desirable behavior is that I either purchase both tickets, or neither.

To do this with Ethereum is easy, but on IC it is not so simple if you have to buy train ticket from canister A and concert ticket from canister B. This difference is due to a conscious design choice in the canister model, because the EVM “roll-back everything” semantics essentially is equivalent to imposing a global lock, which limits scalability. For canisters/actors, it is much natural to limit the atomicity to a single canister, instead of across all canisters.

So IC as a platform does not support cross-canister transactions. If we want to do cross-canister transactions, we’ll have to encode this functionality into application logic instead.

The reason I bring it up in this thread is because I think a token standard should leave room to enable cross-canister transactions. An atomic transfer function is fine for simple applications, but will have trouble when we want to compose two or more transfers across multiple canisters. So this is something perhaps this discussion group should think about.

A two-phase commit kind of interface (prepare & commit) should not be difficult either, given that each update call is atomic, and we can have both success and failure callbacks when doing inter-canister calls. I won’t go into too much detail here, and I’m sure this group will come up with a good solution.

16 Likes

Yes, and this is a very important remark. Thanks @PaulLiu.

I want to elaborate on this a little and to try to prove you once again that the only thing we need in tokens is pub/sub, and we certainly don’t need approvals.

First, let’s see how one could implement such a transaction (a buyer wants to buy a concert ticket, but only with a train ticket to get to this concert in time) on Ethereum, using ERC20 for example.

We have:

  • a buyer with some amount of ERC20 tokens for payment;
  • a train ticket provider with some NFTs representing train tickets (let’s imagine it’s ERC721 with the same approvals functionality);
  • a concert ticket provider with some NFTs representing concert tickets (also ERC721)

It’s obvious that we need some kind of a middle-man in this scenario in order to ensure security of the process - a separate smart contract (SC) with publicly available source code which can automate this process for us and provide an escrow.

Note: approval in ERC20 is essentially an escrow itself - we lock our assets in a temporary area which is controlled by both: us and the middle-man, until the middle-man ensures validity of this transaction and executes all the needed actions for us to proceed. So, in ERC20 (and ERC721) we have an escrow enabled by default.

So, what exactly the process could be? Let’s walk through it step-by-step:

Precondition: the train ticket provider and the concert ticket provider approve to the middle-man SC all of their ticket tokens which they’re willing to sell.

  1. The buyer approves to the middle-man SC a quantity of tokens for payment they think is enough to cover all expenses.
  2. The buyer creates and sends to the middle-man SC an application which is essentially a statement like “I want to buy a ticket on this exact concert and to also buy a ticket on this exact train”.
  3. The middle-man SC sees the application and checks if it has all the required assets allowed to fulfill the application.
  4. If it has - the middle-man SC transfersFrom all the tokens participating in this application to their destinations (success); if it has not - the middle-man SC simply rejects the application (fail) - in this case no transaction revert is needed, because parties could simply cancel the approval of their assets for the middle-man SC right after they see the failed transaction.

Will the same process with approvals work on the IC? No. And this is exactly what @PaulLiu did just said.
Ethereum is single-threaded - only one smart-contract is making progress at any given point of time.
The IC on the other hand is multi-threaded - multiple canisters are making progress simultaneously. It means that step #3 from the above algorithm is useless since it does not guarantee availability of the asset during the whole transaction (because assets are managed by different canisters, which make progress independently). In other words, you could simply fool such a system, approving money, sending an application and then quickly disapproving them back - there is a chance for you to spot the exact moment when the middle-man canister did already checked their allowance of your payment, but did not yet transferedFrom it.

How could one implement the same process using pub/sub? Let’s see:

Precondition 1: there exist token canisters to represent all: fungible tokens for payment, non-fungible tokens for train tickets and non-fungible tokens for concert tickets and all of them implement pub/sub capabilities like I described in initial posts of this thread.

Precondition 2: the middle-man canister is subscribed to all of these token canisters.

Precondition 3: the train ticket provider and the concert ticket provider did already transferred their ticket-NFTs to the middle-man canister and the latter acknowledged them (since it is subscribed to their transfers).

Precondition 4: the middle-man canister manages its internal balance list of each token it is subscribed to.

  1. The buyer transfers to the middle-man canister a quantity of tokens for payment they think is enough to cover all expenses.
  2. The buyer creates and sends to the middle-man canister the same application as from the Ethereum case.
  3. The middle-man canister sees the application, checks its own internal balance list if all conditions are met.
  4. If everything fine - the middle-man canister transfers all the tokens to their destinations fulfilling the application (success); if something wrong - the middle-man canister simply rejects the application (fail). No payment spent.
  5. If the buyer wants to get their money back, they could simply ask the middle-man canister for it and the latter will transfer them back.

It is something like using Binance - you can deposit some money, use them there any way Binance lets you, and then withdraw them back.

In other words, using pub/sub instead of approvals on the IC we’re moving escrow from multiple smart-contracts into a single smart-contract, which is doing things in a single thread.

3 Likes

Oh, god. Maybe you’re right! My bad.

I think you may be able to ‘be your own middleman’ in this scenario. If services have ‘reserve’ with an sla on that reserve then you can get back all your reserves before you commit them.

if((await a.reserve(concertTicket)) and (await b.reserve(trainTicket) and (c.reserve(hotel)){
   a.commit();
   b.commit();
   c.commit();
} else {

  await a.forfiet();
  await b.forfiet();
  await c.forfiet();
}