WEB3DEV

Cover image for Aritmética insegura
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Aritmética insegura

Postado em 13 de Dezembro de 2022

Underflow e Overflow

Não existem números excessivamente grandes na lógica matemática. Quaisquer dois números significantes podem ser somados para obter um resultado preciso. Números não sofrem overflow graças às linguagens de programação de alto nível, como JavaScript e Python. Embora o resultado, em algumas circunstâncias, possa ser infinito quando dois números positivos são somados, nunca pode produzir um resultado negativo. Valores de ponto flutuante não sofrem overflow em Java ou C++, enquanto números inteiros o fazem.

Em contraste às outras numerosas linguagens de programação, os tipos de dados inteiros da Solidity não são inteiros reais. Embora eles se assemelhem aos inteiros quando as quantidades são minúsculas, são impossíveis de expressar números grandes arbitrários.

O resultado da soma é grande demais e não pode ser armazenado no tipo de dados uint8, e o limite de uint8 é 2⁸ - 1 ou 255, portanto, o seguinte código resulta em um overflow:

// Padrão: Verifica se há underflow ou overflow
uint8 x = 255;
uint8 y = 1;
return x + y; // reverte a execução com um erro

// Wrapped na não-verificação: não verifica se há underflow ou overflow
unchecked {
 uint8 x = 255;
 uint8 y = 1;
 return x + y; //retorna 0
}
Enter fullscreen mode Exit fullscreen mode

Antes da Solidity 0.8.0, verificações para overflows e underflows de inteiros eram feitas usando o pacote SafeMath. A Solidity 0.8.0 e versões mais recentes conduzem essa verificação para nós graças ao compilador. Não-verificados ou verificados, ou modo “wrapping” (modo de “enrolamento”), são dois métodos com os quais o compilador pode gerenciar esses overflows.

Os overflows são reconhecidos pelo modo padrão “verificado”, o que resulta em uma afirmação com falha. Essa verificação pode ser desativada com “não-verificado…”, o que fará com que o overflow seja discretamente desconsiderado. Se o código acima fosse incluído no bloco de código “não-verificado {...}”, daria 0.

Mesmo no modo verificado, não pense que você está protegido contra problemas de overflow. Esse modo é o padrão para overflows. Se o overflow puder realmente ser evitado, um contrato inteligente pode se tornar preso em um determinado estado onde todas suas execuções falhem. É por isso que você deve sempre contar com a possibilidade de uma variável exceder ou cair abaixo do limite que você definiu ao escrever as verificações. Em um esforço de identificar prováveis overflows, tente empregar um requisito que limite o tamanho da entrada para um intervalo aceitável. Aqui está uma parte do contrato principal V2 para a Uniswap. Este tutorial não tem a intenção de cobrir as regras de negócios da função, mas você vê como os desenvolvedores da função _update requerem especificamente a entrada do usuário:

// atualiza as reservas e, na primeira chamada por bloco, acumuladores de preços
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
 // Verifica como eles colocaram explicitamente um requisito à entrada do usuário na função:
 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
 uint32 timeElapsed = blockTimestamp - blockTimestampLast;
 // overflow é desejado
 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
   // * nunca sofre overflows, e + overflow é desejado
   price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
   price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
 }
 reserve0 = uint112(balance0);
 reserve1 = uint112(balance1);
 blockTimestampLast = blockTimestamp;
 emit Sync(reserve0, reserve1);
}
Enter fullscreen mode Exit fullscreen mode

Quando o wrapping não-verificado é adequado?

Para garantir que aritmética que você empregou no seu contrato inteligente não vai ter overflow ou underflow, a Solidity executa uma verificação extra. Esses processos extras aumentam o opcode (código de operação) do contrato e aumentam o custo de gas. Podemos instruir o compilador a renunciar a verificação para overflow e underflow se estamos confiantes que as operações aritméticas que realizaremos como um elemento do contrato não terão nenhum desses resultados. Isso pode permitir que o usuário do contrato economize gas durante a execução de certas tarefas. Tome como exemplo a linha de código a seguir:

function foo(uint8 _limit) public {
 for (uint i; i < _limit;) {
   // faça alguma coisa que não mude o valor de i
   unchecked { ++i; }
 }
}
Enter fullscreen mode Exit fullscreen mode

Como limitamos o valor do pós-condicional do loop i para ficar estritamente dentro do intervalo das variáveis do limite uint8, não é necessário verificação para overflow e underflow nesse caso. O compilador já verifica que o limite não excede ou fica abaixo dos seus limites. Por isso, se o limite excede 28¹, ou 255, será gerado um erro. Portanto, seu valor não pode exceder 28-2, ou 254. Portanto, sem comprometer a segurança, é seguro colocar o incremento ++i abaixo do envolvimento “não-verificado” {...}. Essa pequena dica pode poupar até 30 a 40 gramas de gas por loop, que pode aumentar se houver muitas interações.

Bonus

Observe como nós declaramos i na interação, mas não o inicializamos no exemplo acima. Isso se deve ao fato de que, ao contrário de outras linguagens, o valor padrão de uma Uint Solidity é 0 e não apenas nulo. Podemos evitar o desperdício de combustível por não inicializar a variável explicitamente.

Note a substituição de ++i por i++. Para um número inteiro sem sinal, ++i utiliza menos gas que i++ iy i+=1, pois o pré-incremento é menos dispendioso. Independentemente de o otimizador estar ativado, essa afirmação é precisa.

Junte-se ao nosso programa beta agora https://www.olympix.ai/.

Originalmente publicado no https://www.linkedin.com.

Esse artigo foi escrito por Olympix e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)