This specific one has been created by John Manoogian III and Buster Benson, who compiled the list of biases from Wikipedia.
It is a great way to get a sense of the sheer number of biases that exist, but it doesn’t tell you much about how much of the popular mindshare each bias has. All the biases having the same size implies that they are all equally important, but that is obviously not the case. Arguably, for someone who has just started to learn about cognitive biases, confirmation bias should be more important than, say, the Peltzmann effect.
To measure and visualize the popularity of each bias, I…
"<insert cognitive bias here>" cognitive bias
using a SERP API,Here is the result:
The bigger the font, the more Google search results there are for that bias, the assumption being Google search results are a good measure of popularity.
Why should you care about the popularity of biases? The more popular or common a bias is, the more likely you are to be affected by it. So it makes sense to study them in decreasing order of popularity, to maximize the benefit to your own thinking. However, this is all statistics—you could still be impacted more by a bias that is smaller in the wordcloud. For example, there was a time when I was very prone to the sunk cost fallacy, even though it doesn’t show up so large in the wordcloud.
Below is a version of the image without the shape mask:
Below are the top 10 biases ranked by Google search result count:
Cognitive bias | Search result count |
---|---|
Prejudice | 8,560,000 |
Anchoring | 1,100,000 |
Stereotyping | 1,080,000 |
Confirmation bias | 992,000 |
Conservatism | 610,000 |
Essentialism | 436,000 |
Loss aversion | 426,000 |
Attentional bias | 374,000 |
Curse of knowledge | 373,000 |
Social desirability bias | 319,000 |
Click here to see the search result counts for each 188 biases included above.
I have also computed the average search result count for each category of biases, by dividing the total search result count for each category by the number of biases in that category:
Category | Average count |
---|---|
We discard specifics to form generalities | 1,494,378 |
We notice when something has changed | 237,141 |
We fill in characteristics from stereotypes, generalities, and prior histories | 160,170 |
We are drawn to details that confirm our own existing beliefs | 93,350 |
We think we know what other people are thinking | 81,555 |
To act, we must be confident we can make an impact and feel what we do is important | 72,435 |
We notice things already primed in memory or repeated often | 70,835 |
To get things done, we tend to complete things we’ve invested time and energy in | 65,822 |
To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions | 65,750 |
We edit and reinforce some memories after the fact | 59,503 |
We favor simple-looking options and complete information over complex, ambiguous options | 52,491 |
We tend to find stories and patterns even when looking at sparse data | 46,375 |
To stay focused, we favor the immediate, relatable thing in front of us | 37,940 |
Bizarre, funny, visually striking, or anthropomorphic things stick out more than non-bizarre/unfunny things | 37,081 |
We imagine things and people we’re familiar with or fond of as better | 34,379 |
We simplify probabilities and numbers to make them easier to think about | 33,881 |
We notice flaws in others more easily than we notice flaws in ourselves | 31,390 |
We project our current mindset and assumptions onto the past and future | 29,418 |
We reduce events and lists to their key elements | 27,638 |
We store memories differently based on how they were experienced | 20,440 |
Notice that the top few biases such as prejudice and anchoring highly skew the ranking.
Similarly, I have computed the average search result count for each top category of biases:
Top category | Average count |
---|---|
What Should We Remember? | 316,297 |
Too Much Information | 101,842 |
Need To Act Fast | 64,568 |
Not Enough Meaning | 64,134 |
You can see the code I used to create the figure here.
I will not try to reason as to why some biases are more popular than others, and instead leave that for another post.
]]>tl;dr I created Manim Voiceover, a plugin for the Python math animation library Manim that lets you add voiceovers to your Manim videos directly in Python, with both AI voices or actual recordings.
This makes it possible to create “fully code-driven” educational videos in pure Python. Videos can be developed like software, taking advantage of version controlled, git-based workflows (i.e. no more Final.final.final.mp4 :),
It also makes it possible to use AI to automate all sorts of things. For example, I have created a pipeline for translating videos into other languages automatically with i18n (gettext) and machine translation (DeepL).
Follow my Twitter to get updates on Manim Voiceover.
For those who are not familiar, Manim is a Python library that lets you create animations programmatically, created by Grant Sanderson, a.k.a. 3blue1brown. His visual explainers are highly acclaimed and breathtakingly good (to see an example, click here for his introduction to neural networks).
Manim was originally built for animating math, but you can already see it being used in other domains such as physics, chemistry, computer science, and so on.
Creating any video is a very time-consuming process. Creating an explainer that needs to be mathematically exact is even more so, because the visuals often need to be precise to convey knowledge efficiently. That is why Manim was created: to automate the animation process. It turns out programming mathematical structures is easier than trying to animate them in a video editor.
However, this results in a workflow that is part spent in the text editor (writing Python code), and part in the video editor (editing the final video), with a lot of back and forth in between. The main reason is that the animation needs to be synced with voiceovers, which are recorded separately.
In this post, I will try to demonstrate how we can take this even further by making voiceovers a part of the code itself with Manim Voiceover, and why this is so powerful.
Creating a video with Manim is very tedious. The steps involved are usually as follows:
The workflow is often not linear. The average video requires you to rewrite, re-record, re-animate and re-sync multiple scenes:
The less experience you have making videos, the more takes you will need. Creating such an explainer has a very steep learning curve. It can take up to 1 month for a beginner to create their first few minutes of video.
I am a developer by trade, and when I first tried to create a video with the traditional workflow, I found it harder than it should be. We developers are spoiled, because we get to enjoy automating our work. Imagine that you had to manually compile your code using a hex editor every time you made a change. That is what it felt like to create a video using a video editor. The smallest change in the script meant that I had to re-animate, re-record and re-sync parts of the video, the main culprit being the voiceover.
To overcome this, I thought of a simple idea: Create an API that lets one to add voiceovers directly in Python. Manim Voiceover does exactly that and provides a comprehensive framework for automating voiceovers. Once the entire production can be done in Python, editing in the video editor becomes mostly unnecessary. The workflow becomes:
A little demo—see how a video would look like at the end of step (2):
And watch below to see how it would look like at the end of step (3), with my own voice:
I explain why this is so powerful below:
In the previous method, making modifications to the script has a cost, because you need to re-record the voiceover and readjust the scenes in the video editor. Here, making modifications is as easy as renaming a variable, since the AI voiceover is generated from code automatically. This saves a lot of time in the production process:
This lets videos created with Manim to be “fully code-driven” and take advantage of open source, collaborative, git-based workflows. No manual video editing needed, and no need to pay for overpriced video editing software:
(Or at least drastically reduced need for them)
From personal experience and talking to others who have used it, Manim Voiceover increases production speed by a factor of at least 2x, compared to manual recording and editing.
Note: The current major bottlenecks are developing the scene itself and waiting for the render. Regarding render speed: Manim CE’s Cairo renderer is much slower then ManimGL’s OpenGL renderer. Manim Voiceover currently only supports Manim CE, but it is on my roadmap to add support ManimGL.
This all sounds great, but how does it look like in practice? Let’s take a look at the API. Here is a “Hello World” example for Manim, drawing a circle:
from manim import *
class Example(Scene):
def construct(self):
circle = Circle()
self.play(Create(circle))
Here is the same scene, with a voiceover that uses Google Translate’s free text-to-speech service:
from manim import *
from manim_voiceover import VoiceoverScene
from manim_voiceover.services.gtts import GTTSService
class VoiceoverExample(VoiceoverScene):
def construct(self):
self.set_speech_service(GTTSService(lang="en"))
circle = Circle()
with self.voiceover(text="This circle is drawn as I speak."):
self.play(Create(circle))
Notice the with
statement. You can chain such blocks back to back, and Manim will vocalize them in sequence:
with self.voiceover(text="This circle is drawn as I speak."):
self.play(Create(circle))
with self.voiceover(text="Let's shift it to the left 2 units."):
self.play(circle.animate.shift(2 * LEFT))
The code for videos made with Manim Voiceover generally looks cleaner, since it is compartmentalized into blocks with voiceovers acting as annotations on top of each block.
See how this is rendered:
To record an actual voiceover, you simply change a single line of code:
# self.set_speech_service(GTTSService(lang="en")) # Comment this out
self.set_speech_service(RecorderService()) # Add this line
Currently, rendering with RecorderService
starts up a voice recorder implemented as a command line utility. The recorder prompts you to record each voiceover in the scene one by one and inserts audio at appropriate times. In the future, a web app could make this process even more seamless.
Check out the documentation for more examples and the API specification.
Having a machine readable source for voiceovers unlocks another superpower: automatic translation. Manim Voiceover can automatically translate your videos to any language, and even generate subtitles in that language. This will let educational content creators reach a much wider audience.
Here is an example of the demo translated to Turkish and rendered with my own voice:
To create this video, I followed these steps:
_()
per gettext convention. For example, I changed text="Hey Manim Community!"
to text=_("Hey Manim Community!")
.manim_translate blog-translation-demo.py -s en -t tr -d blog-translation-demo
, which created the locale
folder, called DeepL’s API to translate the strings, and saved them under locale/tr/LC_MESSAGES/blog-translation-demo.po
.
-s
stands for source language,-t
stands for target language,-d
stands for the gettext domain..po
file manually, because the translation was still a bit off.manim_render_translation blog-translation-demo.py -s BlogTranslationDemo -d blog-translation-demo -l tr -qh
, which rendered the final video.Check out the translation page in the docs for more details. You can also find the source code for this demo here.
Here is a Japanese translation, created the same way but with an AI voiceover:
Note that I have very little knowledge of Japanese so that the translation might be off, but I was still able to create it with services that are freely available online. This is to foreshadow how communities could create and translate educational videos in the future:
That is the main idea of my next project, GitMovie. If this excites you, leave your email address on the form on the website to get notified when it launches.
While using Manim Voiceover might seem tedious to some who are already using Manim with a video editor, I guarantee that it is overall more convenient than using a video editor when it comes to adding voiceovers to scenes. Feel free to create an issue if you have a use case that is currently not covered by Manim Voiceover.
What is even more interesting, Manim Voiceover can provide AI models such as GPT-4 with a convenient way to generate mathematically precise videos. Khan Academy has recently debuted a private release of Khanmigo, their GPT-4 based AI teacher. Imagine that Khanmigo could create a 3blue1brown-level explainer in a matter of minutes, for any question you ask! (I already tried to make GPT-4 output Manim code, but it is not quite there yet.)
To see why this is powerful, check out my video rendering of Euclid’s Elements using Manim Voiceover (part 1):
This video itself is pedagogically not very effective because books do not necessarily translate into good video scripts. But it serves as preparation for the point that I wanted to make with this post:
Having a machine-readable source and being able to program voiceovers allowed me to generate over 10 hours of video in less than a few days. In a few years, AI models will make such approaches 1000 times easier, faster and cheaper for everyone.
Imagine being able to auto-generate the “perfect explainer” for every article on Wikipedia, every paper on arXiv, every technical specification that would otherwise be too dense. In every language, available instantly around the globe. Universal knowledge, accessible by anyone who is willing to learn. Thanks to 3blue1brown, Manim and similar open source projects, all of this will be just a click away!
]]>All popular social media platforms feature some type of feed: Twitter, Instagram, Reddit, Facebook. Operators of these platforms benefit from increased engagement by their users, so they employ techniques designed to achieve that end. Unfortunately, they often do so at the expense of their users’ well-being. Below are 7 rules to help you retain control over your screen time, without having to leave social media for good, ordered from most important to least important.
On most online platforms, the order of content is determined by an algorithm designed to maximize user engagement, i.e. addict you and keep you looking at ads for as long as possible. Examples: Facebook news feed, Twitter “top tweets”, Instagram explore tab, Tiktok.
Your phone is always within your reach. Access feeds only on your laptop, in order not to condition yourself to constantly check it. Don’t install social media or video apps on your phone.
Your digital experience changes with each new person/source you follow. Be mindful about the utility of the information you would obtain before following a new source.
The amount of content you will have to go through increases roughly linearly with the number of sources you follow. You probably won’t see everything your 500 followees share—maybe it’s time to unfollow some of them.
Your brain has a limited capacity to process and hold information. Schedule a certain hour of the day to receive it, and don’t surpass it. Example: No more than 30 minutes of social media, restricted to 10–11 am.
If you don’t like what you’re seeing, block or unfollow immediately. This is the hardest when someone posts content that is sometimes useful, but otherwise annoying too. Generally, we put up with it for too long until we block someone.
Avoid toxic memes by muting related words, e.g. Trump, ISIS. This will filter out any post that contains that word. Click here to do it on Twitter now—it’s easy.
Follow these simple set of rules, and restore your control over social media and your digital experience in no time.
This post made it to the Hacker News front page.
]]>Blockchain state advances on a block by block basis. On a smart contract platform, the quantity of computation as a resource is measured in terms of the following factors:
The latter two are of secondary importance, because the bottleneck for the entire network is not the computing power or storage capacity of an individual node, but the overall speed of communicating the result of a computation to the entire network. In Bitcoin and Ethereum, that value is around 13 kbps^{1}, calculated by dividing average full block size by average block time. Trying to increase that number, either by increasing the maximum block size or decreasing block time, indeed results in increased computational capacity. However it also increases the uncle rate^{2}, thereby decreasing the quality of consensus—a blockchain’s main value proposition.
Moreover, users don’t just submit bits in their transactions. In Bitcoin, they submit inputs, outputs, amounts etc^{3}. In Ethereum, they can just submit a sender and a receiver of an amount of ETH, or they can also submit data, which can be an arbitrary message, function call to a contract or code to create a contract. This data, which alters Ethereum’s world state, is permanently stored on the blockchain.
Ethereum is Turing complete, and users don’t know when and in which order miners will include their transactions. In other words, users have no way of predicting with 100% accuracy the total amount of computational resources their function call will consume, if that call depends on the state of other accounts or contracts^{4}. Furthermore, even miners don’t know it up until the point they finish executing the function call. This makes it impractical for users to set a lump sum fee that they are willing to pay to have their transaction included, because a correlation between a transaction’s fee and its utilization of resources cannot be ensured.
To solve this problem, Ethereum introduced the concept of gas as a unit of account for the cost of resources utilized during transaction execution. Each instruction featured in the Ethereum Virtual Machine has a universally agreed cost in gas, proportional to the scarcity of the used resource^{5}. Then instead of specifying a total fee, users submits a gas price in ETH and the maximum total gas they are willing to pay.
The costliest operations on Ethereum are those of non-volatile storage and access^{6}, but these need not occupy space in a block. It’s the transactions themselves that are stored in the blocks and thus consume bandwidth. The gas corresponding to this consumption is called “intrinsic gas” (see the Yellow Paper), and it’s one of the reasons for the correlation between gas usage and block size:
The vertical clusterings at 4.7m, 6.7m and 8m gas correspond to current and previous block gas limits. Gas costs of instructions should indeed be set in such a way that the correlation between a resource and overall gas usage should increase with the degree of bottleneck.
The demand for transacting/computing on creates its own market, both similar and dissimilar to the markets of tangible products that we are used to. What is more important to us is the supply characteristics of this market. Supplied quantities aren’t derived from individual capacities and decisions of the miners, but from network bottlenecks. A limit is set on maximum gas allowed per block.
Supplied quantity is measured in terms of gas supplied per unit time, similar to bandwidth. Individual miners contribute hashrate to the network, but this doesn’t affect throughput. The difficulty adjustment mechanism ensures that network throughput remains the same, unless universally agreed parameters are changed by collective decision.
Moreover, the expenditure of mining a block far exceeds the expenditure of executing a block. In other words, changes in overall block fullness doesn’t affect miner operating expenses. Therefore, marginal cost is roughly zero, up until the point supply hits maximum throughput—where blocks become 100% full. At that point, marginal cost becomes infinite. This is characterized by a vertical supply curve located at maximum throughput, preceded by a horizontal supply curve.
This means that given a generic monotonically decreasing demand curve and a certain shift in demand, we can predict the change in the gas price, and vice versa. The price is located at the point where the demand curve intersects the supply curve. Major shifts in price starts to occur only when blocks become full. Past that point, users are basically bidding higher and higher prices to get their transactions included. See the figure below for an illustration.
This sort of econometric analysis can be done simply by looking at block statistics. Doing so reveals 2 types of trends in terms of period:
Note: This view of the market ignores block rewards, but that is OK in terms of analyzing gas price volatility, because block rewards remain constant for very long periods of time. However, a complete analysis would need to take block rewards into account, because they constitute the majority of miner revenue.
Demand for gas isn’t distributed equally around the globe. Ethereum users exist in every inhabited continent, with the highest demand seen in East Asia, primarily China. Europe+Africa and the Americas seem to be hand in hand in terms of demand. This results in predictable patterns that follow the peaks and troughs of human activity in each continent. The correlation between gas usage and price is immediately noticeable, demonstrated by a 5 day period from March 2019.
The grid marks the beginnings of the days in UTC, and the points in the graph correspond to hourly averages, calculated as:
Averaging hourly gives us a useful benchmark to compare, because block-to-block variation in these attributes is too much for an econometric analysis.
One can see above that the average gas price can change up to 2 to 4 times in a day. This shows us that Ethereum has found real use around the world, but also that there exists a huge UX problem in terms of gas prices.
Dividing the maximum gas price in a day by the minimum, we obtain a factor of intraday volatility:
Ethereum has witnessed gas price increases of up to 100x in a day. Smoothing out the data, we can see that the gas price can change up to 4x daily on average.
To understand the effect of geographic distribution on demand, we can process the data above to obtain a daily profile for gas usage and price. We achieve this by dividing up the yearly data set into daily slices, and standardizing each slice in itself. Then the slices are superimposed and their mean is calculated. The mean curve, though not numerically accurate, makes sense in terms of ordinal difference between the hours of an average day.
One can clearly see that gas usage and price are directly correlated. At 00:00 UTC, it’s been one hour since midnight in Central Europe, but that’s no reason for a dip in demand—China just woke up. The first dip is seen at 03:00 when the US is about to go to sleep, but then Europe wakes up. The demand dips again after 09:00, but only briefly—the US just woke up. We then encounter the biggest dip from 15:00 to 23:00 as China goes to sleep.
Surely there must be a way to absorb this volatility! Solving this problem would greatly improve Ethereum’s UX and facilitate even greater mainstream adoption.
The long term—i.e. $\gg$ 1 day—shifts in demand are unpredictable and non-periodic. They are caused by adoption or hype for certain applications or use cases on Ethereum, like
These shifts in price generally mirror ETH’s own price. In fact, it’s not very objective to plot a long term gas price graph in terms of usual Gwei, because most people submit transactions considering ETH’s price in fiat. For that reason, we denote gas price in terms of USD per one million gas, and plot it on a logarithmic scale:
The price of gas has seen an increase of many orders of magnitude since the launch of the mainnet. The highest peak corresponds to the beginning of 2018 when the ICO bubble burst, similar to the price of ETH. Although highly critical for users and traders, this sort of price action is not very useful from a modeling perspective.
The volatility in gas price stems from the lack of scalability. In 2019 on Ethereum, daily gas price difference stayed over 2x on average. The cycle’s effect is high enough to consider it as a recurring phenomenon that requires its own solution.
I think the narrative that gas price volatility is caused only by the occasional game/scam hype is incomplete—in a blockchain that has gained mainstream adoption such as Ethereum, the daily cycle of demand by itself is enough to cause volatility that harms the UX for everyone around the globe.
While increasing scalability is the ultimate solution, users may still benefit from mechanisms that allow them to hedge themselves against price increases, like reserving gas on a range of block heights. This would make a good topic for a future post.
As of October 2019. ↩
The rate at which orphaned blocks show up. ↩
But in practice, they can estimate it reliably most of the time. ↩
See Appendix G (Fee Schedule) and H (Virtual Machine Specification) of the Ethereum Yellow Paper. ↩
https://medium.com/coinmonks/storing-on-ethereum-analyzing-the-costs-922d41d6b316 ↩
Reward generation causes the supply of network currency to increase, resulting in inflation. Potential nodes are incentivized to join the network because they see there is profit to be made, especially if they are one of the early adopters. This brings the notion of a “cake” being shared among nodes, where the shares get smaller as the number of nodes increases.
Since one of the basic properties of a currency is finite supply, a sane protocol cannot have the rewards increase arbitrarily with more nodes. Thus the possible number of nodes is finite, and can be calculated using costs and rewards, given that transaction fees are negligible. The rate by which rewards are generated determines the sensitivity of network size to changes in costs and other factors.
Let $N$ be the number of nodes in a network, which perform the same work during a given period. Then we can define a generalized reward per node, introduced by Buterin^{1}:
\[r = R_0 N^{-\alpha} \tag{1}\]where $R_0$ is a constant and $\alpha$ is a parameter adjusting how the rewards scale with $N$.
Then the total reward issued is equal to
\[R = N r = R_0 N^{1-\alpha}.\]The value of $\alpha$ determines how the rewards scale with $N$:
Range | Per node reward $r$ | Total reward $R$ |
---|---|---|
$\alpha < 0$ | Increase with increasing $N$ | Increase with increasing $N$ |
$ 0 < \alpha < 1$ | Decrease with increasing $N$ | Increase with increasing $N$ |
$\alpha > 1$ | Decrease with increasing $N$ | Decrease with increasing $N$ |
Below is a table showing how different values of $\alpha$ corresponds to different rewarding schemes, given full participation.
$\alpha$ | $r$ | $R$ | Description |
---|---|---|---|
$0$ | $R_0$ | $R_0 N$ | Constant interest rate |
$1/2$ | $R_0/\sqrt{N}$ | $R_0 \sqrt{N}$ | Middle ground between 0 and 1 (Ethereum 2.0) |
$1$ | $R_0/N$ | $R_0$ | Constant total reward (Ethereum 1.0, Bitcoin in the short run) |
$\infty$ | $0$ | $0$ | No reward (Bitcoin in the long run) |
The case $\alpha \leq 0$ results in unlimited network growth, causes runaway inflation and is not feasible. The case $\alpha > 1$ is also not feasible due to drastic reduction in rewards. The sensible range is $0 < \alpha \leq 1$, and we will explore the reasons below.
We relax momentarily the assumption that nodes perform the same amount of work. The work mentioned here can be the hashing power contributed by a node in a PoW network, the amount staked in a PoS network, or the measure of dedication in any analogous system.
Let $w_i$ be the work performed by node $i$. Assuming that costs are incurred in a currency other than the network’s—e.g. USD—we have to take the price of the network currency $P$ into account. The expected value of $i$’s reward is calculated analogous to (1)
\[E(r_i) = \left[\frac{w_i}{\sum_{j} w_j}\right]^\alpha P R_0\]Introducing variable costs $c_v$ and fixed costs $c_f$, we can calculate $i$’s profit as
\[E(\pi_i) = \left[\frac{w_i}{\sum_{j} w_j}\right]^\alpha P R_0 - c_v w_i - c_f\]Assuming every node will perform work in a way to maximize profit, we can estimate $w_i$ given others’ effort:
\[\frac{\partial}{\partial w_i} E(\pi_i) = \frac{\alpha \,w_i^{\alpha-1}\sum_{j\neq i}w_j}{(\sum_{j}w_j)^{\alpha+1}} - c_v = 0\]In a network where nodes have identical costs and capacities to work, all $w_j$ $j=1,\dots,N$ converge to the same equilibrium value $w^\ast$. Equating $w_i=w_j$, we can solve for that value:
\[w^\ast = \frac{\alpha(N-1)}{N^{\alpha+1}} \frac{P R_0}{c_v}.\]Plugging $w^\ast$ back above, we can calculate $N$ for the case of economic equilibrium where profits are reduced to zero due to perfect competition:
\[E(\pi_i)\bigg|_{w^\ast} = \left[\frac{1}{N}\right]^\alpha P R_0 -\frac{\alpha(N-1)}{N^{\alpha+1}} P R_0 - c_f = 0\]which yields the following implicit equation
\[\boxed{ \frac{\alpha}{N^{\alpha+1}} + \frac{1-\alpha}{N^\alpha} = \frac{c_f}{P R_0} }\]It is a curious result that for the idealized model above, network size does not depend on variable costs. In reality, however, we have an uneven distribution of all costs and work capacities. Nevertheless, the idealized model can still yield rules of thumb that are useful in protocol design.
An explicit form for $N$ is not possible, but we can calculate it for different values of $\alpha$. For $\alpha=1$, we have
\[N = \sqrt{\frac{P R_0}{c_f}}.\]as demonstrated by Thum^{2}.
For $0<\alpha<1$, the explicit forms would take too much space. For brevity’s sake, we can approximate $N$ by
\[N \approx \left[ (1-\alpha)\frac{P R_0}{c_f}\right]^{1/\alpha}\]given $N \gg 1$. The closer $\alpha$ to zero, the better the approximation.
We also have
\[\lim_{\alpha\to 0^+} N = \infty.\]which shows that for $\alpha\leq 0$, the network grows without bounds and render the network currency worthless by inflating it indefinitely. Therefore there is no equilibrium.
For $\alpha > 1$, rewards and number of nodes decrease with increasing $\alpha$. Finally, we have
\[\lim_{\alpha\to\infty} N = 0\]given that transaction fees are negligible.
For $0 <\alpha \ll 1$, a $C$x change in underlying factors will result in $C^{1/\alpha}$x change in network size. For $\alpha=1$, the change will be $\sqrt{C}$x.
Let $\alpha=1$. Then a $2$x increase in price or rewards will result in a $\sqrt{2}$x increase in network size. Conversely, a $2$x increase in fixed costs will result in $\sqrt{2}$x decrease in network size. If we let $\alpha = 1/2$, a $2$x change to the factors result in $4$x change in network size, and so on.
Buterin V., Discouragement Attacks, 16.12.2018. ↩
Thum M., The Economic Cost of Bitcoin Mining, 2018. ↩
The paper by Bogdan et al. assumes a model where stake size doesn’t change once it is deposited, presumably to explain the concept in the simplest way possible. After the deposit, a stakeholder can wait to collect rewards and then withdraw both the deposit and the accumulated rewards. This would rarely be the case in real applications, as participants would want to increase or decrease their stakes between reward distributions. To make this possible, we need to make modifications to the original formulation and algorithm. Note that the algorithm given below is already implemented in PoWH3D.
In the paper, the a $\text{reward}_t$ is distributed to a participant $j$ with an associated $\text{stake}_j$ as
\[\text{reward}_{j,t} = \text{stake}_{j} \frac{\text{reward}_t}{T_t}\]where subscript $t$ denotes the values of quantities at distribution of reward $t$ and $T$ is the sum of all active stake deposits.
Since we relax the assumption of constant stake, we rewrite it for participant $j$’s stake at reward $t$:
\[\text{reward}_{j,t} = \text{stake}_{j, t} \frac{\text{reward}_t}{T_t}\]Then the total reward participant $j$ receives is calculated as
\[\text{total_reward}_j = \sum_{t} \text{reward}_{j,t} = \sum_{t} \text{stake}_{j, t} \frac{\text{reward}_t}{T_t}\]Note that we can’t take stake out of the sum as the authors did, because it’s not constant. Instead, we introduce the following identity:
Identity: For two sequences $(a_0, a_1, \dots,a_n)$ and $(b_0, b_1, \dots,b_n)$, we have
\[\boxed{ \sum_{i=0}^{n}a_i b_i = a_n \sum_{j=0}^{n} b_j - \sum_{i=1}^{n} \left( (a_i-a_{i-1}) \sum_{j=0}^{i-1} b_j \right) }\]Proof: Substitute $b_i = \sum_{j=0}^{i}b_j - \sum_{j=0}^{i-1}b_j$ on the LHS. Distribute the multiplication. Modify the index $i \leftarrow i-1$ on the first term. Separate the last element of the sum from the first term and combine the remaining sums since they have the same bounds. $\square$
We assume $n+1$ rewards represented by the indices $t=0,\dots,n$, and apply the identity to total reward to obtain
\[\text{total_reward}_j = \text{stake}_{j, n} \sum_{t=0}^{n} \frac{\text{reward}_t}{T_t} - \sum_{t=1}^{n} \left( (\text{stake}_{j,t}-\text{stake}_{j,t-1}) \sum_{t=0}^{t-1} \frac{\text{reward}_t}{T_t} \right)\]We make the following definition:
\[\text{reward_per_token}_t = \sum_{k=0}^{t} \frac{\text{reward}_k}{T_k}\]and define the change in stake between rewards $t-1$ and $t$:
\[\Delta \text{stake}_{j,t} = \text{stake}_{j,t}-\text{stake}_{j,t-1}.\]Then, we can write
\[\text{total_reward}_j = \text{stake}_{j, n}\times \text{reward_per_token}_n - \sum_{t=1}^{n} \left( \Delta \text{stake}_{j,t} \times \text{reward_per_token}_{t-1} \right)\]This result is similar to the one obtained by the authors in Equation 5. However, instead of keeping track of $\text{reward_per_token}$ at times of deposit for each participant, we keep track of
\[\text{reward_tally}_{j,n} := \sum_{t=1}^{n} \left( \Delta \text{stake}_{j,t} \times \text{reward_per_token}_{t-1} \right)\]In this case, positive $\Delta \text{stake}$ corresponds to a deposit and negative corresponds to a withdrawal. $\Delta \text{stake}_{j,t}$ is zero if the stake of participant $j$ remains constant between $t-1$ and $t$. We have
\[\text{total_reward}_j = \text{stake}_{j, n} \times\text{reward_per_token}_n - \text{reward_tally}_{j,n}\]The modified algorithm requires the same amount of memory, but has the advantage of participants being able to increase or decrease their stakes without withdrawing everything and depositing again.
Furthermore, a practical implementation should take into account that a
participant can withdraw rewards at any time.
Assuming $\text{reward_tally}_{j,n}$ is represented by a mapping reward_tally[]
which is
updated with each change in stake size
reward_tally[address] = reward_tally[address] + change * reward_per_token
we can update reward_tally[]
upon a complete withdrawal of $j$’s total accumulated
rewards:
reward_tally[address] = stake[address] * reward_per_token
which sets $j$’s rewards to zero.
A basic implementation of the modified algorithm in Python is given below. The following methods are exposed:
deposit_stake
to deposit or increase a participant stake.distribute
to fan out reward to all participants.withdraw_stake
to withdraw a participant’s stake partly or completely.withdraw_reward
to withdraw all of a participant’s accumulated rewards.Caveat: Smart contracts use integer arithmetic, so the algorithm needs to be modified to be used in production. The example does not provide a production ready code, but a minimal working example to understand the algorithm.
class PullBasedDistribution:
"Constant Time Reward Distribution with Changing Stake Sizes"
def __init__(self):
self.total_stake = 0
self.reward_per_token = 0
self.stake = {}
self.reward_tally = {}
def deposit_stake(self, address, amount):
"Increase the stake of `address` by `amount`"
if address not in self.stake:
self.stake[address] = 0
self.reward_tally[address] = 0
self.stake[address] = self.stake[address] + amount
self.reward_tally[address] = self.reward_tally[address] + self.reward_per_token * amount
self.total_stake = self.total_stake + amount
def distribute(self, reward):
"Distribute `reward` proportionally to active stakes"
if self.total_stake == 0:
raise Exception("Cannot distribute to staking pool with 0 stake")
self.reward_per_token = self.reward_per_token + reward / self.total_stake
def compute_reward(self, address):
"Compute reward of `address`"
return self.stake[address] * self.reward_per_token - self.reward_tally[address]
def withdraw_stake(self, address, amount):
"Decrease the stake of `address` by `amount`"
if address not in self.stake:
raise Exception("Stake not found for given address")
if amount > self.stake[address]:
raise Exception("Requested amount greater than staked amount")
self.stake[address] = self.stake[address] - amount
self.reward_tally[address] = self.reward_tally[address] - self.reward_per_token * amount
self.total_stake = self.total_stake - amount
return amount
def withdraw_reward(self, address):
"Withdraw rewards of `address`"
reward = self.compute_reward(address)
self.reward_tally[address] = self.stake[address] * self.reward_per_token
return reward
# A small example
addr1 = 0x1
addr2 = 0x2
contract = PullBasedDistribution()
contract.deposit_stake(addr1, 100)
contract.distribute(10)
contract.deposit_stake(addr2, 50)
contract.distribute(10)
print(contract.withdraw_reward(addr1))
print(contract.withdraw_reward(addr2))
With a minor modification, we improved the user experience of the Constant Time Reward Distribution Algorithm first outlined in Batog et al., without changing the memory requirements.
Batog B., Boca L., Johnson N., Scalable Reward Distribution on the Ethereum Blockchain, 2018. ↩
There have been some confusion of the terminology, like people calling Bitcoin deflationary. Bitcoin is in fact not deflationary—that implies a negative inflation rate. Bitcoin rather has negative inflation curvature: Bitcoin’s inflation rate decreases monotonically.
An analogy from elementary physics should clear things up: Speaking strictly in terms of monetary inflation,
Given a supply function $S$ as a function of time, block height, or any variable signifying progress,
In Bitcoin, we have the supply as a function of block height: $S:\mathbb{Z}_{\geq 0} \to \mathbb{R}_+$. But the function itself is defined by the arithmetic^{1} initial value problem
\[S'(h) = \alpha^{\lfloor h/\beta\rfloor} R_0 ,\quad S(0) = 0 \tag{1}\]where $R_0$ is the initial inflation rate, $\alpha$ is the rate by which the inflation rate will decrease, $\beta$ is the milestone number of blocks at which the decrease will take place, and $\lfloor \cdot \rfloor$ is the floor function. In Bitcoin, we have $R_0 = 50\text{ BTC}$, $\alpha=1/2$ and $\beta=210,000\text{ blocks}$. Here is what it looks like:
We can directly compute inflation curvature:
\[S''(h) = \begin{cases} \frac{\ln(\alpha)}{\beta} \alpha^{h/\beta} & \text{if}\quad h\ \mathrm{mod}\ \beta = 0 \quad\text{and}\quad h > 0\\ 0 & \text{otherwise}. \end{cases}\]$S’’$ is nonzero only when $h$ is a multiple of $\beta$. For $0 < \alpha < 1$, $S’’$ is either zero or negative, which is the case for Bitcoin.
Finally, we can come up with a closed-form $S$ by solving the initial value problem (1):
\[\begin{aligned} S(h) &= \sum_{i=0}^{\lfloor h/\beta\rfloor -1} \alpha^{i} \beta R_0 + \alpha^{\lfloor h/\beta\rfloor} (h\ \mathrm{mod}\ \beta) R_0 \\ &= R_0 \left(\beta\frac{1-\alpha^{\lfloor h/\beta\rfloor}}{1-\alpha} +\alpha^{\lfloor h/\beta\rfloor} (h\ \mathrm{mod}\ \beta) \right) \end{aligned}\]Here is what the supply function looks like for Bitcoin:
And the maximum number of Bitcoins to ever exist are calculated by taking the limit
\[\lim_{h\to\infty} S(h) = \sum_{i=0}^{\infty} \alpha^{i} \beta R_0 = \frac{\beta R_0}{1-\alpha} = 21,000,000\text{ BTC}.\]The concept of inflation curvature was introduced. The confusion regarding Bitcoin’s inflation mechanism was cleared with an analogy. The IVP defining Bitcoin’s supply was introduced and solved to get a closed-form expression. Inflation curvature for Bitcoin was derived. The maximum number of Bitcoins to ever exist was derived and computed.
Because $S$ is defined over positive integers. ↩
To control the amount of gas spent by the transaction, the attacker utilizes a special contract. There is a function in the contract which takes as input the amount of gas that the attacker wants to burn. The function runs meaningless instructions in a loop, and either returns or throws an error when the desired amount is burned.
For example let’s say that the average gas price has been 5 Gwei in the last 10 blocks. In order to exert influence over the next block, the attacker needs to submit transactions with gas prices higher than that, say 100 Gwei. The higher the gas price, the higher the chance of inclusion by miners. The attacker can choose to divide the task of using 8,000,000 gas—current gas limit for blocks—into as many transactions as they want. This could be 80 transactions with 100,000 gas expenditure, or 4 transactions with 2,000,000 gas expenditure.
Deciding on how to divide the task is a matter of maximizing the chance of inclusion, and depends on the factors outline below.
Miners want to maximize their profit by including transactions with highest fees. In the current PoW implementation of Ethereum, mining the block takes significantly more time than executing the transactions. So let’s assume all transactions in the pool are trivially executed as soon as they arrive and miners know the amount of gas each one uses.
For miners, maximizing profit is an optimum packing problem. Miners want to choose a subset of the transaction pool that gives them maximum profit per block. Since there are at least tens of thousands of transactions in the pool at any given time, the problem can’t be solved by brute-forcing every combination. Miners use algorithms that test a feasible number of combinations and select the one giving the highest reward.
A block stuffer’s main goal is to target the selection process by crafting a set of transactions that has the highest chance of being picked up by miners in a way that will deplete blocks’ gas limits. They can’t devise a 100% guaranteed strategy since each miner can use a different algorithm, but they can find a sweet spot by testing out the whole network.
(In a PoS system, our assumptions would be wrong since executing transactions is not trivial compared to validating blocks. Validators would need to develop more complex strategies depending on the PoS implementation.)
It could be so that the attacker wants to stall transactions with a specific contract. If the function calls to that contract use a distinctively high amount of gas, say between 300,000 and 500,000, then the attacker has to stuff the block in a way that targets that range.
For example, the attacker can periodically submit $n$ transactions $\{T_1, T_2,\dots, T_{n-1}, T_n\}$ with very high prices where
\[\sum\limits_{i=1}^{n} T_i^{\text{gas}} \approx 8,000,000.\]If the attacker is targeting transactions within a range of $(R_\text{lower}, R_\text{upper})$, they can choose the first $n-1$ transactions to deplete $8,000,000 - R_\text{upper}$ gas in short steps, and submit $T_n$ to deplete the remaining $R_\text{upper}$ gas with a relatively higher price. Note that the revenue from including a single transaction is
\[\text{tx_fee} = \text{gas_price} \times \text{gas_usage}.\]As gas usage decreases, the probability of being picked up by miners decreases, so prices should increase to compensate.
Fomo3D is a gambling game where players buy keys from a contract and their money goes into a pot. At the beginning of each round, a time counter is initiated which starts counting back from 24 hours. Each bought key adds 30 seconds to the counter. When the counter hits 0, the last player to have bought a key wins the majority of the pot and the rest is distributed to others. The way the pot is distributed depends on the team that the winner belongs to.
Key price increases with increasing key supply, which makes it harder and harder to buy a key and ensures the round will end after some point. In time, the stakes increase and the counter reduces to a minimum, like 2 minutes. At this point, the players pay both high gas and key prices to be “it” and win the game. Players program bots to buy keys for them, and winning becomes a matter of coding the right strategy. As you can understand from the subject, the first round was won through a block stuffing attack.
On August 22 2018, the address 0xa16…f85 won 10,469 ETH from the first round by following the strategy I outlined above. The winner managed to be the last buyer in block 6191896 and managed to stall transactions with Fomo3D until block 6191909 for 175 seconds, ending the round. Some details:
Fomo3D Long contract address: 0xA62…Da1
Last transaction by the winner before the attack starts: 0x7a0…349
Transaction ending the round and firing the onBuyAndDistribute
event which
distributes the pot:
0xa14…012
Transaction where winner withdraws the prize: 0xe08…508
Contract used for block stuffing: 0x18e…801
Other addresses possibly belonging to the winner: 0x3c3…f27 , 0xc6a…3e2 , 0x81e…0ac , 0xc96…590 , 0xd27…642 , 0x18d…a9a , 0x87c…4ef , 0x9da…0cf , 0x7dd…c4c , 0xb1d…aef , 0x1a6…d56 , 0xf6e…059 , 0x1dd…a69 , 0x061…63b , 0x00c…776 , 0xa94…eb8 , 0xf03…1f2
Other contracts possibly deployed by the winner for testing prior to the attack: 0xaf1…eae , 0x0c0…5ad , 0x88e…d04 , 0x4f4…6f8 , 0x487…4e5
The user addresses above were scraped from the Ethereum transaction graph as being linked to a primary account which supplied them with funds. The contract addresses were scraped from 0-valued transactions sent from user addresses. These have a distance of 1, there may be other addresses involved with greater distances.
Below are details of the last 4 blocks preceding the end of the round. The rows highlighted with yellow are transactions submitted by the attacker. The crossed out rows are failed transactions. All transactions by the attacker were submitted with a 501 Gwei gas price, and stuffing a single block costed around 4 ETH. The calls to buy keys generally spend around 300,000~500,000 gas, depending on which function was called. Below, you see the successfully stuffed block 6191906.
Idx | From | To | Hash | ETH sent | Gas Price [Gwei] |
Gas Limit | Gas Used | ETH spent on gas |
---|---|---|---|---|---|---|---|---|
0 | 0xF03…1f2 | 0x18e…801 | 0xb97…8e4 | 0 | 501.0 | 4,200,000 | 4,200,000 | 2.1042 |
1 | 0x87C…4eF | 0x18e…801 | 0x96f…1b0 | 0 | 501.0 | 3,600,000 | 3,600,000 | 1.8036 |
2 | 0xf6E…059 | 0x18e…801 | 0x897…2b3 | 0 | 501.0 | 200,000 | 200,000 | 0.1002 |
Sum | 0 | 1503.01 | 8,000,000 | 8,000,000 | 4.0080 |
Block 6191907 was a close call for the winner, because their transactions picked up for the block did not amount up to 8,000,000 and the other transaction was a call to Fomo3D by an opponent to buy keys. Note that it has a gas price of 5559 Gwei, which means either the bot or person who submitted the transaction was presumably aware of the attack. The transaction failed due to low gas limit, presumably due to a miscalculation by the bot or the person.
Idx | From | To | Hash | ETH sent | Gas Price [Gwei] |
Gas Limit | Gas Used | ETH spent on gas |
---|---|---|---|---|---|---|---|---|
0 | 0x32A…370 | 0xA62…Da1 | 0x5e7…be1 | 0.0056 | 5559.7 | 379,000 | 379,000 | 2.1071 |
1 | 0xC6A…3E2 | 0x18e…801 | 0xb8b…40c | 0 | 501.0 | 3,900,000 | 3,900,000 | 1.9539 |
2 | 0xD27…642 | 0x18e…801 | 0xbcf…c62 | 0 | 501.0 | 3,300,000 | 3,300,000 | 1.6533 |
3 | 0x00c…776 | 0x18e…801 | 0xf30…337 | 0 | 501.0 | 400,000 | 400,000 | 0.2004 |
Sum | 0.0056 | 7062.71 | 7,979,000 | 7,979,000 | 5.9147 |
Transactions in block 6191908 belonged to the attacker except for one irrelevant transfer. This block is also considered successfully stuffed, since the 7,970,000 gas usage by the attacker leaves no space for a call to buy keys.
Idx | From | To | Hash | ETH sent | Gas Price [Gwei] |
Gas Limit | Gas Used | ETH spent on gas |
---|---|---|---|---|---|---|---|---|
0 | 0xD27…642 | 0x18e…801 | 0x74a…9b1 | 0 | 501.0 | 3,300,000 | 3,300,000 | 1.6533 |
1 | 0x7Dd…c4c | 0x18e…801 | 0x48c…222 | 0 | 501.0 | 2,700,000 | 2,700,000 | 1.3527 |
2 | 0x3C3…f27 | 0x18e…801 | 0x01b…4aa | 0 | 501.0 | 1,800,000 | 1,800,000 | 0.9018 |
3 | 0xa94…eb8 | 0x18e…801 | 0x776…d43 | 0 | 501.0 | 170,000 | 170,000 | 0.0851 |
4 | 0xbFd…1b4 | 0x663…d31 | 0x3a6…ba1 | 0.05 | 100.0 | 21,000 | 21,000 | 0.0021 |
Sum | 0.05 | 2104.01 | 7,991,000 | 7,991,000 | 3.9950 |
By block 6191909, the counter has struck zero—more like current UTC time surpassed the round end variable stored in the contract—and any call to Fomo3D would be the one to end the round and distribute the pot. And the first transaction in the block is—wait for it—a call to Fomo3D to buy keys by the opponent whose transaction failed a few blocks earlier, submitted with 5562 Gwei. So the guy basically paid 1.7 ETH to declare the attacker the winner!
Idx | From | To | Hash | ETH sent | Gas Price [Gwei] |
Gas Limit | Gas Used | ETH spent on gas |
---|---|---|---|---|---|---|---|---|
0 | 0x32A…370 | 0xA62…Da1 | 0xa14…012 | 0.0056 | 5562.2 | 379,000 | 304,750 | 1.6950 |
1 | 0xC96…590 | 0x18e…801 | 0xf47…9ca | 0 | 501.0 | 2,200,000 | 37,633 | 0.0188 |
2 | 0xb1D…aEF | 0x18e…801 | 0xe4c…edb | 0 | 501.0 | 1,400,000 | 37,633 | 0.0188 |
3 | 0x18D…A9A | 0x18e…801 | 0xf3a…995 | 0 | 501.0 | 800,000 | 37,633 | 0.0188 |
… | … | … | … | … | … | … | … | … |
Another thing to note is that the attacker probably crafted the spender contract to stop the attack when the round has ended, presumably to cut costs. So the 37,633 gas used by the contract are probably to call the Fomo3D contract to check round status. All these point out to the fact that the attacker is an experienced programmer who knows their way around Ethereum.
Here, you can see the details of the 100 blocks preceding the end of the round, with the additional information of ABI calls and events fired in transactions.
Since the end of the first round, 2 more rounds ended with attacks similar to this one. I didn’t analyze all of them because it’s too much for this post, but here are some details if you want to do it yourselves.
Round | Address winning the pot | Winner’s last tx before the end of the round | Block containing that tx | Tx ending the round | Block containing that tx | Tx where winner withdraws prize | Amount won [ETH] | Contract used for block stuffing |
---|---|---|---|---|---|---|---|---|
1 | 0xa169df5ed3363cfc4c92ac96c6c5f2a42fccbf85 | 0x7a06d9f11e650fbb2061b320442e26b4a704e1277547e943d73e5b67eb49c349 | 6191896 | 0xa143a1ee36e1065c3388440ef7e7b38ed41925ca4799c8a4d429fa3ee1966012 | 6191909 | 0xe08a519c03cb0aed0e04b33104112d65fa1d3a48cd3aeab65f047b2abce9d508 | 10,469 | 0x18e1b664c6a2e88b93c1b71f61cbf76a726b7801 |
2 | 0x18a0451ea56fd4ff58f59837e9ec30f346ffdca5 | 0x0437885fa741f93acfdcda9f5a2e673bb16d26dd22dfc4890775efb8a94fb583 | 6391537 | 0x87bf726bc60540c6b91cc013b48024a5b8c1431e0847aadecf0e92c56f8f46fd | 6391548 | 0x4da4052d2baffdc9c0b82d628b87d2c76368914e33799032c6966ee8a3c216a0 | 3,264 | 0x705203fc06027379681AEf47c08fe679bc4A58e1 |
3 | 0xaf492045962428a15903625B1a9ECF438890eF92 | 0x88452b56e9aa58b70321ee8d5c9ac762a62509c98d9a29a4d64d6caae49ae757 | 6507761 | 0xe6a5a10ec91d12e3fec7e17b0dfbb983e00ffe93d61225735af2e1a8eabde003 | 6507774 | 0xd7e70fdf58aca40139246a324e871c84d988cfaff673c9e5f384315c91afa5e4 | 376 | 0xdcC655B2665A675B90ED2527354C18596276B0de |
A thing to note in the following rounds is that participation in the game and amount of pot gradually decreased, presumably owing to the fact that the way of beating the game has been systematized. Although anyone can attempt such an attack, knowing how it will be won takes the “fun” factor out of it.
Credit: Although I’ve found previous instances of the term “block stuffing” online, Nic Carter is the first one to use it in this context.
]]>Inside a transaction, the price paid/received per token is not constant and depends on the amount that is bonded or unbonded. This complicates the calculations.
Let’s say for an initial supply of $S_0$, we want to bond $T$ tokens which are added to the new supply $S_1=S_0+T$. The ETH $E$ that must be spent for this bonding is defined as
\[E = \int_{S_0}^{S_1} P\, dS\]which is illustrated in the figure above. If one wanted to unbond $T$ tokens, the upper limit for the integral would be $S_0$ and the lower $S_0-T$, with E corresponding to the amount of ETH received for the unbonding.
A linear relationship for the bonding curves are defined as
\[P(S) = P_0 + S I_p\]where $P_0$ is the initial price of the token and $I_p$ is the price increment per token.
Let us have $E$ ETH which we want to bond tokens with. Substituting $P$ into the integral above with the limits $S_0\to S_0+T$, we obtain $E$ in terms of the tokens $T$ that we want to bond:
\[E(S, T) = T P_0 + T I_p S + \frac{1}{2} T^2 I_p\]where $S$ is the supply before the bonding. Solving this for $T$, we obtain the tokens received in a bonding as a function of the supply and ETH spent:
\[\boxed{T(S, E) = \frac{\sqrt{S^2I_p^2 + 2E I_p + 2 S P_0 I_p + P_0^2}-P_0}{I_p} - S.}\]Let us have T tokens which we want to unbond for ETH. Unbonding $T$ tokens decreases the supply from $S_0$ to $S_0-T$, which we apply as limits for the above integral and obtain:
\[\boxed{E(S, T) = T P_0 + T I_p S - \frac{1}{2} T^2 I_p.}\]PoWH3D is one of the applications of bonding curves with a twist: 1/10th of every transaction is distributed among token holders as dividends. When you bond tokens with $E$ ETH, you receive $9/10 E$ worth of tokens and $1/10 E$ is distributed to everybody else in proportion to the amount they hold.
This means you are at a loss when you bond P3D (the token used by PoWH3D). If you were to unbond immediately, you would only receive 81% of your money. Given the situation, one wonders when exactly one can break even with their investment. The activity in PoWH3D isn’t deterministic; nonetheless we can deduce sufficient but not necessary conditions for breaking even in PoWH3D.
Let us spend $E_1$ ETH to bond tokens at supply $S_0$. The following calculations are done with the assumption that the tokens received
\[T_1 = T(S_0, 9E_1/10)\]are small enough to be neglected, that is $T_1 \ll S_0$ and $S_1 \approx S_0$. In other words, this only holds for non-whale bondings.
Then let others spend $E_2$ ETH to bond tokens and raise the supply to $S_2$. The objective is to find an $E_2$ large enough to earn us dividends and make us break even when we unbond our tokens at $S_2$. We have
\[S_2 = S_0 + T(S_0, E_2).\]Our new share of the P3D pool is $T_1/S_2$ and the dividends we earn from the bonding is equal to
\[\frac{1}{10}\frac{T_1}{S_2}E_2.\]Then the condition for breaking even is
\[\boxed{\frac{9}{10} E(S_2, T_1) + \frac{1}{10}\frac{T_1}{S_2}E_2 \geq E_1.}\]This inequality has a lengthy analytic solution which is impractical to typeset. The definition should be enough:
\[E^{\text{suff}}_2(S_0, E_1) := \text{solve for $E_2$}\left\{\frac{9}{10} E(S_2, T_1) + \frac{1}{10}\frac{T_1}{S_2}E_2 = E_1\right\}\]and
\[E_2 \geq E^{\text{suff}}_2.\]$E^{\text{suff}}_2$ can be obtained from the source of this page in JavaScript from
the function sufficient_bonding
. The function involves many power
and square operations and may yield inexact results for too high values of
$S_0$ or too small values off $E_1$, due to insufficient precision of the
underlying math functions. For this reason, the calculator is disabled
for sensitive input.
The relationship between the initial supply and sufficient bonding is roughly quadratic, as seen from the graph above. This means that the difficulty of breaking even increases quadratically as more people bond into P3D. As interest in PoWH3D saturates, dividends received from the supply increase decreases quadratically.
The relationship is not exactly quadratic, as seen from the graph above. The function is sensitive to $E_1$ for small values of $S_0$.
Let us spend $E_1$ ETH to bond tokens at supply $S_0$ and receive $T_1$ tokens.
Then let others unbond $T_2$ P3D to lower the supply to $S_2$. The objective is to find a $T_2$ large enough to earn us dividends and make us break even when we unbond our tokens at $S_2$. We have
\[S_2 = S_0 - T_2.\]Our new share of the P3D pool is $T_1/S_2$ and the dividends we earn from the bonding is equal to
\[\frac{1}{10}\frac{T_1}{S_2} E(S_2, T_2)\]Then the condition for breaking even is
\[\boxed{\frac{9}{10} E(S_2, T_1) + \frac{1}{10}\frac{T_1}{S_2} E(S_2, T_2) \geq E_1.}\]Similar to the previous section, we have
\[T^{\text{suff}}_2(S_0, E_1) := \text{solve for $T_2$}\left\{\frac{9}{10} E(S_2, T_1) + \frac{1}{10}\frac{T_1}{S_2} E(S_2, T_2) = E_1\right\}\]and
\[T_2 \geq T^{\text{suff}}_2.\]$T^{\text{suff}}_2$ can be obtained from the function sufficient_unbonding
.
The relationship between $S_0$ and $T^{\text{suff}}_2$ is linear and insensitive to $E_1$. Regardless of the ETH you invest, the amount of tokens that need to be unbonded to guarantee your break-even is roughly the same, depending on your entry point.
Below is a calculator you can input $S_0$ and $E_1$ to calculate $E^{\text{suff}}_2$ and $T^{\text{suff}}_2$.
$S_0$ | |
---|---|
$E_1$ | |
$E^{\text{suff}}_2 $ | |
$T^{\text{suff}}_2 $ |
For the default values above, we read this as:
For 100 ETH worth of P3D bonded at 3,500,000 supply, either a bonding of ~31715 ETH or an unbonding of ~3336785 P3D made by other people is sufficient to break even.
In order to follow these statistics, you can follow this site.
Bonding curve calculations can get complicated because the price paid per token depends on the amount of intended bonding/unbonding. With this work, I aimed to clarify the logic behind PoWH3D. Use the formulation and calculator at your own risk.
The above conditions are only sufficient and not necessary to break even. As PoWH3D becomes more popular, it gets quadratically more difficult to break even from a supply increase. PoWH3D itself doesn’t generate any value or promise long-term returns for its holders. However every bond, unbond and transfer deliver dividends. According to its creators, P3D is intended to become the base token for a number of games that will be built upon PoWH3D, like FOMO3D.
]]>