Diario de desarrollo de contratos inteligentes en Rust (7): problema de precisión en cálculos enteros
Revisión de ediciones anteriores:
Diario de desarrollo de contratos inteligentes en Rust (1) Definición de datos de estado del contrato y implementación de métodos
Diario de desarrollo de contratos inteligentes en Rust (2) Escribir pruebas unitarias para contratos inteligentes en Rust
Diario de desarrollo de contratos inteligentes Rust (3) Despliegue de contratos inteligentes Rust, llamada de funciones y uso de Explorer
Diario de desarrollo de contratos inteligentes Rust ( 4) Rust contratos inteligentes desbordamiento de enteros
Diario de desarrollo de contratos inteligentes Rust (5) ataque de reingreso
Diario de desarrollo de contratos inteligentes en Rust (6) ataque de denegación de servicio
1. Problemas de precisión en las operaciones de punto flotante
A diferencia de Solidity, Rust admite de manera nativa operaciones de punto flotante. Sin embargo, las operaciones de punto flotante presentan problemas de precisión inevitables, por lo que no se recomienda su uso en contratos inteligentes, especialmente al tratar con tasas o intereses que implican decisiones económicas/financieras importantes.
Rust sigue el estándar IEEE 754 para representar números de punto flotante. El tipo de punto flotante de doble precisión f64 se representa internamente en la computadora utilizando notación científica en binario.
Algunos decimales pueden ser representados con precisión en binario con un número finito de dígitos, como 0.8125 que se puede representar como 0.1101. Pero un decimal como 0.7 generará una representación binaria cíclica infinita, lo que hace que no se pueda representar con precisión utilizando un número finito de punto flotante, existiendo un problema de "redondeo".
En el ejemplo de distribuir 0.7 tokens NEAR a 10 usuarios en la cadena de bloques NEAR:
óxido
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("El valor de amount: {:.20}", amount);
assert_eq!(result_0, 0.07);
}
El resultado de la ejecución muestra que el valor real de amount es 0.69999999999999995559, result_0 es 0.06999999999999999, que no es igual a lo esperado de 0.07.
Para resolver este problema, se pueden usar números de punto fijo. En NEAR, normalmente se representa 1 token NEAR con 10^24 yoctoNEAR. Código modificado:
óxido
#[test]
fn precision_test_integer() {
let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000;
let divisor: u128 = 10;
let result_0 = amount / divisor;
assert_eq!(result_0, 70_000_000_000_000_000_000_000);
}
Así se puede obtener un resultado preciso: 0.7 NEAR / 10 = 0.07 NEAR.
2. El problema de precisión en los cálculos enteros de Rust
Aunque las operaciones con números enteros pueden resolver problemas de precisión de números de punto flotante en ciertos escenarios, las operaciones con enteros también tienen problemas de precisión.
2.1 Orden de operaciones
La multiplicación y la división de igual nivel, el cambio de orden puede afectar el resultado:
óxido
#[test]
fn precision_test_div_before_mul() {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
let result_0 = a.checked_mul(c).unwrap().checked_div(b).unwrap();
let result_1 = a.checked_div(b).unwrap().checked_mul(c).unwrap();
assert_eq!(result_0,result_1);
}
Los resultados muestran result_0 = 2, result_1 = 0.
La razón es que la división entera descartará la precisión menor que el divisor. Al calcular result_1, (a / b) pierde primero precisión y se convierte en 0; mientras que result_0 calcula primero (a * c), lo que evita la pérdida de precisión.
2.2 cantidad demasiado pequeña
óxido
#[test]
fn precision_test_decimals() {
let a: u128 = 10;
let b: u128 = 3;
let c: u128 = 4;
let decimal: u128 = 100_0000;
let result_0 = a.checked_div(b).unwrap().checked_mul(c).unwrap();
let result_1 = a.checked_mul(decimal).unwrap()
.checked_div(b).unwrap()
.checked_mul(c).unwrap()
.checked_div(decimal).unwrap();
println!("{}:{}", result_0, result_1);
assert_eq!(result_0, result_1);
}
Los resultados muestran result_0 = 12, result_1 = 13, siendo este último más cercano al valor real 13.3333.
3. Cómo escribir contratos inteligentes de evaluación numérica en Rust
Para aumentar la precisión, se pueden tomar las siguientes medidas:
3.1 Ajustar el orden de las operaciones
Hacer que la multiplicación de enteros tenga prioridad sobre la división.
3.2 aumentar el orden de magnitud de los enteros
Usar una escala mayor para crear moléculas más grandes. Por ejemplo, definir 1 NEAR = 10^24 yoctoNEAR.
3.3 pérdida de precisión en los cálculos acumulativos
Registrar y acumular la pérdida de precisión, compensar en cálculos posteriores:
óxido
const USER_NUM: u128 = 3;
u128 {
let token_to_distribute = offset + amount;
let per_user_share = token_to_distribute / USER_NUM;
let recorded_offset = token_to_distribute - per_user_share * USER_NUM;
recorded_offset
}
#(
fn record_offset_test)[test] {
let mut offset: u128 = 0;
for i in 1..7 {
offset = distribute(10_000_000_000_000_000_000_000_000, offset);
}
}
( 3.4 Uso de la biblioteca Rust Crate rust-decimal
Esta biblioteca es adecuada para cálculos financieros de decimales que requieren alta precisión y sin errores de redondeo.
) 3.5 Considerar el mecanismo de redondeo
En el diseño de contratos inteligentes, el redondeo generalmente sigue el principio de "me favorece": se redondea hacia abajo si me favorece, se redondea hacia arriba si me favorece, y rara vez se utiliza el redondeo convencional.
Esta página puede contener contenido de terceros, que se proporciona únicamente con fines informativos (sin garantías ni declaraciones) y no debe considerarse como un respaldo por parte de Gate a las opiniones expresadas ni como asesoramiento financiero o profesional. Consulte el Descargo de responsabilidad para obtener más detalles.
10 me gusta
Recompensa
10
5
Compartir
Comentar
0/400
DuckFluff
· hace19h
¡Vaya! ¡Los números de punto flotante están causando problemas de nuevo!
Ver originalesResponder0
MaticHoleFiller
· hace19h
¡Los que han caído en la trampa de la precisión vienen a compartir su experiencia!
Ver originalesResponder0
MetadataExplorer
· hace19h
La parte de los enteros a menudo se ignora, es bastante clave.
Ver originalesResponder0
MEVHunter
· hace19h
la precisión es un honeypot mev... mantén tus floats ajustados o serás rekt ser
Ver originalesResponder0
TrustlessMaximalist
· hace19h
Hay que evitar este agujero en las operaciones de punto flotante.
Problemas de precisión en cálculos enteros de contratos inteligentes en Rust y soluciones
Diario de desarrollo de contratos inteligentes en Rust (7): problema de precisión en cálculos enteros
Revisión de ediciones anteriores:
1. Problemas de precisión en las operaciones de punto flotante
A diferencia de Solidity, Rust admite de manera nativa operaciones de punto flotante. Sin embargo, las operaciones de punto flotante presentan problemas de precisión inevitables, por lo que no se recomienda su uso en contratos inteligentes, especialmente al tratar con tasas o intereses que implican decisiones económicas/financieras importantes.
Rust sigue el estándar IEEE 754 para representar números de punto flotante. El tipo de punto flotante de doble precisión f64 se representa internamente en la computadora utilizando notación científica en binario.
Algunos decimales pueden ser representados con precisión en binario con un número finito de dígitos, como 0.8125 que se puede representar como 0.1101. Pero un decimal como 0.7 generará una representación binaria cíclica infinita, lo que hace que no se pueda representar con precisión utilizando un número finito de punto flotante, existiendo un problema de "redondeo".
En el ejemplo de distribuir 0.7 tokens NEAR a 10 usuarios en la cadena de bloques NEAR:
óxido #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("El valor de amount: {:.20}", amount); assert_eq!(result_0, 0.07); }
El resultado de la ejecución muestra que el valor real de amount es 0.69999999999999995559, result_0 es 0.06999999999999999, que no es igual a lo esperado de 0.07.
Para resolver este problema, se pueden usar números de punto fijo. En NEAR, normalmente se representa 1 token NEAR con 10^24 yoctoNEAR. Código modificado:
óxido
#[test] fn precision_test_integer() { let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; let divisor: u128 = 10;
let result_0 = amount / divisor; assert_eq!(result_0, 70_000_000_000_000_000_000_000); }
Así se puede obtener un resultado preciso: 0.7 NEAR / 10 = 0.07 NEAR.
2. El problema de precisión en los cálculos enteros de Rust
Aunque las operaciones con números enteros pueden resolver problemas de precisión de números de punto flotante en ciertos escenarios, las operaciones con enteros también tienen problemas de precisión.
2.1 Orden de operaciones
La multiplicación y la división de igual nivel, el cambio de orden puede afectar el resultado:
óxido #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
}
Los resultados muestran result_0 = 2, result_1 = 0.
La razón es que la división entera descartará la precisión menor que el divisor. Al calcular result_1, (a / b) pierde primero precisión y se convierte en 0; mientras que result_0 calcula primero (a * c), lo que evita la pérdida de precisión.
2.2 cantidad demasiado pequeña
óxido #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
}
Los resultados muestran result_0 = 12, result_1 = 13, siendo este último más cercano al valor real 13.3333.
3. Cómo escribir contratos inteligentes de evaluación numérica en Rust
Para aumentar la precisión, se pueden tomar las siguientes medidas:
3.1 Ajustar el orden de las operaciones
Hacer que la multiplicación de enteros tenga prioridad sobre la división.
3.2 aumentar el orden de magnitud de los enteros
Usar una escala mayor para crear moléculas más grandes. Por ejemplo, definir 1 NEAR = 10^24 yoctoNEAR.
3.3 pérdida de precisión en los cálculos acumulativos
Registrar y acumular la pérdida de precisión, compensar en cálculos posteriores:
óxido const USER_NUM: u128 = 3;
u128 { let token_to_distribute = offset + amount; let per_user_share = token_to_distribute / USER_NUM; let recorded_offset = token_to_distribute - per_user_share * USER_NUM; recorded_offset }
#( fn record_offset_test)[test] { let mut offset: u128 = 0; for i in 1..7 { offset = distribute(10_000_000_000_000_000_000_000_000, offset); } }
( 3.4 Uso de la biblioteca Rust Crate rust-decimal
Esta biblioteca es adecuada para cálculos financieros de decimales que requieren alta precisión y sin errores de redondeo.
) 3.5 Considerar el mecanismo de redondeo
En el diseño de contratos inteligentes, el redondeo generalmente sigue el principio de "me favorece": se redondea hacia abajo si me favorece, se redondea hacia arriba si me favorece, y rara vez se utiliza el redondeo convencional.
![]###https://img-cdn.gateio.im/webp-social/moments-6e8b4081214a69423fc7ae022d05c728.webp###