Introduction to Fixed Point Numbers and Their Implementation in Solidity

Introduction to Fixed Point Numbers and Their Implementation in Solidity

Overview

Fixed point numbers, also known as fixed-point arithmetic, are a way of representing fractional numbers using a fixed number of decimal places. They are commonly used in financial applications and embedded systems where precision is important but floating-point arithmetic is not available or desirable.

Fixed Point Representation

Fixed point numbers are represented by a signed integer and a fixed number of decimal places. The decimal places are typically represented by a power of 10, such as 10^18 or 10^27. The signed integer represents the whole number part of the fixed point number, and the decimal places represent the fractional part.

For example, the fixed point number 12345678901234567890 in 18 decimal places represents the number 1234567.890123456789.

Fixed Point Operations

The basic fixed point operations are addition, subtraction, multiplication, and division. These operations can be implemented using regular integer arithmetic, but care must be taken to account for the decimal places.

For example, to add two fixed point numbers with 18 decimal places, we simply add the two integers together. The resulting integer will have 18 decimal places.

To multiply two fixed point numbers with 18 decimal places, we multiply the two integers together and then divide the result by 10^18. The resulting integer will have 18 decimal places.

The division of fixed point numbers is more complex. There are two common ways to divide fixed point numbers: rounded down and rounded up. Rounded down division always gives a result that is less than or equal to the true result. Rounded up division always gives a result that is greater than or equal to the true result.

Solidity, the programming language for writing smart contracts on the Ethereum blockchain, does not have a built-in fixed-point data type. However, it is possible to implement fixed-point arithmetic using libraries or custom functions. In the realm of blockchain development, where precision and accuracy are paramount, fixed point numbers play a pivotal role. These specialized numbers, often encountered in Solidity contracts and Ethereum smart contracts, allow developers to perform precise calculations in a decentralized environment.

If you are an intermediate developer with a strong foundation in Solidity and have already delved into Capture The Flag (CTF) exercises, then you're in the perfect position to explore the world of fixed point numbers and their applications. In this article, we will unravel the intricacies of fixed point numbers and the functions that make them work. We'll dive into the following key functions, using this codebase as our reference point: https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.md

In the provided code, there are four functions: mulWad, mulWadUp, divWad, and divWadUp.

Note: The above four functions in this codebase use an assembly block code which is marked with /// @solidity memory-safe-assembly, which indicates that each of the following assembly codes has been verified for memory safety.

  1. Function mulWad:
function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
    /// @solidity memory-safe-assembly
    assembly {
        // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
        if mul(y, gt(x, div(not(0), y))) {
            mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
            revert(0x1c, 0x04)
        }
        z := div(mul(x, y), WAD)
    }
}

This function calculates the product of two fixed-point numbers x and y and then divides the result by the scaling factor WAD (1e18), rounding down to the nearest integer. It uses inline assembly to ensure that the multiplication is safe and to add checks for potential errors. Let's take a look at how this function works and then provide an example.

Explanation:

  1. The assembly block is marked with /// @solidity memory-safe-assembly, which indicates that the following assembly code has been verified for memory safety.

  2. The function performs a check to prevent overflow. It does so by verifying that y is not zero and that x is less than or equal to the maximum value that a uint256 can hold divided by y. This is equivalent to require(y == 0 || x &lt;= type(uint256).max / y) in pure Solidity code.

  3. If the check fails, the function uses the mstore operation to write a specific error code to memory, and then it reverts the transaction with this error code. The error code, in this case, is 0xbac65e5b, indicating a "MulWadFailed" error.

  4. If the check passes, meaning it's safe to proceed with the multiplication, the function calculates the product of x and y, and then divides it by the scaling factor WAD (1e18) using the div operation.

Example:

Let's demonstrate how the mulWad function works with an example:

Suppose we have the following values:

  • x = 1500000000000000000 (represents 1.5 in fixed-point)

  • y = 2000000000000000000 (represents 2.0 in fixed-point)

Before performing the multiplication, the function checks whether it's safe to proceed. In this case, it is safe because:

  • y is not zero.

  • x (1.5) is less than or equal to the maximum value of a uint256 (which is 2^256 - 1) divided by y (2.0), which is 1e18.

So, the function proceeds with the multiplication:

z := div(mul(1500000000000000000, 2000000000000000000), WAD)

z := div(3000000000000000000000000000, 1000000000000000000000)

z := 3000

