From Constant to Decaying Flow — An Overview

Miao
Superfluid Blog
Published in
7 min readApr 6, 2022

Superfluid protocol is known to allow anyone to send a stream of money to another in constant flow rate forever without upfront liquidity lock-up, so long as the sender has available balance in its account.

This alone already makes the Superfluid protocol stand out as a unique protocol in Web3 space. Together with its powerful composability, Superfluid protocol is fostering a strong ecosystem in its own right.

But what about non-constant flow rate, e.g. some x-axis mirror of an exponential function?

If this is not yet a compelling enough reason to look into it more, see this:

How Decaying Flows Look Like

First, let’s have some intuitive understanding of how a decaying flow behaves.

Same as the money streaming in constant flow rate, it involves a sender and a receiver. But instead of a constant flow rate, there are two settings you can tune with:

  • Distribution limit — an amount of money will be distributed from sender to receiver eventually, but it takes forever.
  • Half-life of the distribution — how long it takes since the creation of the flow for the sender to send half of the distribution limit to the receiver?

Example A:

  • Alice sends a decaying flow to Bob with a distribution limit of 1000 and half-life of 7 days.

This would be how it looks like:

Example B:

A slightly more complex example:

  • with the same half-life 7-days for all streams,
  • Alice sends Bob a decaying flow with distribution limit of 1000 from day 0,
  • Alice sends Carol a decaying flow with distribution limit of 1000 from day 7,
  • and Dan sends Alice a decaying flow with a distribution limit of 2000 from day 14.

This would be how it looks like:

Mixed Distribution Half-life

What about different distribution half-life for each stream? There is some limitation in what size of the set of distribution half-life settings we can include in the system, mainly due to storage scalability. This will be explained in the next section. But just as an example:

  • Alice sends a decaying flow to Bob with a distribution limit of 1000 and half-life of 7 days from day 0,
  • Alice sends a decaying flow to Bob with a distribution limit of 1000 and half-life of 30 days from day 7,
  • Carol sends a decaying flow to Alice with a distribution limit of 2000 and half-life of 30 days from day 14,

This would look like:

Solutions for Superfluid Protocol

Now that we get a general feel of how the decaying flows look like, how can we implement them in the Superfluid protocol?

The key building blocks for the Superfluid protocol are called “agreements”, they provide real-time balance formulas to the accounts, and operations for managing ongoing “contractual agreements” between accounts.

Let’s call our new agreement DFA (Decaying Flow Agreement), where we at least need to find these formulas:

Formula: rtb(aad: AAD, t: Timestamp)

This provides real-time balance to an account (whose data related to DFA is aad or agreement account data) at time t.

Here, we simply use the decaying flow agreement definition:

To remind ourselves how these formulas look, I recommend you to use https://www.wolframalpha.com/ and simply type “e ^ -t” and play around with different parameters. To understand all these parameters:

ε — By limit t to +∞, it is evident that ε is the maximum theoretical provided balance by DFA to the account.

λ — The “decaying factor”, how fast the rate of money streaming decays. Note that different λ would result in different terms, and they cannot be combined, hence as mentioned above, this agreement is not “storage scalable” for all λ. A practical implementation would have to limit only to a set of practical λ for users to choose from.

α — Amplitude to the decaying curve, it is a “state” settled each time an account receives or sends a new DFA flow of the same λ.

ts- The time that the DFA agreement data is last updated for this account.

Solution: mempty_update_with_acd(acd: ACD) -> ACD

This solution is needed for the problem: if the DFA agreement account data (acd) of the account is empty (or mempty, aka. empty monoid, since acd is also secretly a monoid if you are interested to dig into it more), or in another word an empty account for DFA, how to apply DFA agreement contract data (acd) and output a new aad?

Let’s use sagemath symbolic calculus engine to solve this slightly trivial problem so that we can use its output to solve a later more complex problem:

