Un número de punto fijo es un número entero que almacena solo el numerador de una fracción, mientras que el denominador está implícito.
Este tipo de aritmética no es necesaria en la mayoría de los lenguajes de programación porque tienen números de punto flotante. Es necesaria en Solidity porque Solidity solo tiene números enteros, y a menudo necesitamos realizar operaciones con números fraccionarios.
Los números de punto fijo se encuentran en la mayoría de los contratos inteligentes de DeFi, por lo que comprenderlos es imprescindible.
Por ejemplo, si el “denominador implícito” es 100, entonces un número de punto fijo que contiene “10” se interpreta como 0.1.
El número de punto fijo más común en Solidity es : la cantidad de “decimales” que tienen Ethereum y la mayoría de los tokens ERC-20. Cuando leemos el balance de una dirección de Ethereum, dividimos implícitamente el número por para determinar su cantidad de Ether. Por ejemplo, se interpreta que una dirección que tiene un balance de tiene 10 Ether, porque la división por está implícita.
El número de punto fijo con un denominador de es tan común que los ingenieros en la comunidad de Solidity se refieren a él como un “Wad” (el nombre fue introducido por primera vez por MakerDAO). A veces, un número de punto fijo de 18 dígitos se interpreta como que tiene los 18 dígitos más a la derecha asignados para los decimales, por ejemplo, el número “10” se muestra a continuación:
Sin embargo, hemos descubierto que este modelo mental hace que aprender la aritmética de punto fijo sea más difícil de entender, por lo que este artículo utilizará el modelo mental del número de punto fijo que contiene el numerador, y el denominador de de manera implícita.
En este artículo aprenderemos cómo hacer operaciones aritméticas con números de punto fijo y explicaremos cómo funcionan las bibliotecas populares de punto fijo.
Convertir un número entero a un número de punto fijo
Para convertir un número entero a un número de punto fijo, multiplica el número entero por el denominador implícito. Por ejemplo, “2 ether” es , por lo que al convertir el entero 2 a “2 ether” lo multiplicamos por . El denominador implícito de cancela el .
Multiplicar números de punto fijo
Para multiplicar dos números de punto fijo, seguimos las reglas de la multiplicación de fracciones:
- multiplicar los numeradores entre sí
- multiplicar los denominadores entre sí
- simplificar el resultado.
Por ejemplo:
Sin embargo, podemos optimizar este cálculo en la práctica porque el denominador siempre es el mismo en los números de punto fijo.
Ahora consideremos un conjunto diferente de fracciones que tienen un denominador común:
Sin embargo, no queremos devolver un resultado con un denominador implícito de porque eso no sería compatible con el denominador implícito que seleccionamos. Por lo tanto, necesitamos dividir el numerador y el denominador por para devolver un número de punto fijo consistente con nuestra elección de denominador.
Por lo tanto, si e son números de punto fijo con un denominador implícito podemos calcular su producto como .
Ejemplo de código para multiplicar números de punto fijo
La biblioteca Solady tiene una operación matemática mulWad para multiplicar dos números de punto fijo con un denominador Wad implícito (). A continuación, mostramos el código y luego explicamos cómo se relaciona con nuestra discusión anterior:

El algoritmo principal está en la parte inferior de la captura de pantalla (dentro del recuadro verde). Allí calculamos donde es WAD o (como se muestra en la parte superior de la captura de pantalla donde se declara WAD).
Ejemplo del mundo real
Supongamos que un usuario tiene 1 DAI (que tiene 18 decimales) y deseamos calcular su balance asumiendo que su depósito ha ganado un 15% de interés. Este es un ejemplo claro de la necesidad de la aritmética de punto fijo, ya que no podemos multiplicar directamente un número por 1.15 en Solidity.

