Errores Aritméticos en Solidity: Cómo Evitarlos

Autor del artículo

Errores aritméticos en contratos inteligentes Solidity ocurren cuando los cálculos de tokens, participaciones o precios usan la fórmula incorrecta, escala decimal errónea, dirección de redondeo inapropiada o asunciones numéricas equivocadas, lo que permite a los usuarios acuñar reclamos en exceso, retirar más valor del que depositaron o causar fallos en invariantes contables. La solución es definir unidades explícitamente, usar aritmética acotada con precisión completa y probar los invariantes económicos que la fórmula debe preservar.

Las consecuencias son materiales. Cetus CLMM perdió $223.0M en Sui el 2025-05-22 en un incidente clasificado como Error Aritmético que involucró la contabilidad de tokens y participaciones. Por lo tanto, la seguridad aritmética no solo consiste en evitar revertimientos por desbordamiento en Solidity, sino en asegurar que cada participación acuñada represente un reclamo defendible sobre activos.

¿Qué son los errores aritméticos en Solidity y cómo funcionan los ataques?

Los errores aritméticos en Solidity son defectos en cálculos que traducen depósitos, retiros, saldos, precios, comisiones o liquidez en reclamos económicos. Un atacante explota la discrepancia entre el modelo contable previsto y la fórmula implementada, a menudo escogiendo el orden de transacciones, cantidades de activos o estados del pool que hacen que una conversión incorrecta les beneficie.

En Solidity moderno, el desbordamiento y subdesbordamiento aritmético generan revertimientos por defecto. Esa protección es útil, pero no asegura que una fórmula sea económicamente correcta. Un cálculo puede mantenerse dentro de los límites de uint256 mientras acuña demasiadas participaciones del vault, cobra poco colateral o redondea un retiro a favor del usuario.

Un ataque típico de exceso de participaciones sigue esta secuencia:

  1. Un vault tiene activos y participaciones existentes, por lo que cada participación tiene un precio distinto de uno.
  2. El vault recibe rendimiento, comisiones, donaciones o un aumento rebasing en el saldo.
  3. El precio de la participación sube porque los activos totales aumentaron mientras las participaciones totales no.
  4. Un atacante deposita activos a través de una fórmula que asume que una participación siempre equivale a un activo.
  5. El vault acuña más participaciones de las que debería comprar el depósito al precio actual.
  6. El atacante canjea esas participaciones por una fracción desproporcionada de los activos agrupados.

El invariante central es simple: después de un depósito, la fracción de propiedad del nuevo usuario debe corresponder al valor que aportó. Si un depósito equivalente al 1% de los activos del pool produce más del 1% de las participaciones en circulación, los poseedores existentes se diluyen.

¿Qué patrones en Solidity causan errores en la contabilidad de tokens y participaciones?

Los errores en la contabilidad de tokens y participaciones suelen surgir al mezclar unidades, truncar valores intermedios o tratar un precio de participación variable como fijo. Las funciones con mayor riesgo son deposit, mint, withdraw, redeem, límites de préstamo, liquidación de comisiones, cálculos de liquidez y distribución de recompensas.

Un patrón peligroso es acuñar participaciones directamente a partir de unidades de activos depositados:

shares = assets * 1e18 / assetScale;

Esa conversión puede ser adecuada solo para el primer depósito, cuando una participación se define deliberadamente como una unidad normalizada de activo. Una vez que un vault gana rendimiento o sufre pérdida, los depósitos deben usar la tasa de cambio actual:

shares = assets * totalShares / totalAssets;

Incluso esa expresión requiere un tratamiento cuidadoso. Multiplicar primero puede causar desbordamiento si se hace dentro de un bloque unchecked, mientras dividir primero puede resultar en pérdida de precisión. La normalización decimal crea otra fuente de error. Activos estilo USDC comúnmente usan menos decimales que un token participación con 18 decimales, y un protocolo debe distinguir sistemáticamente unidades crudas de tokens de unidades contables normalizadas.

