How to lose $13 of users’ funds (as a blockchain developer)

The government says we are not in recession, but at the same time we hear about sky-high inflation, interest rate increases and layoffs in almost every sector of the economy.

Despite crypto and TradFi being the most affected, many companies are still building their tokens, protocols and DeFi products. Are you one of them?

Today I’m going to talk about data types, so stay tuned. I have something very important to say. You can imagine me as a 60+ year old professor from MIT who tortures students with lectures on topics that no longer matter. But that is not true.

Data types are still important, and neglecting them leads to serious consequences. I will try to briefly go through all the potential problems and address them so that you don’t find the 8 minutes you spend reading this article a waste.

Modern languages ​​such as JavaScript and Python use “duck typing” to determine the type of a variable. If we assign this type of formula a = 2 + 2 to a variable, the language interpreter knows that it has to do with numbers, and it will perform mathematical operations on this literal.

The duck writing can be explained by this sentence: “If it walks like a duck and it quacks like a duck, then it must be a duck”. When you look into the meaning of it – it makes perfect sense. If the literal has letters and numbers, it must be a string, and that’s clear. But what if it has numbers?

Is that a boolean, integer, decimal, floator date. In most cases, your interpreter will handle types correctly. The problem starts when your program needs to run (even simple) equations on large numbers.

“If it walks like a duck and it quacks like a duck, it must be a duck”, right? It actually isn’t.

Ethereum denominations in a nutshell

In the following paragraphs, I refer to Ethereum’s common denominations – wei and gwei. Let me briefly introduce you to them so that we speak the common language.

The smallest denomination is 1 wayand 1 ether equals 1,000,000,000,000,000,000 Wei (18 zeros). I repeat – 18 zeros. It’s hard to wrap our minds around such big numbers, but they make a difference and mean a lot.

The next common denomination is 1 gwei. 1 ether equals 1,000,000,000 gwei (9 zeros). Gwei is more tolerable for people – in the end, everyone wants to be a millionaire, right? (wink wink)

Let’s sum it up – 1 ether equals:

  • 1,000,000,000 gwei (9 zeros)
  • 1,000,000,000,000,000,000 wei (18 zeros)

Technical note: Ethereum has two layers – the execution layer and the consensus layer. The execution layer uses wei to represent ether values, and the consensus layer uses gwei. If you are a blockchain developer, you need to learn how to interact with both.

Real-world examples: Stakefish’ tips and MEV Pool

I’m a software engineer at stakefish. I am responsible for building our DeFi product palette and one of the latest is our Ethereum tip and MEV pool.

As of September 15, 2022, all validators are eligible for transaction tips and can participate in MEVs to earn additional rewards. Transaction hints and MEVs are earned when the validator proposes a new block.

We decided to build a smart contract that collects all rewards in a common vault and allows the user to claim their share from it. I’m not trying to advertise our product, but I need to set the context of this article.

If you are more interested in this product, you can read more here. I am not selling you anything but my experience.

As I mentioned, we have a smart contract that receives transaction tips and MEV rewards earned by validators. This means that our smart contract has a fairly large balance. There are 963+ Ether now ($1.1 million) and we have 8671 validators contributing to it.

The critical part responsible for synchronization between Ethereum execution and the consensus layer is Oracle. It is a very important system that allows us to find out which validators contribute to the pool.

The oracle is written in Python, but it could be written in JavaScript – the problem remains unchanged, and I’ll prove it soon.

Let’s dive deep into the code!

Why data types are important

The balance of the smart contract corresponds to 963,135,554,442,603,402,422 wei (963 ether) now. This number is not only difficult for humans to understand, but also for computers (language interpreters to be exact). Let’s check JavaScript:

const vault_balance = parseInt("963135554442603402422")
console.log(vault_balance) 
// 963135554442603400000 (lost 2422 wei in total)

I just threw the balance off string to intand I already am 2422 wei card. We haven’t run any equations yet.

The smart contract balance is so high thanks to many validators contributing to it. Now let’s calculate what the average validator share of the contract’s balance now:

const vault_balance = parseInt("963135554442603402422")
const validator_count = 8671

