Precise Numerical Calculations in Rust Smart Contracts: Integers vs Floating-Point Numbers

Rust smart contracts Development Diary (7): Numerical Calculation

Previous Review:

  • Rust smart contracts development diary (1) contract status data definition and method implementation
  • Rust smart contracts development diary ( Writing unit tests for Rust smart contracts
  • Rust smart contracts cultivation diary ) 3( Rust smart contracts deployment, function calls and usage of Explorer
  • Rust smart contracts development diary ) 4( Rust smart contracts integer overflow
  • Rust smart contracts development diary )5( reentrancy attack
  • Rust smart contracts development diary ) Denial of Service Attack

1. Precision Issues in Floating Point Arithmetic

Unlike the commonly used smart contract programming language Solidity, the Rust language natively supports floating-point arithmetic. However, floating-point arithmetic has unavoidable precision issues. Therefore, it is not recommended to use floating-point arithmetic when writing smart contracts, especially when dealing with ratios or interest rates that involve important economic/financial decisions.

Most mainstream programming languages that represent floating-point numbers generally follow the IEEE 754 standard, and Rust is no exception. The following is an explanation of the double-precision floating-point type f64 in Rust and the binary data storage format in computers:

Floating-point numbers are expressed using scientific notation with a base of 2. For example, the decimal 0.8125 can be represented by the finite binary number 0.1101, and the specific conversion method is as follows:

0.8125 * 2 = 1 .625 // 0.1      The first digit of the binary decimal is 1
0.625  * 2 = 1 .25  // 0.11     The 2nd binary decimal obtained is 1  
0.25   * 2 = 0 .5   // 0.110    The 3rd binary decimal is 0
0.5    * 2 = 1 .0   // 0.1101   The 4th digit of the binary decimal is 1

That is 0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1

However, for another decimal 0.7, there will be the following issues during its actual conversion to a floating-point number:

0.7 x 2 = 1. 4 // 0.1
0.4 x 2 = 0. 8 // 0.10
0.8 x 2 = 1. 6 // 0.101
0.6 x 2 = 1. 2 // 0.1011
0.2 x 2 = 0. 4 // 0.10110
0.4 x 2 = 0. 8 // 0.101100
0.8 x 2 = 1. 6 // 0.1011001
....

The decimal 0.7 will be represented as 0.101100110011001100.....(, an infinite loop ), which cannot be accurately represented by a finite-length floating-point number, and there is a "舍入(Rounding)" phenomenon.

Assuming that on the NEAR blockchain, it is necessary to distribute 0.7 NEAR tokens to ten users, the specific amount of NEAR tokens each user receives will be calculated and stored in the result_0 variable.

#(
fn precision_test_float)( {
    // Floating point numbers cannot accurately represent integers
    let amount: f64 = 0.7;     // The variable amount represents 0.7 NEAR tokens
    let divisor: f64 = 10.0;   // define divisor
    let result_0 = a / b;     // Perform floating-point division operation
    println!)"The value of a: {:.20}", a[test];
    assert_eq!(result_0, 0.07, "");
}

The output result of executing this test case is as follows:

running 1 test
The value of a: 0.69999999999999995559
thread "tests::precision_test_float" panicked at "assertion failed: (left == right)
 left: 0.06999999999999999, right: 0.07: ", src/lib.rs:185:9

It can be seen that in the above floating-point operations, the value of amount does not accurately represent 0.7, but rather an extremely close value of 0.69999999999999995559. Furthermore, for a single division operation such as amount/divisor, the result will also become the imprecise 0.06999999999999999, rather than the expected 0.07. This illustrates the uncertainty of floating-point operations.

In this regard, we have to consider using other types of numeric representation methods in smart contracts, such as fixed-point numbers.

  1. According to the fixed position of the decimal point, fixed-point numbers can be divided into fixed-point ( pure ) integers and fixed-point ( pure ) decimals.
  2. If the decimal point is fixed after the lowest digit of the number, it is called a fixed-point integer.

In the actual writing of smart contracts, a fraction with a fixed denominator is usually used to represent a certain value, such as the fraction "x/N", where "N" is a constant and "x" can vary.

If "N" takes the value of "1,000,000,000,000,000,000", which is "10^18", at this time the decimal can be represented as an integer, like this:

1.0 ->  1_000_000_000_000_000_000
0.7 ->    700_000_000_000_000_000
3.14 -> 3_140_000_000_000_000_000

In the NEAR Protocol, the common value for N is "10^24", which means 10^24 yoctoNEAR is equivalent to 1 NEAR token.

Based on this, we can modify the unit test in this section to calculate in the following way:

#(
fn precision_test_integer)( {
    // First, define the constant N, representing precision.
    let N: u128 =    1_000_000_000_000_000_000_000_000;  // This defines 1 NEAR = 10^24 yoctoNEAR
    // Initialize amount, at this time the value represented by amount is 700_000_000_000_000_000 / N = 0.7 NEAR; 
    let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
    // Initialize divisor
    let divisor: u128 = 10; 
    // Calculate the result: result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
    // Actual representation 700_000_000_000_000_000_000_000 / N = 0.07 NEAR; 
    let result_0 = amount / divisor;
    assert_eq!)result_0, 70_000_000_000_000_000_000_000, ""[test];
}

The result of the numerical calculation can be obtained as follows: 0.7 NEAR / 10 = 0.07 NEAR

running 1 test
test tests::precision_test_integer ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s

2. The Issue of Precision in Rust Integer Calculations

From the description in Section 1 above, it can be seen that using integer operations can solve the problem of precision loss in floating-point operations in certain calculation scenarios.

However, this does not mean that the results of integer calculations are completely accurate and reliable. This section will introduce some of the reasons that affect the precision of integer calculations.

