Designing Fixed Point Math in OpenZeppelin Contracts

If we have int128 as the underlying datatype and use 18 decimal places, we're left out with a maximum value of around 10^20. While not small, this wouldn't be enough to e.g. store wei in such a format. Using int256 would greatly extend this range (to around 10^60 or 10^40, depending on how you count).

The thing I'm not sure about is whether we would have simple overflow protection, or try to store the intermediate 512 bit result. A big issue with naive overflow detection is that some numbers would not be able to be multiplied not because the result is too large, but because of how it is computed, which may cause nasty issues on live systems years after the initial deployment.

1 Like

You avoid that by treating wei amounts as being already fixed point numbers with 18 decimals.
For example, if you use 18 decimals, conversions would be like this:

uint256 x = 1
uint256 fixedX = 1 000000 000000 000000
uint256 e = ether(1)
uint256 fixedE = 1 000000 000000 000000
uint256 w = wei(1)
uint256 fixedW = 1

That's why I used 18 decimals in DecimalMath.sol. Wei amounts are already minuscule, we don't need to operate on fractions of wei.

With int128 we can represent numbers up to 10^38. Using the model above that means that we can represent up to 10^20 ether. That should do.

Using int128 helps precisely with the intermediate values, because you require operands to be int128, but you store intermediate values in int256. That means that an int128 multiplied by an int128 will never overflow.

And in terms of range, there is not that much difference, really. Using int128 you can multiply two numbers up to 10^20 (before converting them to fixed). If you use int256 as Fixidity did, your range is actually the same because of the issue with intermediate values.

Yes, you can represent bigger numbers, but you won't know if you can operate with them until you try. What is the point of being able to represent 10^50 if you don't know when it will blow up in an operation?

I solved that problem in Fixidity by coding a bunch of constants for safe operation and doing lots of comparisons. ABDK solved it much more elegantly by using int128.

2 Likes

I would say fixed point is lossless because the multiplication and division operators can result in lost precision. You don't want this to go unnoticed when dealing with amounts of money. (This is also true for integer division, though... :thinking:)

This is a great point and I think these cross-type operations are the ones that should require the developer to explicitly say whether they want rounding up or down.

2 Likes

I didn’t explain myself too well when I said that a decimal representation is lossless, and a binary is not. Let me try again.

Unlike @3sGgpQ8H, I think that wei amounts are themselves in a fixed point format. One ether is 1, the unit is 10^decimals, by using wei amounts in all functions what we are doing is feeding the fixed point format as a parameter.

If you use a fixed point library with decimal representation, you don’t need to convert wei amounts to fixed point, because they already are in fixed point. If you have an interest rate of 5%, and convert it to fixed point you obtain 5 0000 000000 000000. If you want to apply that interest rate to a wei amount, let’s say 1 0000 000000 000000, you don’t need to convert the wei amount. You can multiply both numbers using fixed point multiplication and you get 5 00 000000 000000.

In that sense, using a decimal base is lossless. The conversion between wei amounts and fixed point is just adding and then removing zeroes, so you don’t lose information when converting data. You can even use wei amounts as fixed point numbers and not convert them at all.

Using a binary format, if you convert a money amount to fixed point and back, you could be losing data without doing any other operation.

Rounding money amounts is accepted when that is the result of an operation. When a bank calculates interest on a loan, they will round to the number of significant digits chosen. That is fine, what is not fine is to lose one cent when just taking your deposit.

There are two options here, as I see it:

  1. Create a fixed type for internal use of the library. Stress that money amounts shouldn’t be converted to fixed. Provide cross-type operations.
  2. Offer uint256 and int256 fixed point operations in the library, meaning that they are simple multiplications and divisions but with an additional division or multiplication by the unit. Wei amounts can be accepted as parameters, no conversion or cross-type operations are offered.
2 Likes

Fixed point operations may be implemented in a library, while fixed point data type and literals cannot. Without separate data type and easy to read literals, the code would be error-prone, cumbersome, and messy even with a good library.
In my understanding, data type and literals could be added to the language quite easily. We just need to agree on format.

Hello again everyone!

Progress on this end slowed down due to the release of OpenZeppelin Contracts v3.0, which ended up taking longer than expected due to large amounts of user feedback during the release candidate period.

As some of you may know, the Solidity Summit started today, and native fixed point support was one of the points in the agenda.

During this session, @criseth outlined his plans for expanding the initial support already in Solidity so that these types could be more widely used. I shared the findings of our discussion here, and we arrived at similar conclusions. Among others, it seems like decimal precision will be the choice taken by the compiler.

@axic shared the following tentative levels of support, with the idea being that Solidity covers at least Core and perhaps Basic, allowing for libraries to then step in and fill remaining needs.

Core:
literals
assignment
abi (e.g. function arguments)
casting to integers

Basic operations:
comparison
conversion
addition / substraction / multiplication / division

Extended operations:
exponentiation
others?

Given this, I think it makes sense to pause this proposal until we learn more about how and when the Solidity team plans to work on this feature, to prevent a duplication of efforts. This issue is where discussion started (4 years ago!) and continues: likely new issues will spawn from that.

4 Likes

Any plans on supporting any other advanced math libraries (Like roots, logs, pi, e)?

Also I was curious on if anyone has used https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMathQuad.md before?

1 Like

Yes, this is something we'll look into once the Solidity team settles on a plan for their implementation. We'll likely want to focus on functions we know the space needs, like exponentiation by squaring.

1 Like

It looks like the Solidity team started working on adding native fixed-point types, according to this update made by @axic on Feb 10, 2021:

Add a bare minimum support to the language and compile:

  • a single fixed128 type,
  • ABI coder,
  • conversion to same-width bytes,
  • perhaps, but not necessarily, conversion to same-width integer type

And then @chriseth replied:

Sounds like a good first milestone, so I would say let's do this!

1 Like

I'd like to second @albertocuestacanada's approach to consider wei a fixed-point type. That's how I've always thought about money amounts in my Solidity musings, even before reading this thread.

In case anybody wonders why the result is 5 00 000000 000000, that's because Alberto implied that after multiplying the two numbers, there would be a division by the "unit" number, which is 1e18 (refer to @nventuro's starting post).

An example for a math library that makes this assumption is Compound's Exponential.sol. Multiplication is as simple as this (note: I simplified Compound's implementation since overflows are now checked by the compiler by default):

pragma solidity ^0.8.0;

function mulExp(Exp memory a, Exp memory b) internal pure returns (Exp memory) {
    uint256 doubleScaledProduct = a.mantissa * b.mantissa;

    // We add half the scale before dividing so that we get rounding instead of truncation.
    // See "Listing 6" and text above it at https://accu.org/index.php/journals/1717
    // Without this change, a result like 6.6...e-19 will be truncated to 0 instead of being rounded to 1e-18.
    uint256 doubleScaledProductWithHalfScale = halfExpScale + doubleScaledProduct;

    uint256 product = doubleScaledProductWithHalfScale / expScale;
    return Exp({ mantissa: product });
}

The code above achieves precisely what Alberto described in his post. If you were to pass the following values as the a and b arguments:

  • Exp({ mantissa: 50000000000000000 })
  • Exp({ mantissa: 10000000000000000 })

The following value would be returned:

  • Exp({ mantissa: 500000000000000 })

In other words, 5% of 0.01 ether is 0.0005 ether.

1 Like

I have uploaded to GitHub as well as to NPM a package which supports various math functions, such as floorSqrt, ceilSqrt, floorLog2, pow, log, exp, a function for solving xA^x = B, a set of bonding-curve functions and much more.

I hope that you will find it useful; please feel free to ask any question…

2 Likes