const avg_validator_contribution = vault_balance / validator_count
// 111075487768723730 (lost 7 wei per validator)

The average proportion is 0.111 ether. But this amount is not correct – we are actually missing 7 roads. It is 60,697 wei total (7 wei times 8671 validators). I will show the correct number later.

If you continue down the rabbit hole with losses – let’s calculate the total reward amount per given validator. Remember that the user had to deposit 32 ether to start a validator, so I will deduct that from the validator balance.

And I will take as an example a random validator that contributes to the smart contract that has a balance of 32,779 ether.

const vault_balance = parseInt("963135554442603402422") // (lost 2422 wei)
const validator_count = 8671
const avg_validator_contribution = vault_balance / validator_count // (lost 7 wei)

const initial_deposit = parseInt("32000000000000000000")
const validator_balance = parseInt("32779333896000000000")

const total_validator_rewards = validator_balance - initial_deposit + avg_validator_contribution
// 890409383768723700 (lost 23 wei per validator)

The total reward earned by this validator is equivalent to 0.8904 Ether, but this value is also not exact. At this moment we miscalculated 199,443 wei total (23 wei times 8671 validator). As you can see, this way of calculating numbers is not sustainable.

What’s going wrong?

There are two problems with the code above:

  • In JavaScript, the maximum safe value for integers is equal to 2^53 - 1 just. This means that it can handle up to 9007199254740991 wei (0.009 ether)
  • Technically, we could have used BigInt but we would have problems with sharing. We would end up with “floating” values. The flows are the root of all evil in finance because they are approximate. That means they lose precision. We must use decimals. (The main difference between decimal and float is that decimal stores exact value and float approximately.)

If you had ever done any Ethereum related coding in JavaScript, you must have heard about it ethers.js. This library contains all the necessary tools to interact with the blockchain. To fix the above problem, we use one of the tools called BigNumber which supports extremely large numbers and handles decimals correctly.

Let’s do it!

const vault_balance = BigNumber.from("963135554442603402422") // no loss
const validator_count = BigNumber.from(8671)
const avg_validator_contribution = vault_balance.div(validator_count) // no loss
// 111075487768723723

const initial_deposit = BigNumber.from("32000000000000000000")
const validator_balance = BigNumber.from("32779333896000000000")

const total_validator_rewards = validator_balance.sub(initial_deposit).add(avg_validator_contribution)
// 890409383768723723

As you can see, now we ended up with the exact number. How do I know this is actually the right number? I will repeat the same exercise in Python to prove that I am right.

Let’s try it in Python

Python supports long integers, so the values ​​are not suddenly truncated as we have seen in JavaScript. Unfortunately, it still determines all floating-point numbers as floats by default:

vault_balance = int("963135554442603402422") # no loss
validator_count = 8671
avg_validator_contribution = vault_balance / validator_count
# 111075487768723728 (5 wei too much)

initial_deposit = int("32000000000000000000")
validator_balance = int("32779333896000000000")

total_validator_rewards = validator_balance - initial_deposit + avg_validator_contribution
# 890409383768723712 (lost 11 wei)

Wondering how exactly it lost precision? The division threw avg_validator_contribution to float instead of decimal. The correct extract will look like this:

vault_balance = Decimal("963135554442603402422")
validator_count = Decimal(8671)
avg_validator_contribution = vault_balance / validator_count
# 111075487768723723

initial_deposit = Decimal("32000000000000000000")
validator_balance = Decimal("32779333896000000000")

total_validator_rewards = validator_balance - initial_deposit + avg_validator_contribution
# 890409383768723723

Now the values ​​returned by Python and JavaScript are accurate. Check for yourself!

These types of losses are marginal and easily missed. We often find out about them when they compound over time and grow to significant numbers.

Situations like that always cause headaches, not only for developers, but also for other departments such as finance or legal. You should always test your formulas, and never use nice round numbers to do it!


It would mean the world to me if you Follow me on Twitter. I focus my activity on software development and blockchain. I open source most of my work, so you might want to check GitHub.

LOAD
. . . comments & more!

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *