Thoughts on the token standard

I had some thoughts on the token standard. Several payment flows have been discussed in this thread and other threads and interfaces have been proposed for the flows. Among them were, for example, 1) a 1-step push transfer initiated by the sender, 2) a pull transfer following a pre-approval by the sender (2-steps in total), etc. I thought about how to generalize the different flows into one concept with a single interface. The way to unify the flows is to create symmetry between sender and receiver. Here is what sender and receiver have in common in my model:

  1. Both have to approve of the transfer (i.e. no direct deposit into a receiver’s account without approval of the receiver).
  2. Both have a certain balance differential as a result of the transfer, positive for one, negative for the other.
  3. Both can initiate the transfer, leaving the other to accept.

I will discuss the reasons why 1. is acceptable in a subsequent post, as to not interrupt the exposition here, but briefly summarized they are as follows:

  1. A token transfer is in general an exchange in which the receiver of the transfer also delivers something to the sender at the same time when the transfer is made. It is therefore intuitive if the receiver also approves.
  2. Direct deposits into arbitrary accounts are problematic. There are reasons to disable them, at least by default.
  3. Direct deposits can be re-introduced as a special case, if desired.

Specification

type Transfer = vec Part;
type Part = record {
    of : principal;
    tokens : opt record { subaccount : Subaccount; amount : int };
    memo : opt blob
};
type Subaccount = blob;
type TransferId = nat64;

service : {
    request: (Transfer) -> (variant { Ok: TransferId ; Err });
    accept: (TransferId) -> (variant { Ok; Err });
    reject: (TransferId) -> (variant { Ok; Err });

    // queries
    transfer_details: (TransferId) -> (variant { Ok: Transfer; Err });
}

A simple transfer from principal A to B has two “parts”, one of A with a negative amount and one of B with a positive amount. The general flow is like this:

  1. Both parties negotiate out-of-band the transfer details
  2. Either party calls request to propose and initiate the transfer, providing all transfer details in the call.
  3. The party who called request transmits the TransferId to the other party out-of-band
  4. The other party calls accept with the TransferId
  5. The ledger executes the transfer

The rules are:

  • if any principal who is party to the transfer calls reject then the pending transfer is immediately deleted
  • accept calls get recorded, the transfer is executed as soon as all principals who are party to the transfer have accepted
  • calling request implicitly counts as accepting, so the initiating party does not need to also call accept

Further points:

  • transfers can have more than two parties, the generalization is straight-forward, only requirement is that all token differentials sum up to 0

  • any party who has already accepted, including the requester, can call reject if they changed their mind while the transfer is still pending

  • the principal who calls request does not necessarily have to be a party to the transfer, (however the requester should pay fees, see “Fees” below)

  • the tokens field in the record is optional. If it is omitted this means that the principal has the role of a third-party “committer” as in the “approveTransfer” proposal that came up in the ICRC-1 thread

Fees:

  • it is expected that the implementation stores the details of a pending transfer under the principal who calls request
  • that principal should pay the fees associated with the resource consumption of storing a pending transfer
  • it is up to the implementation to design fee structure and DoS prevention

Extensions:

  • a principal can set a flag called auto-accept for itself. If this flag is set then any transfer with a positive balance differential for the principal is counted as accepted without further interaction. This enables direct deposits as an opt-in feature on a per-principal basis

Payment flows:

  • 2-step push: sender requests, receiver accepts. similar to a check deposit

  • 2-step pull: receiver requests, sender accepts. similar to an e-bill or credit card payment with push notification and in-app approval

  • the previous “approveTransfer” proposal: a third-party, different from the receiver, is listed as a principal without amount, i.e. acts as a “committer”.

  • 1-step push: requires the auto-accept extension, sender deposits directly into the receiver account

  • atomic multi-party payments: A pays C but only if B also pays C

What do you think about this interface?

8 Likes