Due to popular demand (including @albertocuestacanada, @asselstine and @BrendanChou), we've been exploring the space of fixed-point math libraries to come up with a robust yet flexible design that is appropriate for most use cases. The current plan is to include such a library in the v3.1 release of OpenZeppelin Contracts, a couple weeks from now.
Regarding scope, our initial thoughts are to limit support to add
, sub
, mul
and div
. Implementation of functions such as pow
, sqrt
or log
is often heavily dependent on the chosen underlying implementation, and we'd rather get this right first instead of being too ambitious in the feature set. These are great candidates for v3.2 though!
Design Decisions
Our goal is for this library to be as useful to as many users as possible, and here's where we'd like your help in making certain critical design decisions. All feedback is welcome!
For the time being, this is what I'm considering:
- 256 bit word size
- Fixed number of 18 decimals
- Support for signed numbers
Below are in-depth discussions on different aspects of the library, explaining my understanding on each of them and why I think the values above are what makes the most sense. Unsurprisingly, @albertocuestacanada's DecimalMath is very similar - he's quite good at this
1. Fixed or Programmable Decimals
Fixed-point arithmetic relies on all numbers being multiplied by a large number that represents the value '1' (often called unit
). This unit
value is what determines how many decimals can be represented, and is critical when converting to and from fixed point, and when performing any operation other than addition and subtraction.
There are two general approaches here: have a single, constant value for unit
which applies to all fixed-point numbers in the library, or make its functions parametrized over unit
.
A single value makes the library simpler, easier to use, and cheaper in terms of gas, but the choice of unit
might not be a one-size-fits-all. Making it parametric would imply higher gas costs not only in execution but also storage (due to the need to store unit
), and we'd also have to restrict unit
to a range of sensible values. Testing and correctness become harder in this scenario.
Overall, given that the word size of the EVM is so large (256 bits) I think we'll be able to pick a constant unit
value good enough for all reasonable use cases. I'd like to hear arguments for the parametric approach however, especially if they come from projects that are already working this way.
2. Decimal or Binary Decimals
The unit
value determines the smallest number that can be represented with a fixed-point number. Given this, it'd be natural to use a power of ten, leaving us with clear picture of how many decimal places our numbers have. This is what Ethereum numbers (both ETH itself and most tokens) do: their values are displayed as if they had a unit
value of 10^18 (though they are not fixed point!).
Alternatively, we can have unit
be a power of two. This has two big benefits:
- We can use shifts instead of multiplications and divisions, which are cheaper in gas (see note at the end!)
- The numbers are easier to reason about regarding ranges and overflow protection, making the library simpler/safer
The downside is our numbers will no longer behave like decimal point numbers. For example, if unit = 2^2 = 4
, then the smallest number would be 0.25, not 0.1 or 0.01. I'd argue however that this value will be so small that this won't be an issue.
3. Decimals and Word Size
If we go down the fixed unit
value route, then we need to choose what this value is, and how much space to allocate for our number and its decimal places.
Regarding total size, I think the only two reasonable options are 256 or 128 bits. The reason why someone would pick 128 are twofold: it removes the need for an intermediate overflow check on mul
, and it allows for cheaper storage by packing two 128 bit numbers in a single storage slot. This is the approach followed by ABDK.
The obvious benefit behind 256 bits is a much larger range of representable values. We'd need to perform an additional intermediate overflow check, but the upside is so big this is to me clearly the better choice. I'd love to hear more reasoning behind 128 bits though! @3sGgpQ8H
For a decimal unit
, 18 decimal places is a reasonable choice: even if tokens and ETH only use these decimals for display purposes, math is often already done on them as if they were fixed point. For powers of two, 10^18 is about ~2^60, and indeed 2^64 seems to be a popular choice here (again, this is ABDK's chosen value).
4. Sign
Finally, we'd need to decide between signed and unsigned numbers. Given that the whole point behind this is to allow for more sophisticated math, signed seems like a no-brainer.
What We Need From You
In addition to sharing thoughts about the low-level details of the library based on the information above, it'd be super valuable to us if you could share information regarding your usage of fixed-point math, including:
- why you need it (exchange rates? computing interest?)
- what you're currently using to solve this (Fixidity? ABDK? did you roll your own?)
- what you expect to get from the OpenZeppelin Contracts fixed-point library
Thanks a lot to everyone for being a part of this!