Causas comunes incluyen:

  • Codificar 1e18 para un activo con diferente precisión decimal.
  • Acuñar participaciones a tasa fija uno a uno después de que el precio de la participación cambia.
  • Dividir antes de multiplicar y perder precisión significativa sin intención.
  • Redondear depósitos o retiros hacia arriba, generando fugas acumulativas de valor.
  • Usar unchecked alrededor de valores controlados por usuarios o factores de escala.
  • Suponer que los saldos de tokens no cambian fuera de flujos normales de depósitos y retiros.
  • Olvidar que tokens con comisiones por transferencia, rebasing o generadores de rendimiento pueden invalidar contabilidad almacenada.

¿Cómo corregir los errores aritméticos en cálculos de participaciones en Solidity?

Para corregir errores aritméticos en Solidity, normalice las unidades de activo una vez, calcule participaciones según la proporción actual del pool, use multiplicación y división con precisión completa, y elija un redondeo que no transfiera valor de poseedores existentes a un nuevo depositante. El contrato también debe tratar su saldo de activos y su suministro de participaciones como un estado contable ligado.

El vault vulnerable siguiente acepta un token con decimales configurables. Acuña participaciones normalizadas a una tasa fija incluso después que reportYield incrementa activos. Un depositante puede recibir demasiadas participaciones tras el rendimiento acumulado y luego canjearlas contra un pool mayor.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract VulnerableShareVault {
    IERC20 public immutable asset;
    uint8 public immutable assetDecimals;
    uint256 public totalAssets;
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    constructor(address asset_, uint8 decimals_) {
        asset = IERC20(asset_);
        assetDecimals = decimals_;
    }

    function deposit(uint256 assets) external {
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");

        uint256 scale = 10 ** assetDecimals;
        // VULNERABLE: ignora el precio actual de las participaciones existentes.
        uint256 minted = (assets * 1e18) / scale;

        totalAssets += assets;
        totalShares += minted;
        shares[msg.sender] += minted;
    }

    function reportYield(uint256 assets) external {
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");
        totalAssets += assets;
    }

    function withdraw(uint256 shareAmount) external {
        uint256 assets = (shareAmount * totalAssets) / totalShares;
        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;
        totalAssets -= assets;
        require(asset.transfer(msg.sender, assets), "transfer failed");
    }
}

La versión corregida convierte unidades crudas del activo a unidades contables de 18 decimales y acuña participaciones a la tasa de cambio actual. Usa Math.mulDiv de OpenZeppelin con precisión completa, redondea hacia abajo depósitos y retiros, y limita los valores decimales admitidos.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract FixedShareVault {
    IERC20 public immutable asset;
    uint256 public immutable assetScale;
    uint256 public totalAssets;
    uint256 public totalShares;
    mapping(address => uint256) public shares;

    constructor(address asset_, uint8 decimals_) {
        require(decimals_ <= 18, "unsupported decimals");
        asset = IERC20(asset_);
        assetScale = 10 ** decimals_;
    }

    function deposit(uint256 assets) external {
        require(assets > 0, "zero assets");
        require(asset.transferFrom(msg.sender, address(this), assets), "transfer failed");

        uint256 normalizedAssets = Math.mulDiv(assets, 1e18, assetScale);
        uint256 normalizedPool = Math.mulDiv(totalAssets, 1e18, assetScale);

        // CORRECCIÓN: valora las participaciones según la proporción del pool antes del depósito.
        uint256 minted = totalShares == 0
            ? normalizedAssets
            : Math.mulDiv(normalizedAssets, totalShares, normalizedPool);

        require(minted > 0, "deposit too small");
        totalAssets += assets;
        totalShares += minted;
        shares[msg.sender] += minted;
    }

    function withdraw(uint256 shareAmount) external {
        uint256 assets = Math.mulDiv(shareAmount, totalAssets, totalShares);
        shares[msg.sender] -= shareAmount;
        totalShares -= shareAmount;
        totalAssets -= assets;
        require(asset.transfer(msg.sender, assets), "transfer failed");
    }
}

Si su protocolo valora depósitos, retiros, colateral o liquidez mediante aritmética personalizada, una auditoría de contrato inteligente y evaluación de seguridad puede trazar conversiones de unidades e invariantes económicos en todo el ciclo de vida de la transacción.

