Math in Solidity (Part 3: Percentages and Proportions)

   This article is the third in a series about doing math in Solidity. This time the theme is: Percentages and Proportions .

 

introduce 

     Financial mathematics starts with percentages. What is the percentage of x in y ? What percentage of x is y ? We all know the answer: x percent of y is x  ×  y  ÷ 100, and y is x percent of y  × 100 ÷  x . This is school math.

The above formula is a special case of solving for a scale. In general, a ratio is an equation of the form: a  ÷  b  =  c  ÷  d , and solving the ratio is finding one of the three values ​​known. For example, d can be found from a , b , and c as follows: d  =  b  ×  c  ÷  a .

Simple and straightforward in mainstream programming languages, such simple calculations are surprisingly challenging in Solidity, as we demonstrated in a previous article . There are two reasons for this: i) Solidity doesn't support fractions; ii) numeric types in Solidity can overflow.

In Javascript, one can simply compute x  ×  y  ÷  zx*y/z like this : such an expression would not pass a security audit in solidity, because for large enough x and y the multiplication might overflow and the result might be incorrect. Using SafeMath doesn't help much because it can cause transactions to fail even if the final calculation fits in 256 bits. In the previous article, we called this situation "phantom overflow". Doing division before multiplication, e.g. x/z*yor y/z*xcan solve the phantom overflow problem, but may cause loss of precision.

In this article, we discover what are the better ways to work with percentages and ratios in Solidity .

go full scale

The goal of this article is to implement the following functionality in Solidity:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint)

Computes x  ×  y  ÷  z , rounds the result down, and throws if z is zero or the result does not fit uint. Let's start with this simple solution:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint)
{
  return x * y / z;
}

This solution basically meets most needs: it seems to compute x  ×  y  ÷  z , round the result down, and throw if z is zero. However, there is a problem: what it actually calculates is x  ×  mod  2²⁵⁶  ÷  z . This is how multiplicative overflow works in Solidity. When the multiplication result does not fit in 256 bits, only the lowest 256 bits of the result are returned. For small x and y values, when x  ×  y  < 2²⁵⁶, there is no difference, but for larger x and y this produces incorrect results. So the first question is:

How can we prevent overflow?

Spoiler alert: we shouldn't.

A common way to prevent multiplication overflow in Solidity is to use mula function from the SafeMath library:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint)
{
  return mul (x, y) / z;
}

This code guarantees correct results, so now all requirements seem to be met, right? not so fast.

The requirement is to recover if the result doesn't fit uint, and this implementation seems to satisfy it. However, even if the final result fits, this implementation reverts when x  ×  y does not fit. uintWe call this situation "phantom overflow". In the previous article, we showed how to solve the phantom overflow problem at the cost of precision, but that solution won't work here because we need precise results.

Since just restoring the phantom overflow is not an option, then

How can we avoid phantom overflow and maintain precision?

Spoiler alert: simple math trick.

Let's make the following substitutions: x  =  a  ×  z  +  b and y  =  c  ×  z  +  d , where a , b , c and d are integers and 0 ≤  b  <  z and 0 ≤  d  <  z . Then:

x × y ÷ z =
a × z + b )×( c × z + d )÷ z =
a ×c×  +( a × d + b × c )× z + b × d )÷ z =
a × c × z + a × d + b × c + b × d ÷ z

The values ​​a , b , c , and d can be calculated as quotients and reminders by dividing x and y by z , respectively .

Therefore, the function can be rewritten like this:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint)
{
  uint a = x / z; uint b = x % z; // x = a * z + b
  uint c = y / z; uint d = y % z; // y = c * z + d
  return a * b * z + a * d + b * c + b * d / z;
}

Here we use plain  +and  *operators for readability, while real code should use SafeMath functions to prevent real, i.e. non-phantom overflows.

Phantom overflow is still possible in this implementation, but only for the last item: b * d / z. However, this code is guaranteed to work when z  ≤ 2¹²⁸, since b and d are both smaller than z , so b  ×  d is guaranteed to fit in 256 bits. Therefore, this implementation can be used when z is known not to exceed 2¹²⁸. A common example is fixed-point multiplication with 18 decimal places: x  ×  y  ÷10¹⁸. but,

How can we completely avoid phantom overflow?

Spoiler alert: use wider numbers.

The root of the phantom overflow problem is that the intermediate multiplication result does not fit in 256 bits. So, let's use a wider type. Solidity doesn't natively support numeric types larger than 256 bits, so we have to emulate them. We basically need two operations: uint  ×  uint  →  wide  and  wide  ÷  uint  →  uint .

Since the product of two 256-bit unsigned integers cannot exceed 512 bits, wider types must be at least 512 bits wide. We can emulate 512-bit unsigned integers in Solidity by having a pair of two 256-bit unsigned integers holding the low and high 256-bit parts of the entire 512-bit number respectively.

So the code might look like this:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint)
{
  (uint l, uint h) = fullMul (x, y);
  return fullDiv (l, h, z);
}

This fullMulfunction multiplies two 256-bit unsigned integers and returns the result as a 512-bit unsigned integer divided into two 256-bit parts. The function fullDivdivides a 512-bit unsigned integer, passed as two 256-bit parts, but the 256-bit unsigned integer and returns the result as a 256-bit unsigned integer.

Let's implement these two functions in school math fashion:

