6

With solidity, we cannot directly calculate 0.1 or 0.001. We usually use a decimal multiplier and divisor integer to calculate numbers like this:

0.01 = 1 / 100

When the number is more than 1, it is relatively easy because we just repeat the multiplication:

function(uint base, uint exponent) external {
    for (uint256 i = 1; i < exponent; i++) 
        uint256 z = base;
        z = z * base; 
    }

Or just:

z ** exponent

However, when we want to use fractional numbers as exponents, those functions won't work. Can anybody think of a nice way to do this?

Paul Razvan Berg
  • 17,902
  • 6
  • 73
  • 143
kohshiba
  • 467
  • 4
  • 11
  • Maintain the value as a pair of integers (numerator and denominator) right up until the point where you have to return it. Alternatively, return it as a pair of integers and let the receiver handle that. – goodvibration Feb 16 '20 at 17:03
  • Thanks, but seems like this easily causes integer overflow because it exceeds 2^256 when calculating numbers like 10^100 – kohshiba Feb 17 '20 at 01:35
  • @kohshiba There are a couple of libraries that implement some floting point operations: https://github.com/cag/solidity-float-point-calculation, https://github.com/abdk-consulting/abdk-libraries-solidity. Unless you have a good reason I'd try to re-write the equation to use integers. – Ismael Feb 17 '20 at 17:10

2 Answers2

5

Binary Fractions

I currently know of only one solution, authored by Mikhail Vladimirov. It works by computing the input as a binary fraction.

Code

ABDKMath64x64.sol has an exp_2 function, which looks like this:

function exp_2(int128 x) internal pure returns (int128) {
    unchecked {
        require(x < 0x400000000000000000); // Overflow
    if (x &lt; -0x400000000000000000) return 0; // Underflow

    uint256 result = 0x80000000000000000000000000000000;

    // REMOVED: Binary fraction logic, explained below...

    result &gt;&gt;= uint256(int256(63 - (x &gt;&gt; 64)));
    require(result &lt;= uint256(int256(MAX_64x64)));

    return int128(int256(result));
}

}

For brevity, I skipped a big chunk of the function body.

Explanation

Any fractional number can be written as the sum of the integer part and the fractional part:

x = n + f, 0 <= f < 1

Then, using x as an exponent:

y = 2^x = 2^(n + f) = 2^n * 2^f;

Observing that we can write f as a binary fraction:

f = f1 * 2^-1 + f2 * 2^-2 + ...

Where f_i can be either 0 or 1. Then:

y = 2^n * (2^(f1*2^-1) * 2^(f2*2^-2) ...)

Note that:

for f_i = 0 => 2^(f_i * 2^-i) = 1
for f_i = 1 => 2^(f_i * 2^-i) = root(2, 2^i)

So we pre-compute these root(2, 2^-i) magic factors and use them as hardcoded constants. The i stands for the position of the bit in the fractional binary representation. E.g. if we were to match the very first bit, we would multiply by the square root of 2, i.e. ~1.4142.

Arbitrary Base

Given the basic logarithm rule:

x = b^(log_b(x))

We can deduce:

x^y = b^(y*log_b(x))

Applying to base 2:

x^y = 2^(y*log_2(x))

We already have an implementation for exp_2 and log_2 is also offered by ABDKMath64x64.

Trade Offs

What I don't like about Mikhail's solution:

  1. Requires users to work with binary numbers
  2. Precision is limited to int128, doesn't go to int256 or near that
Paul Razvan Berg
  • 17,902
  • 6
  • 73
  • 143
4

Paul's answer is very good. If you want to approximate an exponential function, I used a series of linear functions with a target half-life:

function getPrice(uint256 value, uint256 t, uint256 halfLife) public pure returns (uint256 price) {
    price = value >> (t / halfLife);
    price -= price * (t % halfLife) / halfLife / 2;
}

Half-life in my code was the number of seconds in two days.