Thoughts on the token standard

Thanks for sharing this. I just took a look, and there’s a lot of stuff in there.

I’m gonna focus on transaction atomicity and try to figure out whether this proposed interface can solve the standard DEX user journey.

In this user journey, we have the following entities:

  • Alice, who wants to swap Token A for Token B
  • Token A canister
  • Token B canister
  • DEX canister (assume there’s only one canister for simplicity)

Now, this is the typical ERC-20 flow that would work on Ethereum:

  1. Alice calls approve on Token A canister to let DEX canister transfer some number of Token As on her behalf
  2. Alice calls some custom function (e.g. swap) on DEX canister to initiate the swap, which does the following:
    a. DEX canister calls transferFrom on Token A canister to transfer Token As from Alice’s address to the DEX canister’s address (to add to its Token A reserves)
    b. DEX canister internally calculates how many Token Bs Alice should get based on some formula like x * y = k
    c. DEX canister calls transfer on Token B canister to transfer Token Bs from DEX canister’s address to Alice’s address (which subtracts from its Token B reserves)
    d. DEX canister returns true on Alice’s swap call if steps 2a, 2b, and 2c were successful; otherwise, it returns false and the whole transaction is atomically rolled back

Note that:

  • The DEX canister makes 2 inter-canister calls: transferFrom and transfer. This example assumes that inter-canister calls are atomic because they are in Ethereum. On IC, they are not.
  • transferFrom is used for Token A but transfer is used for Token B. This is intentional because the only 3rd party transfer (i.e. Alice authorizing DEX canister to transfer on her behalf) happens for Token A. For Token B, the entity that owns the tokens and who initiates the transaction is one and the same, i.e. DEX canister.
1 Like

OK, so how would we use this new DRC20 interface to implement this same flow?

Based off what I read, here is my guess (please correct me if I’m wrong):

  1. Alice calls approve on Token A canister, same as before
  2. Alice calls some custom function (e.g. swap) on DEX canister to initiate the swap, which does the following:
    a. DEX canister calls lockTransferFrom on Token A canister to prepare to transfer Token As from Alice to DEX canister, with itself as the “decider”
    b. DEX canister internally calculates how many Token Bs Alice should get, same as before
    c. DEX canister calls lockTransfer on Token B canister to prepare to transfer Token Bs from DEX canister to Alice, with itself as the “decider”
    d. Now, depending on whether if steps 2a, 2b, and 2c were successful, DEX canister does different things…
    • If all were successful, DEX canister calls executeTransfer on both Token A canister and Token B canister to complete the transfers (using sendAll as the execute type). Once both executeTransfer calls complete successfully, DEX canister returns true to Alice
    • If at least one step failed, DEX canister calls executeTransfer on both Token A canister and Token B canister to rollback the transfers (using fallback as the execute type). Once both executeTransfer calls complete successfully, DEX canister returns false to Alice

This assumes that executeTransfer must succeed if lockTransfer (or lockTransferFrom) succeeds.

^ Does this flow sound right to you? @bitbruce

You are right in your understanding.

lockTransfer/executeTransfer is an abstract base function for improving the atomicity of Tokens on the IC. It can be applied to many scenarios. SWAP is one of them.

executeTransfer may fail. Dex canister can remedy this. It can execute executeTransfer again. To be safe, Dex canister should execute Token A canister’s executeTransfer first.

2 Likes

I thought the whole point of lockTransfer returning true is so that executeTransfer doesn’t fail. In a two-phase commit protocol, isn’t the point of phase 1 so that phase 2 won’t fail?

Also, I’m curious why DEX canister should execute Token A canister’s executeTransfer first.

If you could provide a bit more documentation on how you expect clients to use this standard, I think it would be very helpful to the community.


General observations about the standard:

  • The standard is a little bloated. Is it necessary to continue to support the approve / transferFrom flow? ERC-20 has it for historical reasons, but since we are developing a new standard in IC, we don’t have to follow in their footsteps. And my understanding is that callback-based approaches like transferAndCall (or in this case, pub/sub) is the next-gen replacement for approve / transferFrom.
  • I’m not sure I understand how gas works. Why do you let users pick between paying gas in tokens or cycles? If they choose to pay in tokens, how will the token canister convert those tokens to cycles, since it can only burn cycles to continue running? Why is there a distinction between gas and setGas to begin with?
  • We might need certified variables to ensure that query methods (e.g. balanceOf) return the correct content, without resorting to slow updates. This is what the ICP ledger canister does, and it’s not a trivial implementation. For example, they store ICP transactions in some sort of Merkle tree as an internal data structure in the canister.

Also, I’m curious what you plan on doing with this standard. Are you also implementing a wallet or DEX? Thanks for the great work.

2 Likes

You are right, it was my misunderstanding of your example.

I talked about another scenario: if Dex has token pool (with Token A and Token B), and if Aice wants to exchange Token A for Token B (transfer Token A to Dex pool and get Token B from Dex pool), then Dex should first execute Token A canister’s executeTransfer before executing Token B canister’s transfer.

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.

Let sender pay for gas, the purpose is to prevent ddos attack. The charging method is cycles, token, if token is charged as gas, it will be destroyed (default) or sent to FEE_TO, canister’s cycles need to be provided by someone else; if cycles are charged as gas, it will be used as canister’s cycles (default) or sent to FEE_TO.

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.

2 Likes

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