The final result is that z represents 3.0 in fixed-point, and the function executes without errors because the checks for overflow and division by zero have passed in this example.

This function is particularly useful in financial applications where precise multiplication of fixed-point numbers is required, and the checks help ensure that the operations are safe and do not result in unexpected errors.

Use Cases:

  • DeFi Platforms: Used for accurate calculations of interest rates, asset prices, and trading pairs in decentralized finance applications.

  • Stablecoin Systems: Ensures precise calculations for stablecoin mechanisms and maintaining price stability.

  • Cryptocurrency Exchanges: Provides accurate trading rates in decentralized exchanges (DEXs).

  • Insurance Contracts: Used in premium calculations and claim payouts.

  1. Function mulWadUp:

    • This function is similar to mulWad, but it rounds the result up to the nearest integer.

    • mulWadUp takes two uint256 inputs, x and y, representing fixed-point numbers to be multiplied.

    • It uses inline assembly to ensure safe multiplication and adds checks to prevent overflow and division by zero.

    • The function calculates the product of x and y, rounds the result up to the nearest integer, and adjusts the scaling factor using WAD (1e18).

function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
    /// @solidity memory-safe-assembly
    assembly {
        // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
        if mul(y, gt(x, div(not(0), y))) {
            mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
            revert(0x1c, 0x04)
        }
        z := add(iszero(iszero(mod(mul(x, y), WAD)), div(mul(x, y), WAD))
    }
}

Explanation:

  1. The function begins with a check similar to mulWad. It verifies that the product x * y would not cause overflow or division by zero. This is equivalent to require(y == 0 || x <= type(uint256).max / y) in pure Solidity code.

  2. If the check fails, the function uses the mstore operation to write an error code to memory and reverts the transaction. The error code in this case is 0xbac65e5b, indicating a "MulWadFailed" error.

  3. If the check passes, indicating that it's safe to proceed with the multiplication, the function calculates the product of x and y. It then uses mod and div operations to round the result up and adjust the scaling factor, ensuring that the result is always greater than or equal to the actual mathematical product of x and `y.

Example:

Let's demonstrate how the mulWadUp function works with an example:

Suppose we have the following values:

  • x = 1500000000000000000 (represents 1.5 in fixed-point)

  • y = 2000000000000000000 (represents 2.0 in fixed-point)

Before performing the multiplication, the function checks whether it's safe to proceed. In this case, it is safe because:

  • y is not zero.

  • x (1.5) is less than or equal to the maximum value of a uint256 (which is 2^256 - 1) divided by y (2.0), which is 1e18.

So, the function proceeds with the multiplication:

z := add(iszero(iszero(mod(mul(x, y), WAD)), div(mul(x, y), WAD))

z := add(iszero(iszero(mod(1500000000000000000 * 2000000000000000000, 1000000000000000000000)), div(1500000000000000000 * 2000000000000000000, 1000000000000000000000))

z := add(iszero(iszero(3000000000000000000 % 1000000000000000000000)), div(3000000000000000000, 1000000000000000000000))

z := add(iszero(iszero(0)), div(3000000000000000000, 1000000000000000000000))

Since iszero(0) is true and div(3000000000000000000, 1000000000000000000000) is 3, the final result is z = 3.

In this example, the mulWadUp function rounds up the result of the multiplication, ensuring that the result is always greater than or equal to the actual mathematical product of x and `y`. The function executes without errors because the checks for overflow and division by zero have passed.

Use Cases:

  • Financial Calculations: Ensures precise financial calculations, especially when rounding up is necessary for interest, fees, and risk assessments.

  • Cryptocurrency Trading: Used in trading platforms to offer traders more favourable rates by rounding up results.

  • Loan and Mortgage Calculators: Provides accurate monthly payments and interest calculations in financial applications.

  • Insurance Premiums: Calculates insurance premiums with rounding up for accurate pricing.

  1. Function divWad;
  • The divWad function is used for fixed-point division, and it ensures that the division is performed safely and accurately. It divides the product of x and a scaling factor WAD (1e18) by y while applying checks to avoid division by zero or overflow. Let's break down how this function works and provide an example.

      function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
          /// @solidity memory-safe-assembly
          assembly {
              // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
              if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
                  mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
                  revert(0x1c, 0x04)
              }
              z := div(mul(x, WAD), y);
          }
      }
    

