Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for Streaming Token Standard #2100

Open
d10r opened this issue Jun 5, 2019 · 2 comments

Comments

Projects
None yet
2 participants
@d10r
Copy link

commented Jun 5, 2019

eip: <to be assigned>
title: Streaming Token
author: Dietmar Hofer <dho@lab10.coop>, David Forstenlechner <dfo@lab10coop>, Markus Teufelberger <mat@lab10.coop>, Peter Grassberger <pgr@lab10.coop>
discussions-to: https://github.com/ethereum/EIPs/issues/2100
status: Draft
type: Standards Track
category: ERC
created: 2019-06-05
requires: 20

Simple Summary

A standard interface for Streaming Tokens.

Abstract

The following standard extends the ERC20 Token Standard.
It adds an interface which allows tokens to be streamed between accounts.

Motivation

Atomic value transfers are not the ideal tool for economic transactions which are long lived.
Ethereum allows us to implement cheap continuous value transfers by leveraging block timestamps.

The interface proposed here offers an alternative to ERC-1620 for several reasons:

  • It's designed to extend the ERC20 Token Standard
  • It makes less assumptions about the implementation
  • It defines streams such that no settlement/withdrawal of/from streams is required

Specification

Preliminary definitions

A stream defines a directed relation between two accounts (sender and receiver).
A stream can be opened by the sender account and closed by the sender or the receiver account.
Once closed, a stream ceases to exist and can't be re-opened.
The stream balance is the amount of tokens which were transferred through a stream up until a given point in time.
The stream balance is defined by an implementation specific function which must be monotonically increasing in time.
now refers to the point in time defined by the block timestamp of the last known block.

Interface

interface ERC2100Token {
    enum CanOpenResult {
        OK,
        ERR_SENDER_RECEIVER_TUPLE,
        ERR_SENDER_QUOTA,
        ERR_RECEIVER_QUOTA,
        ERR_SYSTEM_LIMIT,
        ERR_FLOWRATE,
        ERR_MAXAMOUNT,
        ERR_OTHER
    }

    enum TransferType { UNDEFINED, ATOMIC, STREAM }

    function canOpenStream(address from, address to, uint256 flowrate, uint256 maxAmount) external view returns(CanOpenResult);
    function openStream(address to, uint256 flowrate, uint256 maxAmount) external returns(uint256);
    function getStreamInfo(uint256 streamId) external view returns(uint256 startTS, address sender, address receiver, uint256 flowrate, uint256 maxAmount, uint256 transferredAmount, uint256 outstandingAmount);
    function closeStream(uint256 streamId) external;

    // overrides ERC20 event, adding a field "transferType"
    event Transfer(address indexed from, address indexed to, uint256 value, TransferType transferType);
    event StreamOpened(uint256 id, address indexed from, address indexed to, uint256 flowrate, uint256 maxAmount);
    event StreamClosed(uint256 id, uint256 transferredAmount, uint256 outstandingAmount);

openStream

flowrate is the target amount of tokens per second to be transferred. If set to a non-zero value, the average flowrate of a stream over its lifetime (TODO: clarify that this refers to any possible time range from stream opening until an arbitrary later point in time?) must never exceed this value.
If set to 0, the sender doesn't pose an upper limit.

maxAmount is the maximum amount of tokens which can be transferred through a stream overall. Once that amount is reached, the stream balance can't change anymore and the stream loses its utility.
(TODO: add a method modifyStream which allows to increase the limit?).
If set to 0, the sender doesn't pose an upper limit.

Returns streamId which uniquely identifies the opened stream and emits an event StreamOpened.
Throws if a stream with the given parameters couldn't be opened.

canOpenStream

Implementations are free to impose constraints on which accounts can be stream senders or stream receivers, which combinations of sender/receiver are possible, and which values are accepted for flowrate and maxAmount.
This constant method allows to check beforehand if a stream with a given set of parameters could be opened, given the current state of the contract.
The return value of type CanOpenResult returns OK if such a stream could be opened, and an error code if not. The possible error codes are:

