Diário de Desenvolvimento de Contratos Inteligentes Rust (7): Problema de Precisão em Cálculos Inteiros
Revisão de edições anteriores:
Diário de desenvolvimento de contratos inteligentes Rust (1) Definição de dados de estado do contrato e implementação de métodos
Diário de desenvolvimento de contratos inteligentes Rust (2) Escrevendo testes unitários para contratos inteligentes Rust
Diário de desenvolvimento de contratos inteligentes Rust (3) Implementação de contratos inteligentes Rust, chamadas de funções e uso do Explorer
Diário de desenvolvimento de contratos inteligentes Rust (4) Rust contratos inteligentes estouro de inteiro
Rust contratos inteligentes养成日记(5)重入攻击
Diário de desenvolvimento de contratos inteligentes Rust (6) ataque de negação de serviço
1. Problema de precisão em operações de ponto flutuante
Ao contrário do Solidity, Rust suporta nativamente operações com números de ponto flutuante. No entanto, as operações com ponto flutuante têm problemas de precisão inevitáveis, portanto, não é recomendado o seu uso em contratos inteligentes, especialmente ao lidar com taxas ou juros que envolvem decisões econômicas/financeiras importantes.
Rust segue o padrão IEEE 754 para representar números de ponto flutuante. O tipo de ponto flutuante de dupla precisão f64 é representado internamente em notação científica binária no computador.
Alguns números decimais podem ser representados com precisão em binário com um número finito de dígitos, como 0.8125 que pode ser representado como 0.1101. Mas decimais como 0.7 geram uma representação binária que se repete infinitamente, não podendo ser representados com precisão por números de ponto flutuante de tamanho finito, resultando em um problema de "arredondamento".
No exemplo de distribuição de 0,7 tokens NEAR para 10 usuários na blockchain NEAR:
ferrugem
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("O valor da quantia: {:.20}", quantia);
assert_eq!(result_0, 0.07);
}
O resultado da execução mostra que o valor real de amount é 0.69999999999999995559, e result_0 é 0.06999999999999999, que não é igual ao esperado de 0.07.
Para resolver este problema, pode-se usar números de ponto fixo. No NEAR, normalmente é usado 10^24 yoctoNEAR para representar 1 token NEAR. Código modificado:
ferrugem
#[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);
}
Assim, pode-se obter o resultado exato: 0,7 NEAR / 10 = 0,07 NEAR.
2. Problemas de precisão nos cálculos inteiros em Rust
Embora as operações inteiras possam resolver problemas de precisão de números de ponto flutuante em certos cenários, os cálculos inteiros também apresentam problemas de precisão.
2.1 Ordem das operações
A ordem das operações de multiplicação e divisão de mesmo nível pode afetar o resultado:
ferrugem
#[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);
}
Os resultados mostram result_0 = 2, result_1 = 0.
A razão é que a divisão inteira descarta a precisão que é menor que o divisor. Ao calcular result_1, (a / b) perde a precisão e se torna 0; enquanto result_0 primeiro calcula (a * c), evitando a perda de precisão.
2.2 magnitude muito pequeno
ferrugem
#[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);
}
Os resultados mostram que result_0 = 12, result_1 = 13, sendo este último mais próximo do valor real 13.3333.
3. Como escrever contratos inteligentes de cálculo numérico em Rust
Para melhorar a precisão, podem ser adotadas as seguintes medidas:
3.1 Ajustar a ordem das operações
Fazer com que a multiplicação de inteiros tenha prioridade sobre a divisão.
3.2 aumentar a ordem de grandeza dos inteiros
Usar uma magnitude maior, criar moléculas maiores. Por exemplo, definir 1 NEAR = 10^24 yoctoNEAR.
3.3 perda de precisão nos cálculos acumulados
Registar e acumular perdas de precisão, compensar nas operações subsequentes:
ferrugem
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 Usando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros de decimais que exigem alta precisão e sem erros de arredondamento.
) 3.5 considerar o mecanismo de arredondamento
Na concepção de contratos inteligentes, o arredondamento geralmente segue o princípio de "a meu favor": se arredondar para baixo for favorável, arredonda-se para baixo; se arredondar para cima for favorável, arredonda-se para cima; raramente se utiliza o arredondamento convencional.
Esta página pode conter conteúdos de terceiros, que são fornecidos apenas para fins informativos (sem representações/garantias) e não devem ser considerados como uma aprovação dos seus pontos de vista pela Gate, nem como aconselhamento financeiro ou profissional. Consulte a Declaração de exoneração de responsabilidade para obter mais informações.
17 gostos
Recompensa
17
8
Partilhar
Comentar
0/400
LostBetweenChains
· 07-25 17:53
Não consegue lidar com cálculos inteiros, que contratos está a brincar?
Ver originalResponder0
WagmiWarrior
· 07-25 11:45
Já vi o 7º artigo, é tão detalhado.
Ver originalResponder0
MetaMisfit
· 07-25 01:13
Rust tem muitos buracos em contratos inteligentes, não tem?
Ver originalResponder0
DuckFluff
· 07-22 22:44
Bons tempos, os números de ponto flutuante estão causando problemas novamente~
Ver originalResponder0
MaticHoleFiller
· 07-22 22:44
Aqueles que já caíram nas armadilhas da precisão vêm compartilhar suas experiências!
Ver originalResponder0
MetadataExplorer
· 07-22 22:44
A parte dos inteiros é muitas vezes ignorada, mas é bastante crucial.
Ver originalResponder0
MEVHunter
· 07-22 22:29
a precisão é um pote de mel mev... mantenha os seus floats apertados ou será rekt ser
Ver originalResponder0
TrustlessMaximalist
· 07-22 22:16
Esta operação de ponto flutuante deve ser evitada.
Problemas de precisão no cálculo de inteiros em contratos inteligentes Rust e soluções.
Diário de Desenvolvimento de Contratos Inteligentes Rust (7): Problema de Precisão em Cálculos Inteiros
Revisão de edições anteriores:
1. Problema de precisão em operações de ponto flutuante
Ao contrário do Solidity, Rust suporta nativamente operações com números de ponto flutuante. No entanto, as operações com ponto flutuante têm problemas de precisão inevitáveis, portanto, não é recomendado o seu uso em contratos inteligentes, especialmente ao lidar com taxas ou juros que envolvem decisões econômicas/financeiras importantes.
Rust segue o padrão IEEE 754 para representar números de ponto flutuante. O tipo de ponto flutuante de dupla precisão f64 é representado internamente em notação científica binária no computador.
Alguns números decimais podem ser representados com precisão em binário com um número finito de dígitos, como 0.8125 que pode ser representado como 0.1101. Mas decimais como 0.7 geram uma representação binária que se repete infinitamente, não podendo ser representados com precisão por números de ponto flutuante de tamanho finito, resultando em um problema de "arredondamento".
No exemplo de distribuição de 0,7 tokens NEAR para 10 usuários na blockchain NEAR:
ferrugem #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("O valor da quantia: {:.20}", quantia); assert_eq!(result_0, 0.07); }
O resultado da execução mostra que o valor real de amount é 0.69999999999999995559, e result_0 é 0.06999999999999999, que não é igual ao esperado de 0.07.
Para resolver este problema, pode-se usar números de ponto fixo. No NEAR, normalmente é usado 10^24 yoctoNEAR para representar 1 token NEAR. Código modificado:
ferrugem
#[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); }
Assim, pode-se obter o resultado exato: 0,7 NEAR / 10 = 0,07 NEAR.
2. Problemas de precisão nos cálculos inteiros em Rust
Embora as operações inteiras possam resolver problemas de precisão de números de ponto flutuante em certos cenários, os cálculos inteiros também apresentam problemas de precisão.
2.1 Ordem das operações
A ordem das operações de multiplicação e divisão de mesmo nível pode afetar o resultado:
ferrugem #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
}
Os resultados mostram result_0 = 2, result_1 = 0.
A razão é que a divisão inteira descarta a precisão que é menor que o divisor. Ao calcular result_1, (a / b) perde a precisão e se torna 0; enquanto result_0 primeiro calcula (a * c), evitando a perda de precisão.
2.2 magnitude muito pequeno
ferrugem #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
}
Os resultados mostram que result_0 = 12, result_1 = 13, sendo este último mais próximo do valor real 13.3333.
3. Como escrever contratos inteligentes de cálculo numérico em Rust
Para melhorar a precisão, podem ser adotadas as seguintes medidas:
3.1 Ajustar a ordem das operações
Fazer com que a multiplicação de inteiros tenha prioridade sobre a divisão.
3.2 aumentar a ordem de grandeza dos inteiros
Usar uma magnitude maior, criar moléculas maiores. Por exemplo, definir 1 NEAR = 10^24 yoctoNEAR.
3.3 perda de precisão nos cálculos acumulados
Registar e acumular perdas de precisão, compensar nas operações subsequentes:
ferrugem 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 Usando a biblioteca Rust Crate rust-decimal
Esta biblioteca é adequada para cálculos financeiros de decimais que exigem alta precisão e sem erros de arredondamento.
) 3.5 considerar o mecanismo de arredondamento
Na concepção de contratos inteligentes, o arredondamento geralmente segue o princípio de "a meu favor": se arredondar para baixo for favorável, arredonda-se para baixo; se arredondar para cima for favorável, arredonda-se para cima; raramente se utiliza o arredondamento convencional.
![]###https://img-cdn.gateio.im/webp-social/moments-6e8b4081214a69423fc7ae022d05c728.webp###