
Aritmética en Solana: prácticas recomendadas para crear aplicaciones financieras
Tabla de contenido
- Usa enteros y unidades menores
- Porcentajes
- Interés compuesto
- Desbordamientos y subdesbordamientos
- ¿Qué es un desbordamiento?
- ¿Qué es un subdesbordamiento?
- Multiplica y luego divide
- Ejemplo de un mercado de predicciones
- Usa una política de redondeo coherente
- Consideraciones de redondeo específicas de Rust
- Redondeo frente a redondeo hacia abajo
- Funciones aritméticas saturadas
- Usa cálculos de intereses sin números de punto flotante
- Conclusión
- Recursos adicionales
Gracias a 0xIchigo y Lostin por editar este artículo y colaborar en él.
Solana es un espacio competitivo. Ya sea que trabajes en un nuevo protocolo de préstamos, agregador, mercado de predicciones, tokenización de RWA o cualquier otra cosa, puede ser tentador apresurarte a lanzar en mainnet.
Sin embargo, es crucial recordar que las aplicaciones blockchain, debido a que administran valor, son inherentemente aplicaciones financieras.
Si nunca has creado aplicaciones financieras, ya sea en una blockchain o en las finanzas tradicionales, debes conocer la importancia de las matemáticas financieras.
Hay muchos recursos excelentes sobre temas de programación específicos de Solana: comprobar qué cuentas deben firmar cada instrucción, cuáles son los programas propietarios de las cuentas que usas, evitar ataques de reapertura de cuentas, etc. Las restricciones de cuentas de Anchor facilitan la implementación de muchas de estas comprobaciones, y Rust ofrece algunos valores predeterminados inteligentes, como detectar desbordamientos y subdesbordamientos de enteros en modo Debug.
Pero la programación financiera segura abarca más que los temas específicos de Solana. Un solo error en la aritmética de tokens puede provocar fugas, inflación no deseada y usuarios molestos. En Solana, donde el volumen de transacciones es mayor que en otras blockchains, las vulnerabilidades pueden explotarse aún más rápido.
Muchas técnicas de programación financiera procedentes de las finanzas tradicionales no tienen nada que ver con blockchain. Por eso no reciben la atención que merecen, aunque siguen siendo fundamentales para proteger los tokens de tus usuarios.
En este artículo, abordaremos específicamente:
- El uso de enteros y unidades menores
- Cómo evitar la pérdida de precisión: multiplicar y luego dividir
- Políticas de redondeo coherentes
- El uso de cálculos de intereses sin números de punto flotante
Usa enteros y unidades menores
La falta de precisión es una vulnerabilidad común en los contratos inteligentes. Cuando las operaciones matemáticas no son precisas, pueden ser vulnerables. Debemos usar enteros y unidades menores para realizar operaciones matemáticas seguras en las aplicaciones de Solana.
Veamos un ejemplo básico.
En las finanzas tradicionales, cada operación que hayas realizado «en USD» se hizo realmente en centavos. Si usaste GBP, se hizo en peniques.
De igual manera, tus transacciones en SOL deben manejarse en lamports y tus transacciones en USDC, en millonésimas de USDC.
Los centavos, los peniques y los lamports son tipos de unidades menores (también llamadas unidades base). La razón para hacer todo en unidades menores es simple: las computadoras no pueden manejar números de punto flotante.
Este es el uno en binario:
| Treinta y dos | Dieciséis | Ocho | Cuatro | Dos | Uno |
| 0 | 0 | 0 | 0 | 0 | 1 |
Este es el nueve en binario:
| Treinta y dos | Dieciséis | Ocho | Cuatro | Dos | Uno |
| 0 | 0 | 1 | 0 | 0 | 1 |
Pero ¿cómo representarías, por ejemplo, 0.3 USDC?
La respuesta correcta es: no puedes.
let answer = 0.1 + 0.2;
msg!("0.1 + 0.2 = {}", answer);Produce este resultado:
El programa registró: "0.1 + 0.2 = 0.30000000000000004"
En su lugar, trata los dólares, GBP, SOL, USDC y cualquier otra «moneda» como una cantidad entera de su unidad menor.
Por ejemplo, para sumar 0.1 y 0.2 con USDC:
let answer_ints: u128 = 100000 + 200000;
msg!("100000 + 200000 = {}", answer_ints);Produce este resultado:
El programa registró: "100000 + 200000 = 300000"
Usar unidades menores evita pérdidas.
Los desarrolladores deben usar los decimales de cada token y recordar que no todos los tokens tienen la misma cantidad de decimales.
Usar enteros para las cantidades de tokens puede parecer obvio, pero también debes considerar que necesitas usar enteros en todas partes, no solo para las cantidades de tokens.
Porcentajes
Los porcentajes deben expresarse como números enteros. La mayoría usa puntos básicos (a veces llamados «bips»), que se representan como bps.
Por ejemplo, 4.74 % equivale a 474 bps.
Interés compuesto
El interés compuesto no debe calcularse con e (el número de Euler), que representa el límite del interés compuesto cuando la frecuencia de capitalización se aproxima al infinito.
Aunque usar e permite una «capitalización continua», que básicamente produce una línea continua para el interés compuesto, e se representa como un número de punto flotante en Rust.
Al usar e, obtendrás errores de redondeo y un resultado distinto al de otros mecanismos.
Mostraremos ambas diferencias más adelante en este artículo.
Desbordamientos y subdesbordamientos
Rust almacena los enteros como variables de tamaño fijo. Esto significa que, según si la variable tiene signo o no, solo puede ocupar cierta cantidad de espacio en memoria.
Por ejemplo, el tipo u8 puede contener cualquier valor entre 0 y 255. Sin embargo, si almacenamos un valor fuera de ese rango, se producirá un desbordamiento o un subdesbordamiento.
¿Qué es un desbordamiento?
Un desbordamiento ocurre cuando el valor supera la capacidad máxima que puede almacenar ese tipo de variable, por lo que vuelve al valor mínimo.
Por ejemplo, si intentáramos almacenar 256 en un u8, el valor volvería a 0. 257 volvería a 1, 258 a 2, 511 a 255 y 512 volvería a 0.
¿Qué es un subdesbordamiento?
Un subdesbordamiento ocurre cuando el valor está por debajo del mínimo posible y vuelve al valor máximo. En un u8, -1 volvería a 255, -2 a 254, y así sucesivamente. En resumen, un subdesbordamiento es como un desbordamiento, pero el comportamiento ocurre en la dirección opuesta.
Aunque Rust incluye varias comprobaciones que hacen que un programa entre en pánico durante la ejecución si ocurre un desbordamiento o subdesbordamiento, no las incluye cuando compilas en modo release. Además, la cadena de herramientas que forma parte integral del entorno de desarrollo de Solana compila los programas de Solana en modo release de forma predeterminada.
Para obtener más información sobre desbordamientos y subdesbordamientos, así como sobre cómo mitigarlos, consulta nuestra guía de seguridad de programas de Solana.
Multiplica y luego divide
A menudo parece natural dividir antes de multiplicar. Tomemos como ejemplo la creación de un mercado de predicciones. Cuando un usuario apuesta por un resultado ganador, debemos calcular el pago correspondiente. En los mercados de predicciones, los ganadores reciben un pago basado en su proporción de las apuestas al resultado ganador. Mentalmente, esto se traduce con claridad en:
fondo ganador ÷ total de apuestas al resultado ganador × cantidad apostada
Esto parece muy natural.
Primero dividimos el fondo ganador en pequeñas partes que representan una «participación» de las ganancias y luego calculamos cuántas participaciones debemos entregar a cada persona.
Pero si multiplicamos primero, obtenemos un resultado intermedio mayor. Esto nos permite reducir el impacto de los errores de redondeo de la división posterior.
fondo ganador × cantidad apostada ÷ total de apuestas al resultado ganador
Ejemplo de un mercado de predicciones
Aquí tienes una demostración rápida. Si hiciéramos este cálculo como personas y no como computadoras, la respuesta correcta sería 10.5.
Intentemos dividir primero:
let answer: u128 = 7 / 2 * 3;
msg!("7 / 2 * 3 = {}", answer);Produce este resultado:
El programa registró: "7 / 2 * 3 = 9"
Al dividir primero, perderemos 1.5 unidades menores.
¿Qué ocurre si multiplicamos primero?
let answer: u128 = 7 * 3 / 2;
msg!("7 * 3 / 2 = {}", answer);Produce este resultado:
El programa registró: "7 * 3 / 2 = 10"
Al multiplicar primero, la pérdida por redondeo es de solo 0.5 unidades menores.
Este concepto puede ampliarse para indicar que debes «ejecutar primero cualquier operación que aumente el orden de magnitud».
Por ejemplo, si calculas una expresión más compleja que usa potencias o raíces, como al calcular riesgos, aplica primero las potencias y después las raíces. Esto se debe a que, con la aritmética de punto fijo, dividir primero puede causar una pérdida de precisión si el cociente se redondea hacia abajo antes de multiplicarlo.
Usa una política de redondeo coherente
Una política de redondeo incoherente que genere pequeñas diferencias de un solo token puede hacer que parezca que los tokens se pierden o se crean de la nada. Con el tiempo, esto puede agotar la liquidez de un programa o generar una inflación no deseada de tokens.
Como señaló Will Thieme de Orca en su charla de Breakpoint sobre cómo evitar errores comunes en los programas de Solana, obtener un token no parece gran cosa.
Sin embargo, Solana tiene transacciones reales, en el sentido de las finanzas tradicionales, que incluyen varias instrucciones y tienen comisiones bajas. Por eso, es posible llenar una transacción con muchas instrucciones individuales que aprovechen errores de desfase por uno y ejecutarla a un costo muy bajo.
Los errores de redondeo son inevitables, pero pueden controlarse con una política de redondeo coherente. Decide de antemano si redondearás hacia arriba o hacia abajo y cómo manejarás los valores «a la mitad» (es decir, 0.5). El redondeo hacia arriba a partir de la mitad es más común y algunas normas financieras lo exigen. Lo más importante es aplicar esa política de manera coherente en todo tu código.
Consideraciones de redondeo específicas de Rust
Los desarrolladores deben conocer varias funciones específicas de Rust que pueden ser problemáticas, ya que las operaciones de redondeo son una causa común de pérdida de precisión. La elección del método de redondeo puede afectar de forma significativa la precisión y el comportamiento de tu programa.
Redondeo frente a redondeo hacia abajo
Por ejemplo, la función try_round_u64() redondea al número entero más cercano. Si quisiéramos crear un programa que convierta garantías en liquidez, redondear hacia arriba podría generar más tokens de liquidez de los que justifican las garantías proporcionadas. En su lugar, debemos usar la función try_floor_u64() para redondear hacia abajo al número entero más cercano.
Funciones aritméticas saturadas
Además, los desarrolladores suelen usar funciones aritméticas saturating_* (por ejemplo, saturating_add) para limitar los valores a sus respectivos máximos y mínimos, y así evitar desbordamientos y subdesbordamientos. Sin embargo, estas funciones pueden provocar pérdidas sutiles de precisión.
Por ejemplo, si tu función multiplica el importe de una transacción por un multiplicador de recompensa y el producto supera el valor máximo del tipo de variable, recompensarás al usuario con menos de lo debido.
Es fundamental tener esto presente, especialmente porque tu programa debe usar aritmética de punto fijo.
Usa cálculos de intereses sin números de punto flotante
Una técnica común para calcular el interés compuesto consiste en usar el número de Euler, e, pero e es un número de punto flotante. Evita usar números de punto flotante para calcular intereses. En su lugar, usa aritmética de punto fijo.
La biblioteca spl-math de Solana incluye PreciseNumber, que, como puedes imaginar, funciona en el entorno más limitado de Rust de Solana y puede representar fracciones decimales minúsculas, de hasta 12 decimales, sin perder precisión.
use spl_math::precise_number::PreciseNumber;
fn calculate_compound_interest(
principal: u128,
rate_basis_points: u128,
time: u128,
compounds_per_year: u128,
) -> u128 {
// Formula: result = principal * (1 + rate/compounds_per_year)^(compounds_per_year * time)
// Where rate_basis_points is expressed in basis points (500 for 5%)
// Convert principal to PreciseNumber
let principal = PreciseNumber::new(principal).unwrap();
// Convert basis points to decimal percentage (divide by 10000)
let rate = PreciseNumber::new(rate_basis_points)
.unwrap()
.checked_div(&PreciseNumber::new(10_000).unwrap())
.unwrap();
// Calculate rate/compounds_per_year
let rate_per_period = rate
.checked_div(&PreciseNumber::new(compounds_per_year).unwrap())
.unwrap();
// Calculate 'base', which is 1 + rate/compounds_per_year
let one = PreciseNumber::new(1).unwrap();
let base = rate_per_period.checked_add(&one).unwrap();
// Calculate 'total_periods', which is compounds_per_year * time
let total_periods = compounds_per_year.checked_mul(time).unwrap();
// Calculate compound_factor, which is (1 + rate/compounds_per_year)^(compounds_per_year * time)
let compound_factor = base.checked_pow(total_periods).unwrap();
// Calculate result = principal * compound_factor
principal
.checked_mul(&compound_factor)
.unwrap()
.to_imprecise()
.unwrap()
}
Puedes ver las diferencias al ejecutar el código:
- El programa registró: "Uso de spl-math
PreciseNumber" - El programa registró: "Inversión de $1000 al 5% durante 5 años, con capitalización 1 vez por año:"
- El programa registró: "Importe final: $1276"
En comparación, al usar e:
- El programa registró: " Uso de e (no hagas esto)"
- El programa registró: "Inversión de $1000 al 5% durante 5 años, con capitalización 1 vez por año:"
- El programa registró: "Importe final: $1284"
Conclusión
La aritmética de tokens es un tema aburrido, pero no prestarle atención puede generar el tipo de emoción que no quieres. Tus aplicaciones on-chain deben manejar los tokens con la precisión y seguridad que exigen los usuarios.
En este artículo, explicamos cómo usar enteros y unidades menores, multiplicar, evitar la pérdida de precisión, mantener una política de redondeo coherente y calcular intereses sin números de punto flotante.
Después de aprender estos fundamentos de aritmética, pide a otra persona, específicamente a una empresa de auditoría especializada en Solana, que revise tu código antes de lanzarlo en mainnet.
Recursos adicionales
Artículos relacionados
Suscríbete a Helius
Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos


