WEB3DEV

Cover image for Decimais em Solidity: Uma Perspectiva de Ponto Fixo
Adriano P. Araujo
Adriano P. Araujo

Posted on

Decimais em Solidity: Uma Perspectiva de Ponto Fixo

Introdução

No mundo do desenvolvimento blockchain e contratos inteligentes, a precisão é vital. Para os desenvolvedores Solidity, a aritmética de ponto fixo é um conceito crucial que garante precisão em operações financeiras e matemáticas dentro de aplicativos descentralizados e contratos inteligentes. Neste artigo, exploraremos a aritmética de ponto fixo em Solidity, sua importância e casos de uso práticos. Este guia é destinado a desenvolvedores Solidity intermediários prontos para se aprofundar no mundo dos números e operações de ponto fixo.

Para o propósito deste artigo, que é fornecer uma visão geral da utilidade das bibliotecas matemáticas de Ponto Fixo, as funções que estaremos revisando estão nesta biblioteca FixedPoint, extraída da FixedPointMathLib da Solady.

A FixedPointMathLib é uma biblioteca abrangente com operações para números de ponto fixo. Inclui funções para calcular máximos, mínimos, valores absolutos, exponenciais, raízes quadradas e muito mais. Em particular, examinaremos quatro funções para entender como funcionam e como podem ser usadas em seu próximo projeto: mulWad, mulWadUp, divWad, divWadUp.

O que é Aritmética de Ponto Fixo?

Uma das desvantagens do Solidity é a limitação na representação e operação com valores decimais. A aritmética de ponto fixo surge como uma solução para superar esse problema. Ao utilizar a aritmética de ponto fixo, podemos trabalhar com números que têm (ou precisam ter) casas decimais. Tipicamente, números de ponto fixo são implementados como inteiros sem sinal com um número predeterminado de casas decimais.

Essa abordagem garante cálculos precisos multiplicando e/ou dividindo por um fator de precisão. Ao empregar essa técnica, os desenvolvedores podem realizar operações matemáticas envolvendo valores decimais com mais precisão. É uma ferramenta essencial ao lidar com transações financeiras ou qualquer cenário que exija cálculos precisos envolvendo frações, especialmente em aplicações DeFi.

Precisão de Ponto Fixo

O nível de precisão na aritmética de ponto fixo depende do número de casas decimais utilizadas. Na maioria das aplicações blockchain, a escolha é de 18 casas decimais, o que faz sentido, pois é a relação decimal entre ETH e wei. Isso é frequentemente representado como WAD (provavelmente abreviação de "wei as decimal", mas a origem parece incerta), e serve como base para as operações que exploraremos.

Você pode encontrar algumas aplicações que usam um valor RAY em vez disso, que tem 27 casas decimais.


uint256 internal constant WAD = 1e18;

Enter fullscreen mode Exit fullscreen mode

Usaremos WAD  repetidamente para garantir que todas as operações de ponto fixo sejam consistentes e estejam de acordo com a precisão desejada.

Mãos à Obra

Agora, vamos explorar as funções Solidity projetadas para operações de aritmética de ponto fixo, todas projetadas para manter a precisão com base nas 18 casas decimais definidas por  WAD.

Em relação ao cálculo, e também, por uma questão de simplicidade e para trabalhar com menos zeros, trabalharemos com um WAD de 100 (1e2). Isso significaria que o arredondamento seria feito com base em 100.

mulWad()


function mulWad(uint256 x, uint256 y) public pure returns (uint256 z) {

  assembly {

    // Equivalente a `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)

  }

}

Enter fullscreen mode Exit fullscreen mode

Observação: not(0) == type(uint256).max

A função mulWad calcula o produto de dois números de ponto fixo, x e y (que chamaremos de xy a partir deste ponto), e depois divide o resultado por WAD, arredondando para baixo até o inteiro mais próximo (sem fazer cálculos extras, pois o Solidity arredonda para baixo por padrão ao trabalhar com restos).

A função também inclui uma verificação de segurança para garantir que dividir xy por WAD não leve a problemas de overflowde inteiro. Se a divisão for segura, a função prossegue com o cálculo.

Basicamente, ela reverte se x > (type(uint256).max / y), pois isso significaria um overflowpotencial no resultado.

Exemplos:

  • Em relação à segurança, digamos, para simplificar, que type(uint256).max é 100. Se as entradas forem x = 5 e y = 25, a função reverteria, pois 5 > (100 / 25), revelando um overflow.

  • Se as entradas forem x = 5e2 (500) e y = 25e2 (2500), a função retornaria 12500, o que se traduz em 125.00.

  • Se quisermos multiplicar, digamos, 0.2  (tokens) * 25,11 (preço), inseriríamos x = 20 e y = 2511, e obteríamos _ 502_, o que se traduz em 5.02.