¿Cómo pueden los desarrolladores evitar errores aritméticos?

Los desarrolladores evitan errores aritméticos especificando unidades contables antes de implementar fórmulas, aplicando un redondeo conservador y tratando cada conversión como un límite de seguridad. Una fórmula debe especificar qué representa cada entrada, su precisión decimal, su rango permitido y quién se beneficia del redondeo.

Técnica Qué detiene Costo en gas o complejidad Limitación
Normalización explícita de unidades Mezcla de unidades crudas de token con participaciones de 18 decimales Multiplicación o división extra Requiere una política decimal documentada
Math.mulDiv Desbordamiento y pérdida de precisión en a * b / c Más gas que operadores simples No valida la fórmula económica
Redondeo hacia abajo en depósitos y retiros Acuñación excesiva y redención excesiva por redondeo Puede crear balances residuales (“dust”) Los balances residuales necesitan política de recuperación
Umbral mínimo para acuñar participaciones Depósitos que redondean a cero participaciones Una comparación No debe bloquear usuarios pequeños legítimos inesperadamente
Entrada acotada y decimales Exponenciación insegura e inputs extremos Chequeos de validación Los límites deben coincidir con activos admitidos
Pruebas de invariantes Transiciones de estado que violan contabilidad del pool Esfuerzo de escritura de test Los invariantes deben cubrir secuencias adversarias

Use redondeo hacia abajo donde la alternativa emitiría valor que el pool no puede soportar. Por ejemplo, un vault normalmente redondea hacia abajo las participaciones depositadas y los activos retirados. Si un producto requiere redondeo favorable al usuario, debe financiar explícitamente el subsidio resultante en lugar de imponerlo silenciosamente a otros depositantes.

Perspectiva de seguridad: Según la metodología Soken, la revisión aritmética comienza con unidades y leyes de conservación, no con operadores. Una multiplicación verificada aún puede ser explotable si el resultado representa un activo, participación, precio o reclamo de propiedad incorrecto.

¿Qué incidentes reales muestran el impacto de errores aritméticos?

Los incidentes de Error Aritmético demuestran que la contabilidad de tokens y participaciones es una propiedad de seguridad de todo el protocolo, no un problema local de calidad de código. La lógica afectada puede estar en reservas AMM, recompensas de préstamos, participaciones de vaults, cálculos de liquidez o rutinas de conversión de activos.

Protocolo Fecha Monto perdido Cadena
Cetus CLMM 2025-05-22 $223.0M Sui
Compound V2 2021-09-29 $147.0M Ethereum
Uranium Finance 2021-04-28 $57.2M BSC
ValueDefi 2021-05-07 $11.0M BSC
KiiChain 2026-08-22 $9.7M KiiChain
Truebit 2026-01-08 $26.4M Ethereum
Notional V2 2026-09-04 $1.7M Ethereum
USM 2026-08-09 $136K Ethereum

Estos incidentes comparten una característica crítica: un cálculo gobernaba la distribución o valoración del valor on-chain. La falla no fue necesariamente un simple uint256 sin chequeo. En la contabilidad de tokens y participaciones, la condición vulnerable suele ser una relación incorrecta entre reservas, suministro, precio y reclamos de usuario.

DefiLlama registra 35 incidentes de Error Aritmético en contratos inteligentes desde 2020 hasta 2026, con pérdidas combinadas de $561.0M. Para equipos de protocolo, esa historia justifica revisar rutas contables con la misma prioridad que control de acceso y manejo de llamadas externas.

¿Cómo detectar errores aritméticos antes del despliegue?

Los errores aritméticos se detectan mejor revisando transiciones de estado y probando propiedades económicas a lo largo de muchas secuencias, no solo verificando fórmulas aisladas manualmente. El análisis estático identifica operadores riesgosos y bloques unchecked, mientras que fuzzing y pruebas de invariantes establecen si el modelo contable se sostiene ante depósitos, retiros, rendimiento, pérdidas y valores decimales inusuales.