( 2.1 Order of Operations

For multiplication and division with the same arithmetic priority, changing their order may directly affect the calculation result, leading to issues with the precision of integer calculations.

For example, there is the following operation:

#)
fn precision_test_div_before_mul() {
    let a: u128 = 1_0000;
    let b: u128 = 10_0000;
    let c: u128 = 20;
    // result_0 = a * c / b
    let result_0 = a
        .checked_mul###c[test]
        .expect("ERR_MUL")
        .checked_div(b)
        .expect("ERR_DIV");
    // result_0 = a / b * c
    let result_1 = a
        .checked_div(b)
        .expect("ERR_DIV")
        .checked_mul(c)
        .expect("ERR_MUL");
    assert_eq!(result_0,result_1,"");
}

The results of the unit tests are as follows:

running 1 test
thread "tests::precision_test_0" panicked at "assertion failed: (left == right)
 left: 2, right: 0: ", src/lib.rs:175:9

We can find that result_0 = a * c / b and result_1 = (a / b) * c, although their calculation formulas are the same, the operation results are different.

The specific reason for the analysis is: for integer division, the precision less than the divisor will be discarded. Therefore, in the process of calculating result_1, the first calculation of (a / b) will lose calculation precision first, becoming 0; while in the calculation of result_0, the result of a * c is calculated first as 20_0000, which will be greater than the divisor b, thus avoiding the problem of precision loss and obtaining the correct calculation result.

( 2.2 too small magnitude

#)
fn precision_test_decimals() {
    let a: u128 = 10;
    let b: u128 = 3;
    let c: u128 = 4;
    let decimal: u128 = 100_0000;
    // result_0 = ###a / b[test] * c
    let result_0 = a
        .checked_div(b)
        .expect("ERR_DIV")

.checked_mul(c) .expect("ERR_MUL"); // result_1 = (a * decimal / b) * c / decimal;
let result_1 = a .checked_mul(decimal) // mul decimal .expect("ERR_MUL") .checked_div(b) .expect("ERR_DIV") .checked_mul(c) .expect("ERR_MUL") .checked_div(decimal) // div decimal .expect("ERR_DIV"); println!("{}:{}", result_0, result_1); assert_eq!(result_0, result_1, ""); }

The specific results of this unit test are as follows:

running 1 test
12:13
thread "tests::precision_test_decimals" panicked at "assertion failed: (left == right)
 left: 12, right: 13: ", src/lib.rs:214:9

The visible operation processes result_0 and result_1 are equivalent, yet their operation results are not the same, and result_1 = 13 is much closer to the actual expected calculation value: 13.3333....

3. How to Write Numeric Actuarial Rust Smart Contracts

Ensuring correct precision is very important in smart contracts. Although there is also the problem of precision loss in integer operations in Rust language, we can take some protective measures to improve precision and achieve satisfactory results.

( 3.1 Adjust the order of operations

  • Make integer multiplication take precedence over integer division.

) Increase the order of magnitude of integers 3.2

  • Use larger magnitudes for integers to create larger numerators.

For example, for a NEAR token, if we define N as described above to be 10, it means that to represent a NEAR value of 5.123, the actual integer value used in calculations will be represented as 5.123 * 10^10 = 51_230_000_000. This value continues to participate in subsequent integer calculations, which can improve calculation accuracy.

( 3.3 Accumulated loss of computational precision

For unavoidable integer calculation precision issues, the project team may consider recording the cumulative loss of calculation precision.

u128 to distribute tokens to USER_NUM users.

const USER_NUM: u128 = 3;

u128 { let token_to_distribute = offset + amount; let per_user_share = token_to_distribute / USER_NUM; println!)"per_user_share {}",per_user_share###; let recorded_offset = token_to_distribute - per_user_share * USER_NUM; recorded_offset } #### fn record_offset_test###( { let mut offset: u128 = 0; for i in 1..7 { println!)"Round {}",i(; offset = distribute)to_yocto("10"), offset[test]; println!("Offset {}\n",offset); } }

In this test case, the system distributes 10 tokens to 3 users each time. However, due to the issue of integer arithmetic precision, when calculating per_user_share in the first round, the result of the integer arithmetic is 10 / 3 = 3, meaning that the users in the first round will each receive an average of 3 tokens, with a total of 9 tokens being distributed.

At this time, it can be found that there is still 1 token left in the system that has not been distributed to users. Therefore, it can be considered to temporarily store the remaining token in the global variable offset of the system. When the system calls distribute again to distribute tokens to users, this value will be taken out and attempted to be distributed to users together with the amount of tokens distributed in this round.

The following is a simulated token distribution process:

running 1 test
Round 1
per_user_share 3
Offset1
Round 2
per_user_share 3
Offset 2
Round 3
per_user_share 4
Offset 0
Round 4
per_user_share 3
Offset 1
Round 5
per_user_share 3
TOKEN3.11%
View Original
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
  • Reward
  • 6
  • Share
Comment
0/400
WalletWhisperervip
· 21h ago
fascinating how rust's floating points could be our next vulnerability honeypot... watching closely
Reply0
OnlyOnMainnetvip
· 21h ago
Floating point calculation + on-chain Haha scared me to death
View OriginalReply0
TopEscapeArtistvip
· 21h ago
Brothers, this precision issue is as accurate as when I step on the top.
View OriginalReply0
RamenDeFiSurvivorvip
· 21h ago
I'm out, I'm out. This precision issue is really annoying.
View OriginalReply0
NFTArchaeologistvip
· 21h ago
The issue of precision is the most fatal... If not done well, it could lead to total loss.
View OriginalReply0
MaticHoleFillervip
· 21h ago
When can I write a collection of debugging articles?
View OriginalReply0
Trade Crypto Anywhere Anytime
qrCode
Scan to download Gate app
Community
English
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)