mulWadUp()


function mulWadUp(uint256 x, uint256 y) public pure returns (uint256 z) {

  assembly {

    // Equivalente a `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))

  }

}

Enter fullscreen mode Exit fullscreen mode

A função mulWadUp  funciona de forma semelhante ao  mulWad  na maneira como calcula o produto de dois números de ponto fixo, xy, e subsequentemente divide o resultado por  WAD. Assim como mulWad, a função também inclui uma verificação de segurança que fará a função reverter se x > type(uint256).max / y. No entanto, ela emprega uma estratégia de arredondamento distinta, arredondando para cima até o inteiro mais próximo.

Em termos de arredondamento, o que diferencia  mulWadUp de mulWad  é a utilização da operação   mod para calcular o resto quando xy é dividido por WAD. Se o resto não for zero, isso implica que é necessário arredondar para cima para manter a precisão na aritmética de ponto fixo. A função então adiciona 1 ao resultado da divisão de xy por WAD, garantindo que o resultado final seja arredondado para cima para o inteiro mais próximo sempre que necessário (considerando o ponto de precisão WAD).

Se quisermos traduzir o código assembly para Solidity, ficaria assim:


if ((x * y) % WAD > 0) {

 z = ((x * y) / WAD) + 1

}

Enter fullscreen mode Exit fullscreen mode

Exemplos:

  • Com entradas x = 5 e y = 25, a função nos retornaria 2. Isso ocorre porque o resultado, 125, é maior que 100. Então, nesse caso, ela arredondará para cima para 200, resultando em 2.

  • Com entradas x = 50 e y = 25, o resultado retornado seria 13, já que o resultado de 50 * 25 = 1250, então ele arredonda para cima para 1300.

  • Se quisermos multiplicar  0,2 (tokens) * 25,11 (preço), inseriríamos  x = 20y = 2511, e obteríamos  503, que é arredondado para cima a partir do valor  5.02 que obtivemos de um exemplo anterior de mulWad.

divWad()


function divWad(uint256 x, uint256 y) public pure returns (uint256 z) {

  assembly {

    // Equivalente a `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)

  }

}

Enter fullscreen mode Exit fullscreen mode

A função divWad oferece uma divisão precisa de números de ponto fixo. Assim como  mulWadmulWadUp, ela também inclui uma verificação de segurança para garantir que a divisão não leve a problemas de underflowde inteiro.

Como sempre em matemática, o denominador em uma divisão não pode ser 0, então a função inclui uma verificação de segurança para garantir isso para a variável _ y_ . Além disso, o código verifica que, se x > type(uint256).max / WAD, deverá reverter, pois isso significaria um overflow ao multiplicar x vezes WAD.

A divisão é então realizada multiplicando x por WAD para manter a precisão necessária e depois dividindo por  _ y_. Como mencionado anteriormente, não há necessidade de cálculos extras para arredondar para baixo, pois o Solidity funciona dessa maneira.

Exemplos:

  • Com entradas x = 28 e y = 5, a função retornaria 560, já que (28 * 100) / 5 = 560. Isso se traduziria em 5.60.

  • Com entradas x = 25 e y = 50, a função retornaria 50, já que (25 * 100) / 50 = 50. Isso se traduziria em 0.5 pois estamos (obviamente) levando em consideração o ponto de precisão WAD.

  • Com entradas x = 10 e y = 6, a função retornaria 166, já que (10*100) / 6 = 166.6. Isso se traduziria em 166, arredondando para baixo.

divWadUp()


function divWadUp(uint256 x, uint256 y) public pure returns (uint256 z) {

  assembly {

    // Equivalente a `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))

  }

}

Enter fullscreen mode Exit fullscreen mode

A função divWadUp oferece uma divisão precisa de números de ponto fixo, mas arredondando para cima até o inteiro mais próximo.

Como na função anterior, verificações de segurança para overflows/underflowsestão incluídas e o código é o mesmo que em divWad.

A divisão é realizada multiplicando x por WAD para manter a precisão necessária e depois dividindo por y. Semelhante a mulWadUp, a função também inclui uma operação mod para calcular o resto quando xy é dividido por y. Se o resto não for zero, isso implica que é necessário arredondar para cima para manter a precisão na aritmética de ponto fixo. A função então adiciona  1 ao resultado da divisão de  xy por  y, garantindo que o resultado final seja arredondado para cima para o inteiro mais próximo sempre que necessário (novamente, considerando o ponto de precisão  WAD).