Use esta lista de verificación focalizada:

  1. Liste cada variable de activo, participación, precio, tasa y comisión con su unidad y decimales.
  2. Verifique que cada depósito use la tasa de cambio previa al depósito.
  3. Verifique que cada retiro use una tasa consistente con las participaciones quemadas.
  4. Busque *, /, unchecked y constantes de escala codificadas directamente en las rutas contables.
  5. Pruebe activos con distintas precisiones decimales y depósitos cercanos a tamaños mínimos.
  6. Confirme que el redondeo no acuñe participaciones no soportadas ni libere activos no soportados.
  7. Modele cambios de saldo por rebasing, donaciones, comisiones y rendimiento antes de cada acción del usuario.
  8. Verifique que los reclamos totales de usuarios no puedan exceder los activos disponibles para redención.

Slither puede señalar patrones aritméticos peligrosos, bloques sin verificación y operaciones sospechosas de precisión, pero no puede determinar si una fórmula de precio de participaciones es económicamente sólida. El fuzzing con Foundry es más efectivo combinado con afirmaciones como que totalAssets cubra todos los reclamos redimidos y que un depositante no pueda obtener ganancia de un ciclo inmediato de depósito-retiro sin rendimiento legítimo.

Las pruebas de invariantes deben generar secuencias multiusuario: depósito, reporte de rendimiento, retiro, retiro parcial, donación y repetidos depósitos pequeños. La práctica de revisión Soken también compara el modelo matemático previsto con cada actualización de estado, porque una fórmula correcta aplicada antes y no después de un cambio de estado puede aún malvaluar participaciones.

La seguridad aritmética es la disciplina de preservar la verdad económica en cada conversión. Un vault puede usar la aritmética con chequeos de Solidity 0.8 y seguir siendo vulnerable si acuña reclamos al precio equivocado, asume decimales erróneos o redondea valor hacia fuera.

Comience escribiendo la unidad y el invariante para cada variable contable, luego codifique esos invariantes en tests con Foundry antes de añadir nuevos tipos de activos o rutas de valoración. Para investigación más profunda sobre patrones de seguridad en Solidity, visite el Soken Hub.

Autor del artículo

Preguntas frecuentes

¿Qué son los errores aritméticos en los smart contracts de Solidity?

Los errores aritméticos en Solidity son fallas en cálculos de depósitos, retiros, balances, precios, tarifas, liquidez, tokens o participaciones. Pueden causar reclamos excesivos, retiros mayores o fallas contables si las fórmulas, escalas decimales, redondeos o suposiciones numéricas son incorrectas.

¿Cómo permiten los errores aritméticos que usuarios retiren demasiado?

Los errores aritméticos permiten que usuarios creen reclamos excesivos o retiren más valor del depositado cuando cálculos de tokens, participaciones o precios usan fórmulas incorrectas, escalas decimales erróneas, redondeos mal aplicados o suposiciones numéricas falsas. Así, se asigna un reclamo económico no respaldado por los activos.

¿Cómo prevenir errores aritméticos en Solidity?

Para prevenir errores aritméticos en Solidity se debe definir explícitamente las unidades de cada cálculo, usar aritmética acotada de precisión completa y probar invariantes económicos que la fórmula debe mantener. Esto corrige errores en fórmulas, escalas decimales, redondeos y suposiciones que distorsionan reclamos sobre tokens, participaciones o precios.

¿Son suficientes los reverts por overflow para evitar errores aritméticos en Solidity?

Los reverts por overflow no eliminan todos los errores aritméticos. La seguridad aritmética también necesita fórmulas correctas, unidades explícitas, escalas decimales adecuadas, dirección correcta de redondeo y suposiciones numéricas sólidas. Un cálculo puede evitar overflow y aun así asignar reclamos indefendibles.

¿Qué enseña la pérdida de Cetus CLMM sobre errores aritméticos?

La pérdida de $223M en Cetus CLMM en Sui fue un Error Aritmético en contabilidad de tokens y participaciones. Demuestra que la seguridad aritmética va más allá de prevenir overflow; cada participación emitida debe representar un reclamo válido sobre los activos subyacentes.

Chat