Cómo usar decimales en Solidity

Emilio S.
coinosis
6 min readApr 27, 2020

--

Cuando estaba escribiendo el contrato inteligente de coinosis, me encontré en la documentación de Solidity algo que me sorprendió:

Fixed point numbers are not fully supported by Solidity yet. They can be declared, but cannot be assigned to or from.
Los números de punto fijo todavía no están totalmente implementados en Solidity. Se pueden declarar, pero no pueden asignar o ser asignados.

Lo que me sorprendió no es tanto que falte un guión entre fixed y point (casi nadie conoce las reglas de los guiones en inglés) sino el hecho de que en Solidity sólo se trabaja con números enteros. Porque si los números fraccionarios sólo se pueden declarar pero no asignar, pues entonces no se puede hacer casi nada con ellos. Y me sorprendió porque lo que yo quería hacer era muy básico: almacenar un valor en dólares. ¿Cómo hago para almacenar los centavos de dólar?

Lo primero que se me ocurrió fue utilizar dos variables enteras: una para los dólares y otra para los centavos. Pero en seguida caí en cuenta de una cosa: ¿cómo es posible que no haya decimales en Solidity si los valores de ether nunca son enteros? Y es ahí cuando me acordé de que en realidad sí son enteros porque la unidad de valor en Solidity no es el ether: es el wei.

Un wei es una 10¹⁸ava fracción de ether, y todos los valores se tienen que transformar a esta unidad cuando uno está interactuando con un contrato inteligente. Así, por ejemplo, 1.5 ETH = 1.5 × 10¹⁸ wei = 1500000000000000000 wei. Ésa es la manera como Solidity, y la Ethereum Virtual Machine (EVM) en general, lidia con cantidades monetarias.

Las cantidades monetarias tienen una particularidad, y es que tienen que ser exactas. Esto es un poco problemático para la manera como usualmente los lenguajes de programación almacenan los números fraccionarios, porque los tienden a almacenar como una fracción: un número dividido por otro. El problema es que las fracciones no son siempre exactas, en particular si intentamos almacenar números irracionales. La alternativa son las variables de coma fija, que usan dos números enteros para almacenar ambos lados del punto: ésos son los números de punto fijo que todavía no están “totalmente” implementados en Solidity.

Resulta que la Ethereum Virtual Machine, de la que Solidity sólo es un intermediario, siguió el ejemplo de Bitcoin en la manera como usa las fracciones, y en lugar de enredarse con números fraccionarios — que en la práctica igual son números enteros — y el costo computacional adicional que esto conlleva, decidió usar una fracción de su moneda como la unidad de cálculo. Sin embargo, el satoshi es sólo la 10⁸ ava parte de un bitcoin, lo cual puede que se vuelva problemático si el bitcoin se valoriza mucho. En este momento, un satoshi vale un tercio de peso colombiano, pero ¿qué vamos a hacer cuando un satoshi valga 3 mil pesos?

La solución

Luego de pensarlo un poco, opté por imitar para los dólares el procedimiento que tiene Solidity con el ether: en lugar de almacenar el valor en dólares, decidí almacenarlo en fracciones de dólar.

La signatura de una de mis funciones

Pero, ¿en cuáles fracciones? Lo lógico sería usar centavos de dólar. Así, 4.23 USD = 423 ¢. Sin embargo, opté por usar una unidad que me inventé, el USDWei. Un USDWei es la 10¹⁸ava parte de un dólar. Me parece práctico usar USDWei porque esto permite utilizar las funciones de conversión que ya existen para el ether cuando interactuamos con el contrato inteligente usando la librería web3.js:

La función toWei() permite convertir un valor de ether a wei

Puede que utilizar tantos ceros innecesariamente tenga consecuencias en el desempeño (que igual no creo porque la EVM está diseñada para manejar números grandes), pero la usabilidad de tener una unidad de referencia tiene más peso para mí que esta dudosa optimización.

Las divisiones enteras

Mi objetivo era obtener una cantidad de ether a partir de una cantidad en dólares y la tasa de cambio. Entonces uno pensaría que habría que dividir la cantidad en dólares por la tasa del ether en dólares:

Pero no. Resulta que si uno divide un número grande por otro número grande, el resultado es un número pequeño… y volvemos al problema de que no podemos usar fracciones. En realidad eso que hice ahí fue una bobada porque es lo mismo que hacer

Y como el 10¹⁸ de arriba se cancela con el de abajo, es como si no hubiera hecho nada. Así que, después de pensarlo un poco, caí en cuenta de que el número de arriba tiene que ser mucho más grande que el de abajo para que no se pierda precisión:

En Solidity existe una constante llamada ether que equivale a, ya lo adivinaste, 10¹⁸. Así que añadiendo esa constante en el numerador obtenemos un resultado en wei y no perdemos precisión. Pero esto conlleva un problema que puede llegar a ser muy serio.

Cuidado con los overflows

Una de las primeras cosas que uno aprende cuando empieza a trabajar con la EVM es que es un lenguaje de bajo nivel. Es como si fuera lenguaje de máquina; puras instrucciones de máquina, u OPCODES, que emulan la manera como funciona un procesador. Y Solidity no es más que una fina capa encima de eso. Así que las cosas raras que pasan en lenguajes como C y que para la gente de mi generación tienden a no ser más que cuentos de horror, también pasan en Solidity.

Una de ésas es el overflow. Permíteme explicarte qué es: las variables numéricas (en ningún lenguaje, de hecho) no pueden almacenar números infinitamente grandes; tienen un máximo. Llamemos a ese máximo MAX. Pues bien, ¿qué pasa si hago MAX + 1? En un lenguaje de alto nivel, como JavaScript, seguramente voy a recibir un error; pero ¿en Solidity? no, para nada: MAX + 1 es cero.

Esto se puede utilizar para hackear contratos inteligentes. Y como esta manera de hacer divisiones enteras implica multiplicar dos números absurdamente grandes, la posibilidad de un overflow de repente resulta muy real. Pero la cosa no es tan grave. Existe una librería que se llama SafeMath y que se encarga de protegernos de los overflows. Así se ve mi código después de incluir SafeMath en mi contrato:

Y con esto termina mi peripecia con los decimales en Solidity. Espero que te haya gustado y hayas aprendido algo. Avísame si conoces una mejor manera de hacer las cosas. Y sobre todo, ayúdame a difundir el uso del USDWei como la nueva moneda de referencia internacional.

--

--

Emilio S.
coinosis

“Don’t ask the chicken about the chicken soup” L. von Trier