El resultado es 1.15 después de dividir por 1e18. Por supuesto, en realidad no podemos dividir por 1e18 porque eso borraría los decimales. Necesitamos una representación de punto fijo porque 1.15 no se puede representar como un número entero. El código anterior puede ser probado en Remix aquí.
Multiplicar un número de punto fijo por un número entero
Multiplicar una fracción por un número entero es lo mismo que multiplicar por :
Por lo tanto, cuando multiplicamos un número de punto fijo por un número entero, no necesitamos ningún paso adicional. Simplemente interpretamos el valor de retorno como un número de punto fijo sin modificar el denominador.
Dividir números de punto fijo
Para dividir fracciones, “invertimos” la segunda fracción y las multiplicamos. Por ejemplo:
Ahora consideremos un ejemplo en el que tienen el mismo denominador:
Ten en cuenta que el denominador común de 10 se canceló. Si queremos representar 2 con un denominador implícito de 10 (es decir, como un número de punto fijo con denominador 10), necesitamos multiplicarlo por 10 nuevamente:
Por lo tanto, para un caso general de e que tienen un denominador común , si queremos expresar el resultado con un denominador implícito de , debemos hacer lo siguiente:
Por lo tanto, si e son números de punto fijo con un denominador implícito podemos calcular su cociente como .
/// @dev Equivalent to `(x * WAD) / y` rounded down.
function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to `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)
}
}
Si colocamos mulWad() y divWad() uno al lado del otro, podemos ver que la única diferencia entre ellos (en el paso de cálculo, no en la comprobación de desbordamiento), es que el caso de div está multiplicando por una fracción invertida.

Dividir un número de punto fijo por un número entero
Supongamos que queremos dividir 2.5 por 2 (o alguna fracción por un número entero en general). No es necesario convertir 2 a un número de punto fijo usando .
Dividir una fracción por un número entero es lo mismo que dividir el numerador de por .
Ten en cuenta que , no 11.666, porque estamos utilizando división de enteros, no de punto flotante. Simplemente dividimos el número de punto fijo por el número entero e interpretamos el resultado como un número de punto fijo. Al igual que al multiplicar un número de punto fijo por un entero, el denominador sigue siendo el mismo.
Sumar y restar números de punto fijo
Para sumar y restar fracciones que tienen el mismo denominador, simplemente se suman y se ignora el denominador. Interpretamos la suma como un número de punto fijo con el mismo denominador implícito que los sumandos. Por ejemplo,
Por lo tanto, al sumar números de punto fijo con el mismo denominador, simplemente sumamos los números de la misma manera que lo haríamos con enteros regulares.
Considera el ejemplo con un denominador implícito de 100:
Para calcularlo, simplemente hacemos , no necesitamos incorporar 100 en nuestro cálculo.
Números de punto fijo binarios vs decimales
Un número de punto fijo binario es un número de punto fijo donde el denominador se puede expresar como . Los números de punto fijo binarios generalmente se denotan con la notación Q. Por ejemplo, UQ112x112 usa como denominador. La U significa “unsigned” (sin signo). El tipo de dato usado para almacenar UQ112x112 sería 224. Otra forma de interpretar esto es que la “parte fraccionaria después del decimal” se almacena en los 112 bits más a la derecha y la “parte entera” se almacena en los 112 bits de la izquierda.
Como otro ejemplo, UQ64x64 (o UQ64.64) es un uint128 que almacena la “parte fraccionaria” en los 64 bits menos significativos y el “número entero” en los bits más significativos. Esto aún puede interpretarse como tener un denominador implícito de , como veremos a continuación.
La ventaja de un número de punto fijo binario es que podemos usar un desplazamiento de bits hacia la izquierda (left bit shift), que es eficiente en el uso de gas, en lugar de multiplicar por el denominador (al convertir un entero a un número de punto fijo), o hacer un desplazamiento de bits hacia la derecha (right bit shift) al dividir.
Como ejemplo básico, considera que:
(1) 2 tiene una representación binaria de 10
(2) 16 tiene una representación binaria de 10000
(3)
(4) binary(100) = binary(10) << 3
Ten en cuenta que 3 es el exponente en (3) y la cantidad de posiciones que desplazamos los bits hacia la izquierda en (4).
La relación entre el desplazamiento de bits en una cantidad e y la multiplicación por se mantiene en general. Las siguientes operaciones son equivalentes:
// x * 2¹¹² equals x left bitshifted by 112 bits
x * 2 ** 112 == x << 112
// x / 2¹¹² equals x right bitshifted by 112 bits
x / 2 ** 112 == x >> 112
x puede ser un número arbitrario siempre que encaje en el número entero sin signo (unsigned integer).
La biblioteca ABDK convierte enteros sin signo en números de punto fijo (con un denominador implícito de ) utilizando la siguiente función:

La declaración require asegura que x sea menor que type(int64).max, ya que la biblioteca ABDK utiliza números de punto fijo con signo. El desplazamiento hacia la izquierda en 64 es equivalente a multiplicar por .
De manera similar, cuando ABDK realiza una multiplicación, en lugar de dividir el producto de x e y por , realiza un desplazamiento hacia la derecha de 64 bits:

Biblioteca de punto fijo de Uniswap V2
La biblioteca de punto fijo de Uniswap V2 es bastante simple porque las únicas operaciones que hace Uniswap V2 con números de punto fijo son la suma y la división de un número de punto fijo por un número entero.
pragma solidity =0.5.16;
// A library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))
// range: [0, 2**112 - 1]
// resolution: 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // never overflows
}
// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
La función encode() convierte un uint112 a un número de punto fijo almacenado en un uint224. Uniswap V2 utiliza un denominador implícito de 2**112. Podría ser más eficiente en gas si usara un desplazamiento de bits en lugar de una multiplicación (probablemente esto sea un error de los desarrolladores de Uniswap).
El número de punto fijo se almacena en un uint224, que es el doble del tamaño del uint112 con el que interactuará. Durante la operación encode, los bits del número uint112 se desplazan efectivamente a los 112 bits más significativos del uint224.
Esta operación “encode” es más fácil de visualizar con tamaños de uint más pequeños. Usemos un número de punto fijo hipotético con un denominador de . A continuación, mostramos qué sucede cuando codificamos un uint8 a un número de punto fijo con un denominador de :
Comenzando con el número 125, que tiene la representación binaria 01111101, si lo multiplicamos por , el producto es 32000, que cuando se almacena en un uint de 16 bits se representa como 0111110100000000. Ten en cuenta que multiplicar 125 por tiene el mismo efecto que hacer un desplazamiento hacia la izquierda de 8 bits.
La función uqdiv() simplemente realiza la división de un número de punto fijo por un número entero, lo cual no necesita pasos adicionales.
Uniswap usa esta biblioteca para acumular los precios para el oráculo TWAP a continuación. El TWAP agrega el último precio cada vez que ocurre una actualización en un acumulador (el cual se usa para calcular el precio promedio con pasos adicionales, algo fuera del alcance de este artículo). Como los precios se representan como fracciones, los números de punto fijo son una forma ideal de representarlos.
Las variables _reserve0 y _reserve1 contienen los saldos de tokens más recientes del pool y son uint112. price0CumulativeLast y price1CumulativeLast son UQ112x112 (números de punto fijo con un denominador implícito de ). El código a continuación de Uniswap V2 convierte el numerador a un número de punto fijo (UQ112x112) y lo divide por un entero (el denominador no se convierte a un UQ112x112). El resultado es un número de punto fijo.

Redondeo hacia arriba vs redondeo hacia abajo
Las bibliotecas de punto fijo comúnmente tienen la opción de redondear hacia arriba al dividir. Por ejemplo, Solady tiene:
mulWadUp— multiplica dos números de punto fijo, pero al dividir por d, redondea hacia arriba. Recuerda, la fórmula para multiplicar dos números de punto fijo es .mulDivUp— divide dos números de punto fijo, pero redondea hacia arriba al dividir.
La división de Solidity siempre redondea hacia abajo, por ejemplo . Sin embargo, si tuviéramos que redondear hacia arriba, sería igual a 4. Al calcular créditos o precios, uno siempre debe redondear a favor del protocolo y en contra del usuario. Por ejemplo, si estamos calculando cuánto debe pagar un usuario por una cantidad fija de otro activo, deberíamos redondear el precio hacia arriba.
Por ejemplo:
- redondeado hacia abajo es 3.3333
- redondeado hacia arriba es 3.3334 (dependiendo del tamaño de nuestro denominador)
Redondear hacia arriba simplemente significa sumar 1 al resultado si el resto es distinto de cero. Por ejemplo, exactamente, por lo que no deberíamos devolver 4. Sin embargo, y tienen un resto de 1 y 2 respectivamente, por lo que deberíamos sumar 1 al resultado de la división.
Así es como lo hace la biblioteca Solmate:

En el subrayado verde, el código verifica si el módulo es mayor que cero. Si lo es, entonces se suma 1 al resultado (redondeo hacia arriba), de lo contrario se suma 0 (sin redondeo hacia arriba).
Publicado originalmente el 10 de junio