function fullMul (uint x, uint y)
public pure returns (uint l, uint h)
{
  uint xl = uint128 (x); uint xh = x >> 128;
  uint yl = uint128 (y); uint yh = y >> 128;
  uint xlyl = xl * yl; uint xlyh = xl * yh;
  uint xhyl = xh * yl; uint xhyh = xh * yh;

  uint ll = uint128 (xlyl);
  uint lh = (xlyl >> 128) + uint128 (xlyh) + uint128 (xhyl);
  uint hl = uint128 (xhyh) + (xlyh >> 128) + (xhyl >> 128);
  uint hh = (xhyh >> 128);
  l = ll + (lh << 128);
  h = (lh >> 128) + hl + (hh << 128);
}

and

function fullDiv (uint l, uint h, uint z)
public pure returns (uint r) {
  require (h < z);
  uint zShift = mostSignificantBit (z);
  uint shiftedZ = z;
  if (zShift <= 127) zShift = 0;
  else
  {
    zShift -= 127;
    shiftedZ = (shiftedZ - 1 >> zShift) + 1;
  }
  while (h > 0)
  {
    uint lShift = mostSignificantBit (h) + 1;
    uint hShift = 256 - lShift;
    uint e = ((h << hShift) + (l >> lShift)) / shiftedZ;
    if (lShift > zShift) e <<= (lShift - zShift);
    else e >>= (zShift - lShift);
    r += e;
    (uint tl, uint th) = fullMul (e, z);
    h -= th;
    if (tl > l) h -= 1;
    l -= tl;
  }
  r += l / z;
}

This mostSignificantBitis a function that returns the zero-based index of the most significant bit of the argument. This functionality can be achieved by:

function mostSignificantBit (uint x) public pure returns (uint r) {
  require (x > 0);
  if (x >= 2**128) { x >>= 128; r += 128; }
  if (x >= 2**64) { x >>= 64; r += 64; }
  if (x >= 2**32) { x >>= 32; r += 32; }
  if (x >= 2**16) { x >>= 16; r += 16; }
  if (x >= 2**8) { x >>= 8; r += 8; }
  if (x >= 2**4) { x >>= 4; r += 4; }
  if (x >= 2**2) { x >>= 2; r += 2; }
  if (x >= 2**1) { x >>= 1; r += 1; }
}

The code above is rather complex and should probably be explained, but we'll skip the explanation for now and focus on a different problem. The problem with this code is that it consumes about 2.5K gas per function call  mulDiv, which is quite a lot. so,

Can we do it cheaper?

Spoiler alert: math magic!

The code below is based on the exciting mathematical discovery

. If you like this code, please like his "mathemagic" article.

First, we rewrite fullMulthe function:

function fullMul (uint x, uint y)
public pure returns (uint l, uint h)
{
  uint mm = mulmod (x, y, uint (-1));
  l = x * y;
  h = mm - l;
  if (mm < l) h -= 1;
}

fullMulThis saves about 250 gas per call.

Then we rewrite mulDivthe function:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint) {
  (uint l, uint h) = fullMul (x, y);
  require (h < z);
  uint mm = mulmod (x, y, z);
  if (mm > l) h -= 1;
  l -= mm;
  uint pow2 = z & -z;
  z /= pow2;
  l /= pow2;
  l += h * ((-pow2) / pow2 + 1);
  uint r = 1;
  r *= 2 - z * r;
  r *= 2 - z * r;
  r *= 2 - z * r;
  r *= 2 - z * r;
  r *= 2 - z * r;
  r *= 2 - z * r;
  r *= 2 - z * r;
  r *= 2 - z * r;
  return l * r;
}

This implementation only consumes ~550 gas per call mulDivand can be further optimized. 5 times better than school math methods. very good! But one really has to have a PhD in mathematics to write such code, and not every problem has such a magical mathematical solution. It would be much simpler if we could

Working with floating point numbers in Solidity

As we said at the beginning of this article, in JavaScript, you simply write a * b / cand the language takes care of the rest. What if we could do the same in Solidity?

Actually we can. While the core language doesn't support floating point, some libraries do. For example, for the ABDKMathQuad library, one could write:

function mulDiv (uint x, uint y, uint z)
public pure returns (uint) {
  return
    ABDKMathQuad.toUInt (
      ABDKMathQuad.div (
        ABDKMathQuad.mul (
          ABDKMathQuad.fromUInt (x),
          ABDKMathQuad.fromUInt (y)
        ),
        ABDKMathQuad.fromUInt (z)
      )
    );
}

Not as elegant as JavaScript, not as cheap as math magic solutions (and even more extensive than school math), but straightforward and very precise, since the quad-precision floats used here have about 33 significant decimal places.

Over half of the gas consumption of this implementation is spent converting uintvalues ​​to floats and back, the scale calculation itself only consumes about 1.4K gas. Therefore, it may be much cheaper to use floating point numbers in all smart contracts than to mix integers and floating point numbers.

in conclusion

Percentages and ratios can be challenging in Solidity due to overflow and lack of fraction support. However, various mathematical tricks can solve proportional problems correctly and efficiently.

Library-supported floats might make life better at the cost of gas and precision.

In our next article, we will explore financial mathematics in more depth

Guess you like

Origin blog.csdn.net/weixin_39842528/article/details/128729693