class ACD:
def __init__(self, suffix = ""):
self.distributionLimit = var("theta" + suffix)
self.updatedAt = var("t" + suffix)
def solve_aad_mempty_update_with_acd(newACD: ACD) -> AAD:
aad = AAD()
t = var("t")
solution = solve(
[ rtb(aad, newACD.updatedAt) == 0
, rtb(aad, t).limit(t = oo) == - newACD.distributionLimit
, aad.settledAt == newACD.updatedAt],
[aad.alpha, aad.epsilon, aad.settledAt])
assert(len(solution) == 1)
aad_prime = AAD()
aad_prime.alpha = aad.alpha.subs(solution[0][0])
aad_prime.epsilon = aad.epsilon.subs(solution[0][1])
aad_prime.settledAt = aad.settledAt.subs(solution[0][2])
return aad_prime
acd1 = ACD("_1")
aad1 = solve_aad_mempty_update_with_acd(acd1)

The solution given by the sagemath is:

Solution: mappend(aad1: AAD, aad2: AAD) -> ACD

Since we have already disclosed that aad is actually a monoid, to combine two aad together it is also known as a monoid append operation.

While it seems just a fancy way of saying how to combine two account states together, it is at the same time an extremely useful mental model for defining how to update the same account state when it is sending or receiving a new DFA flow. Because such operation could reuse the previous mempty_update_with_acd building block, as if the same account just created/received its first DFA flow, and then use it to “mappend” to its actual state. Let’s use sagemath again for modeling this:

acd1 = ACD("_1")
aad1 = solve_aad_mempty_update_with_acd(acd1)
solve_aad_mappend(AAD(), aad1)

Now all we need to define is the actual solve_aad_mappend solution finder:

def solve_aad_mappend(aad1: AAD, aad2: AAD) -> AAD:
t = var("t")
# Next account state
aad_prime = AAD("_prime")
# New account state
equition0 = rtb(aad_prime, t) == rtb(aad1, t) + rtb(aad2, t)
# Solution (with assistant)
aad_prime.settledAt = aad2.settledAt
aad_prime.epsilon = aad1.epsilon + aad2.epsilon
equition1 = rtb(aad_prime, t) == rtb(aad1, t) + rtb(aad2, t)
solution = solve(equition1, aad_prime.alpha)
assert(len(solution) == 1)
print("solved:", solution[0])

The output of the solution in solve_aad_mappend(AAD(), aad1) is:

This is how the α is settled each time a new DFA is received or sent for an account.

As you can see, no matter how many times the mappend operation is applied, there is only one α variable required for each λ. This is what’s informally called “storage scalable”, and that’s the secret of how Superfluid protocol can scale for millions of constant flow, and that’s how Superfluid protocol will scale for decaying flow too for a limited set of chosen lambdas.

Solution: Half-life of Distribution

Sagemath can also conveniently re-calculate the well-known half-life formula from any decaying curve:

def solve_half_life():
t_0 = var("t_0")
t_h = var("t_h")
theta = var("theta")
aad = AAD()
aad.settledAt = t_0
aad.alpha = theta
aad.epsilon = -theta
solution = solve(-theta == 2 * rtb(aad, t_0 + t_h), t_h)
print("Solution:\n\t", solution[0])
print("\n")

The result is conspicuously similar to what you can find from the Wikipedia page:

This will help us to choose the set of λ that will serve the most users for the most often use cases.

Full Solution Code

The full solution using sagemath can be found at https://github.com/superfluid-finance/protocol-monorepo/blob/dev/packages/spec-haskell/maths/DFA.py

What’s Next

As of the time this article was written, the Decaying Flow Agreement has been prototyped in the Superfluid spec-haskell project.

To bring it to the Superfluid Protocol EVM v1 production environment, an efficient exponent data structure and its math library needs to be chosen or created that is suitable for the DFA agreement to be implemented.

Furthermore, to allow users to have more flexibility in configuring the decaying curves, a third setting allowing our sentinel to close other’s flow at an arbitrary distributed-so-far amount smaller than the distribution limit can also be interesting to add.

The discussion of this topic will be available in our future public protocol forum video.

If this article inspires you to contribute to the Superfluid Protocol, join our discord server and leave us a message!

--

--

Miao
Superfluid Blog

https://miaozc.me/ mottoes: "Tu ne cede malis, sed contra audentior ito." "I have fought a good fight, I have finished my course, I have kept the faith."