Explanation:

  1. The function starts with a check to prevent division by zero. It verifies that y is not zero and that x is less than or equal to the maximum value of a uint256 divided by WAD (1e18). This is equivalent to require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD) in pure Solidity code.

  2. If the check fails, the function uses the mstore operation to write an error code to memory and then reverts the transaction. The error code in this case is 0x7c5f487d, indicating a "DivWadFailed" error.

  3. If the check passes, indicating that it's safe to proceed with the division, the function calculates the product of x and WAD and then divides it by y. This effectively divides the fixed-point number x by y.

Example:

How divWad function works with an example:

Suppose we have the following values:

  • x = 2000000000000000000 (represents 2.0 in fixed-point)

  • y = 1500000000000000000 (represents 1.5 in fixed-point)

Before performing the division, the function checks whether it's safe to proceed. In this case, it is safe because:

  • y is not zero.

  • x (2.0) is less than or equal to the maximum value of a uint256 divided by WAD (1e18).

So, the function proceeds with the division:

z := div(mul(2000000000000000000, 1000000000000000000), 1500000000000000000)

z := div(2000000000000000000000000000, 1500000000000000000)

z := 1333

The final result is that z represents 1.333 in fixed-point.

The divWad function is valuable in various scenarios where accurate fixed-point division is required, such as

Use Cases:

  • Lending and Borrowing Platforms: Ensures precise interest calculations and loan management in DeFi lending platforms.

  • Cryptocurrency Exchanges: Provides accurate exchange rates and asset distribution in trading applications.

  • Insurance Contracts: Calculates premiums and payout amounts with precision.

  • Token Distribution and Vesting: Allocates tokens and manages vesting schedules.

  1. Function divWadUp : This function is used for fixed-point division, similar to divWad, but with the key difference that it rounds the result up to the nearest integer. This function divides the product of x and a scaling factor WAD (1e18) by y while applying checks to prevent division by zero and overflow. Breakdown of how this works:
function divWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
    /// @solidity memory-safe-assembly
    assembly {
        // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
        if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
            mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
            revert(0x1c, 0x04)
        }
        z := add(iszero(iszero(mod(mul(x, WAD), y)), div(mul(x, WAD), y))
    }
}
  1. The function begins with a check to prevent division by zero. It verifies that y is not zero and that x is less than or equal to the maximum value of a uint256 divided by WAD (1e18). This is equivalent to require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD) in pure Solidity code.

  2. If the check fails, the function uses the mstore operation to write an error code to memory and then reverts the transaction. The error code in this case is 0x7c5f487d, indicating a "DivWadFailed" error.

  3. If the check passes, indicating that it's safe to proceed with the division, the function calculates the product of x and WAD and rounds the result up to the nearest integer using the mod and div operations.

Example:

Let's demonstrate how the divWadUp function works with an example:

Suppose we have the following values:

  • x = 2000000000000000000 (represents 2.0 in fixed-point)

  • y = 1500000000000000000 (represents 1.5 in fixed-point)

Before performing the division, the function checks whether it's safe to proceed. In this case, it is safe because:

  • y is not zero.

  • x (2.0) is less than or equal to the maximum value of a uint256 divided by WAD (1e18).

So, the function proceeds with the division:

z := add(iszero(iszero(mod(mul(x, WAD), y)), div(mul(x, WAD), y))

z := add(iszero(iszero(mod(2000000000000000000 * 1000000000000000000, 1500000000000000000)), div(2000000000000000000 * 1000000000000000000, 1500000000000000000))

z := add(iszero(iszero(2000000000000000000000000000 % 1500000000000000000)), div(2000000000000000000000000000, 1500000000000000000))

z := add(iszero(iszero(1000000000000000000)), div(1333333333333333333333333333, 1500000000000000000))

Since iszero(1000000000000000000) is false and div(1333333333333333333333333333, 1500000000000000000) is approximately 888888, the final result is z = 888888.

Use Cases:

The divWadUp function is valuable in scenarios where accurate fixed-point division is required, and rounding the result up to the nearest integer is important.

Use cases:

  • Financial Calculations: Provides precise division with rounding up, ensuring that results are not underestimated.

  • Cryptocurrency Trading: Offers users more favourable rates by rounding up division results.

  • Loan and Mortgage Calculators: Ensures accurate monthly payments and amortization schedules.

  • In-Game Economics: Maintains fairness in item pricing, rewards, and transaction settlements in blockchain-based games.