  • ERR_SENDER_RECEIVER_TUPLE if the combination of sender and receiver account is not allowed
  • ERR_SENDER_QUOTA if the sender account has achieved the limit of open streams
  • ERR_RECEIVER_QUOT if the receiver account has achieved its limit of open streams
  • ERR_SYSTEM_LIMIT if any other limit defined by the contract prevents the stream from being opened, e.g. a limit to the number of open streams which is not tied specifically to an account
  • ERR_FLOWRATE if the given value flowrate value is not supported
  • ERR_MAXAMOUNT if the given maxAmount value is not supported
  • ERR_OTHER for errors not covered by any other error code

getStreamInfo

This constant method returns some metadata and the current balance of an open stream identified by the given streamId.
Throws if no such open stream exists.

On success, it returns multiple values:

  • startTS is the timestamp of the block which contains the transaction which opened the stream.
  • sender is the address of the sender account which opened the stream
  • receiver is the address of the account which receives the tokens transferred through the stream
  • flowrate is the flowrate set when opening the stream
  • maxAmount is the maxAmount set when opening the stream
  • transferredAmount is the amount of tokens transferred through the stream until now
  • outstandingAmount is the amount of tokens which would additionally have been transferred, but weren't because of a lack of funds of the sender account

TODO: split into getStreamMetadata and getStreamBalance instead?

closeStream

Closes the stream defined by streamId.
At least the sender and receiver account have permission to close a stream at any time.
Emits an event StreamClosed where transferredAmount is the amount of tokens transferred through the stream and outstandingAmount is the amount of tokens which would additionally have been transferred, had the sender had enough funds.
Emits an ERC20 compliant event Transfer with the additional field transferType set to TransferType.STREAM.

Rationale

This standard is specifically NOT about replicating the behaviour of payment channels on-chain.
It is designed for implementations which allow for continuous transfers which don't require any settle-/withdraw-type transactions.

Challenges

The fundamental difficulty for implementations is to deal with possible underflow situations - that is, streams not transferring at their designated capacity due to insufficient funds of the sender account. In order to not allow streams to issue additional tokens out of thin air in such underflow situations, calculating the stream balance always requires knowledge about the balance of the sender account - which in turn may depend on the balance of streams it's currently receiving, etc. This can lead to potentially infinite recursion depths.

While it's possible to correctly implement this, the complexity budget of a transaction - hard limited by the block gas limit - forces implementations to define constraints which prevent the contract from reaching states at which it can get stuck, e.g. due to recursions hitting the stack limit of the EVM.
There's several ways how this can be prevented, e.g. by allowing accounts to not be sender and receiver of streams at the same time, or by limiting the recursion depth.
In order to reason about the complexity of calculating the stream balance, it's useful to model the state of a Streaming Token as a directed graph where nodes represent accounts and edges represent open streams.

Genericity

The optimal choice of constraints for safe complexity limits depends on the intended applications. The chosen interface thus doesn't make any assumptions about those and is instead kept generic and as simple as possible.

Implementations are however free to extend the interface with additional methods, e.g. for batch operations.

Implicit State Updates

Block timestamps (BT) are the core building block which make this kind of Streaming Token possible in the first place.
They change with every new block and their correctness is guaranteed by the consensus protocol. More specifically, the block timestamps are guaranteed to be strictly increasing (see its definition in the yellow paper). The formal specification doesn't mention a limit to the maximum allowed drift from the current system time of a node, but implementations do impose such a limit (TODO: check and link).
Since the EVM allows to read the timestamp value of the last block (in Solidity, that is done with block.timestamp), the block timestamp can effectively be used like a state variable for the current time which is updated for free with every block.
Since we can rely on the time never going backwards and since this specification mandates the stream balance to be a monotonically increasing function in time, we can construct contracts with implicit balance updates which don't depend on settle/withdraw-type transactions.

Flowrate

Typical implementations will have a linear stream balance function. In this case, the actual flowrate will be constant and equal to the value of the flowrate parameter, as long as the sender has funds.
Other implementations may have non-linear stream balance functions or even functions with dynamic parameters (e.g. a streaming exchange driven by a price oracle).
If an implementation accepts a non-zero value for the flowrate parameter, it has to guarantee that the average flowrate never exceeds that value.
There is also types of implementations which require an unlimited flowrate (value of the flowrate parameter set to zero), e.g. a stream based splitter which causes incoming tokens (no matter if they are received through an atomic transfer or through a stream) to immediately being forwarded to a defined set of receiver accounts.

ERC20 compatibility

While all of the ERC20 interface can be adopted / extended by this standard, the implementations of the ERC20 methods needs to be considerably different.
Most importantly, the method balanceOf of a streamable token can't be a simple mapping lookup, as is usually the case. Instead, it needs to calculate the current balance based on a more complex set of state variables, which may also involve recursions.

Some applications may assume that balance changes can take place only in the presence of a Transfer log event. This can result in an outdated account balance being shown.
The constant method balanceOf however always returns the correct balance. In order to always show the correct balance, applications monitoring the token balance of an account should call this method for every new block - as many (most?) are already doing anyway.

In order to avoid applications relying on Transfer events alone for token balance updates (e.g. Explorers) to permanently show wrong values, a Transfer event is emitted when streams are closed.
In order to allow ERC2100 aware applications to easily distinguish between Transfer events triggered by atomic transfers from those triggered by closed streams, an additional field transferType is set.

Overall, the interface makes sure that existing applications using the ERC20 interface can use ERC2100 tokens just like any other ERC20 token, without any guarantees specified by the ERC20 standard being violated.
The most likely cause for troubles may be applications using hardcoded gas limits, assuming all ERC20 implementations to look basically the same. That's why implementations of this standard should try to keep gas requirements as low as possible, especially for the ERC20 methods.

Implementations

Example implementations can be found here: https://github.com/lab10-coop/streaming-token-contracts

Copyright

Copyright and related rights waived via CC0.

@d10r

This comment has been minimized.

Copy link
Author

commented Jun 5, 2019

Details I'm not yet so sure about:

a) canOpenStream should make it easy for applications to check if a stream with the given participants and parameters can be opened. If not, it shouldn't be too difficult for the application to figure out why, thus the enum return type. On the other hand, returning a bool would feel more natural here.
And one could even argue that the method is superfluous, because the same result could then be achieved by making a call to openStream instead and see if it would succeed, relying on error messages associated to the exception. The latter would be my favorite approach (because simplest), but as far as I'm aware, getting such error messages is still a tricky business (afaik ganache delivers them, but do e.g. Parity and Geth?).

b) Transfer event: is there a risk I overlooked when using it with an additional parameter, as suggested?
Is it worth specifying the additional enum type TransferType or would it be better to take the minimalist approach and just add a bool which is set to true when emitted for a stream being closed?

c) Should openStream (and maybe also closeStream) have an additional bytes parameter for implementation specific data, as is e.g. the case in ERC-777? In this proposal I opted not to, because if the encoding of that data is implementation specific anyway, the implementation can as well add an external method which allows to hand over additional arguments - in a structured (properly typed) way; this may even be done by providing additional openStream methods overloaded with additional parameters.

d) Should getStreamInfo be split into two methods, one for (static) stream metadata and one for the current balance?

@SurfingNerd

This comment has been minimized.

Copy link

commented Jun 13, 2019

we have a video showing an example of this streams here: https://www.youtube.com/watch?v=TqVXTohUEpI.
A in depth description of the project can be found here: https://medium.com/lab10-collective/lab10-wins-infineon-blockchain-hackathon-and-streams-music-over-artis-mainnet-5fff84ffd140 .

source code for the 2 raspbery pi:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.