Exemplos:

  • Com entradas x = 10 e y = 6, a função retornaria 167, já que ((10 * 100) / 6 ) + 1 = 167. Isso se traduziria em 1.67, que é a versão arredondada para cima do exemplo divWad.

  • Com entradas x = 100 e y = 300, a função retornaria 34, já que ((100 * 100) / 300 ) + 1 = 34. Isso se traduziria em 0.34 (1 dividido por 3 arredondado para cima).

  • Com entradas x = 21e2 e y = 4e2, a função retornaria 525, já que ((2100 * 100) / 400 ) = 525. Isso se traduziria em 5.25, que é a mesma versão não arredondada que divWad retornaria.

Significado da Aritmética de Ponto Fixo

A aritmética de ponto fixo é crucial no desenvolvimento de blockchain e contratos inteligentes, especialmente em finanças descentralizadas (DeFi) e aplicações financeiras. Aqui está o porquê ela é significativa:

  • Precisão: a aritmética de ponto fixo permite a representação e manipulação precisa de valores não inteiros, o que é fundamental em aplicações financeiras onde até as menores discrepâncias podem ter consequências significativas.

  • Consistência: o uso de um número fixo de casas decimais garante consistência em diferentes operações e contratos inteligentes. Na DeFi, onde interoperabilidade e confiança são vitais, essa consistência é crucial.

  • Segurança:  prevenir overflows e lidar com divisão por zero é crucial para a segurança de contratos inteligentes. A aritmética de ponto fixo fornece mecanismos para lidar com esses problemas, reduzindo o risco de vulnerabilidades.

  • Confiabilidade: a aritmética de ponto fixo minimiza erros de arredondamento e imprecisões, tornando os contratos inteligentes mais confiáveis e precisos em cálculos financeiros.

Casos de Uso da Aritmética de Ponto Fixo

Agora que o significado da aritmética de ponto fixo está explicado, vamos explorar alguns casos de uso chave no ecossistema web3:

  • Plataformas de Concessão e Contração de Empréstimos: plataformas DeFi como Aave e Compound usam bibliotecas de aritmética de ponto fixo para cálculo de taxa de juros, relação empréstimo-valor e cálculos de liquidação. Esses cálculos exigem aritmética precisa para garantir transações precisas e justas.

  • Exchanges Descentralizadas (DEXs): bibliotecas de aritmética de ponto fixo são usadas em DEXs para realizar cálculos para pares de negociação, fornecimento de liquidez e agricultura de rendimento. DEXs como Uniswap e Sushiswap usam bibliotecas semelhantes para calcular a quantidade de tokens a serem trocados e a quantidade de tokens a serem recebidos.

  • Organizações Autônomas Descentralizadas (DAOs) e protocolos de stablecoin: DAOs muitas vezes exigem cálculos financeiros precisos para propostas, mecanismos de votação e gestão de fundos. A MakerDAO utiliza bibliotecas de aritmética de ponto fixo para manter a estabilidade de sua stablecoin.

  • Stake de Tokens e Fornecimento de Liquidez: aplicações DeFi usam bibliotecas de aritmética de ponto fixo para calcular rendimentos e recompensas para provedores de liquidez. Esses cálculos exigem precisão para garantir que os usuários recebam a quantidade correta de recompensas proporcionalmente à sua participação.

  • NFTs e DAppsde Jogos: em DAppsde jogos e plataformas NFT, bibliotecas de aritmética de ponto fixo podem ser usadas para calcular mecânicas de jogo, tokenomics e distribuição de recompensas. Por exemplo, podem ser usadas para calcular a probabilidade de obter um item raro de uma caixa de itens ou as recompensas ganhas por um jogador em um jogo.

Conclusão

Essencialmente, qualquer cálculo matemático que exija precisão decimal pode se beneficiar desta biblioteca. Ao usar a aritmética de ponto fixo, podemos garantir que todos os cálculos sejam consistentes e robustos.

A aritmética de ponto fixo é um conceito fundamental para os desenvolvedores Solidity, especialmente no campo de aplicações DeFi. As funções fornecidas para operações de aritmética de ponto fixo garantem precisão, consistência e segurança em cálculos financeiros e matemáticos.

Para os desenvolvedores Solidity, entender a aritmética de ponto fixo e como usar essas funções é uma habilidade valiosa que aprimora a criação de contratos inteligentes confiáveis e seguros. Esteja você trabalhando em plataformas DeFi, stablecoins ou outras aplicações financeiras, e até mesmo em jogos, a aritmética de ponto fixo é uma ferramenta que aprimora as capacidades matemáticas de seus contratos inteligentes de maneira funcional e confiável.

Referências:

https://github.com/dapphub/ds-math

Se você achou este artigo útil, sinta-se à vontade para mostrar seu apoio através de uma doação para meu endereço ou via Kofi:

Sua contribuição seria muito apreciada!


Este artigo foi escrito por  Juan Xavier Valverde e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Oldest comments (0)