Saltar al contenido
0%

12 mejores prácticas de seguridad para smart contracts en Solidity

Usman Asim headshot

Escrito por Usman Asim

Publicado el 13 de noviembre de 202515 min de lectura

Un escudo abstracto que representa la seguridad del código

Los smart contracts son los magos detrás de la magia de blockchain. Son los bloques de código que automatizan la lógica onchain, especificando desde los umbrales de liquidación de los protocolos de préstamos descentralizados hasta la tokenización de activos del mundo real, y mucho más.

Pero un gran poder conlleva una gran responsabilidad, especialmente cuando se trata de seguridad. Un bug puede llevar a exploits masivos, y nadie quiere que su proyecto sea el próximo titular de un hackeo multimillonario.

Solo en la primera mitad de 2025, se perdieron más de $2.300 millones en criptomonedas por exploits y brechas de seguridad, y los problemas de control de acceso representaron por sí solos más de $1.600 millones de esa cifra. Si estás construyendo con Solidity en Ethereum o cadenas similares, dominar la seguridad no es opcional: es lo que mantiene a salvo los fondos de tus usuarios y tu reputación intacta.

En este post, desglosaremos qué significa realmente la seguridad de smart contracts, profundizaremos en algunas de las vulnerabilidades más grandes de los contratos, y compartiremos prácticas recomendadas con ejemplos de código para ayudarte a blindar tu código y desplegar contratos seguros en mainnet.

Y recuerda, siempre prueba primero en testnets antes de desplegar en mainnet. Es mucho mejor encontrar vulnerabilidades antes de que tus usuarios empiecen a enviar fondos a tu contrato.

Qué es la seguridad de smart contracts

Los smart contracts son piezas de código autoejecutables que corren cuando se cumplen ciertas condiciones. Piensa en acuerdos automatizados que manejan transferencias, votaciones, o incluso maniobras complejas de DeFi sin intermediarios. Se despliegan como bytecode en la blockchain, y una vez en vivo, son inmutables, así que cualquier falla en un contrato queda a la vista del público.

La seguridad aquí se reduce a asegurarse de que el código sea sólido, porque muchas veces los propios contratos custodian activos onchain. ¿Pueden los hackers drenar fondos de un contrato en particular? ¿La lógica del contrato se mantiene ante casos extremos raros? ¿Los activos están protegidos de accesos no autorizados?

Una vulnerabilidad en esas respuestas significa que un actor malicioso podría drenar el contrato de los fondos de los usuarios y quedarse con ese dinero. Para proteger esas vulnerabilidades, necesitas pensar en implementar validaciones de entrada, patrones de diseño y auditorías de código que puedan endurecer tu código antes del despliegue en mainnet.

Para una introducción más profunda, revisa la documentación de Ethereum sobre seguridad de smart contracts.

Por qué la seguridad te importa

Construir contratos inseguros no solo es riesgoso para los usuarios, puede hundir tu proyecto de la noche a la mañana. Cuando un proyecto es drenado, los usuarios pierden la confianza, y muchas veces no vuelven. La irreversibilidad de blockchain significa que una vez que los fondos se van, se van. No hay contracargos aquí. Y así como los fondos pueden desaparecer en un instante, también tu reputación.

Con miles de millones de dólares fluyendo onchain, los hackers siempre están buscando puntos débiles; las estadísticas recientes muestran lo que está en juego: esos $2.300 millones perdidos en el primer semestre de 2025. La mayoría de esas pérdidas provinieron de bugs prevenibles como malos controles de acceso y reentrancy. Al priorizar la seguridad, puedes proteger a tus usuarios, generar confianza, y evitar la pesadilla de que tu proyecto llegue a $0 en una sola transacción.

Con las herramientas y prácticas recomendadas disponibles en el mercado, hay muchas cosas que puedes hacer para escribir código mejor, no solo funcional, sino resiliente.

Entendiendo las vulnerabilidades de smart contracts

Un recurso útil para entender las vulnerabilidades de smart contracts es OWASP (por sus siglas en inglés, Open Web Application Security Project), una organización sin fines de lucro que crea recursos gratuitos para mejorar la seguridad de software a nivel mundial. Su Smart Contract Top 10 es como una lista de las vulnerabilidades más peligrosas en aplicaciones blockchain, basada en exploits del mundo real y datos de incidentes que causaron pérdidas de miles de millones.

Esta lista le da a los desarrolladores un listado priorizado de vulnerabilidades en las que enfocarse durante auditorías y diseño de contratos, basándose en patrones de hackeos en distintos ecosistemas. La edición 2025 destaca riesgos como fallas de acceso que llevaron a $953 millones en pérdidas solo el año pasado. Al revisar tu código, tener presentes estas vulnerabilidades comunes es un excelente marco para empezar a construir la resiliencia de tu código.

  1. Vulnerabilidades de control de acceso: Fallas que permiten a personas no autorizadas manipular datos o funciones, a menudo por falta de validaciones de permisos. Este problema encabeza la lista por una razón. Piensa en claves de administrador robadas que drenan contratos. Simple, pero efectivo y prevalente.
  2. Manipulación de oráculos de precios: Los hackers pueden manipular fuentes de datos externas (oráculos) para distorsionar precios, lo que lleva a préstamos o intercambios malos. Esto es común en DeFi donde los precios precisos son cruciales.
  3. Errores de lógica: Bugs donde el contrato hace algo no intencionado, como acuñar tokens extra o calcular mal las recompensas. Una vulnerabilidad donde tienes que cubrir cada caso extremo. Requiere una auditoría profunda de la lógica del contrato.
  4. Falta de validación de entradas: No validar las entradas del usuario puede permitir que datos basura rompan la lógica o exploten desbordamientos.
  5. Ataques de reentrancy: Las llamadas externas pueden permitir que hackers vuelvan a entrar a funciones en medio de la ejecución, a menudo para retirar fondos múltiples veces.
  6. Llamadas externas sin verificar: No manejar llamadas fallidas puede hacer que el contrato continúe con supuestos incorrectos.
  7. Ataques de flash loans: Abusar de préstamos instantáneos puede manipular mercados o protocolos en una sola transacción.
  8. Desbordamiento y subdesbordamiento de enteros: Errores matemáticos cuando los números sobrepasan sus límites pueden llevar a cálculos incorrectos o robo.
  9. Aleatoriedad insegura: Un RNG malo y predecible puede ser explotado en juegos o loterías.
  10. Ataques de denegación de servicio (DoS): Sobrecargar contratos con operaciones que consumen mucho gas para volverlos inutilizables.

Para los detalles completos de OWASP, dirígete a su página del Smart Contract Top 10. Y para consejos específicos de Ethereum, revisa sus lineamientos de seguridad.

12 prácticas recomendadas de seguridad para smart contracts

Ahora que entiendes el panorama de amenazas, veamos defensas prácticas. Cada vulnerabilidad en la lista de OWASP tiene prácticas recomendadas correspondientes que han sido probadas en miles de contratos.

Las siguientes secciones desglosan estas fallas comunes una por una con ejemplos concretos de código que muestran tanto patrones vulnerables como implementaciones seguras. Piensa en esto como tu manual defensivo: técnicas sencillas que, aplicadas de forma consistente, pueden reducir tu superficie de ataque.

1. Usa delegatecall con cuidado

Delegatecall permite que un contrato ejecute código de otro mientras usa su propio almacenamiento – muy útil para librerías o actualizaciones, pero riesgoso porque puede llevar a cambios de estado inesperados si el código llamado altera tus variables. Está detrás de exploits importantes donde los hackers inyectan lógica maliciosa que sobrescribe estado crítico como direcciones de propietario o drena fondos.

El peligro es que delegatecall preserva el contexto del contrato que llama (msg.sender, msg.value, almacenamiento), así que el código externo se ejecuta con privilegios completos. Úsalo solo cuando sea absolutamente necesario, y asegúrate de que los diseños de almacenamiento coincidan perfectamente entre contratos: slots desalineados pueden corromper tus datos.

Aquí un ejemplo vulnerable:

solidity
Copied
contract LibraryContract { address public owner; // Slot 0 function updateOwner() public { owner = msg.sender; // Changes caller's storage slot 0! } } contract VulnerableProxy { uint256 public value; // Slot 0 - MISMATCH! address public owner; // Slot 1 function delegateCall(address lib) public { // DANGER: updateOwner() will overwrite 'value', not 'owner'! lib.delegatecall(abi.encodeWithSignature("updateOwner()")); } }

Aquí un enfoque más seguro:

solidity
Copied
contract SecureProxy { address public implementation; address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function execute(bytes memory data) public onlyOwner { (bool success,) = implementation.delegatecall(data); require(success, "Execution failed"); } }

Prácticas recomendadas: Usa patrones de proxy establecidos como los contratos actualizables de OpenZeppelin, mantén diseños de almacenamiento idénticos entre versiones (nunca reordenes ni cambies tipos de variables), restringe delegatecall solo a contratos confiables y auditados, y considera usar librerías con la palabra clave library que usa delegatecall de forma segura por debajo, y nunca permitas direcciones controladas por usuarios en delegatecall: eso es un vector instantáneo de apropiación.

2. Usa un reentrancy guard

Reentrancy ocurre cuando una llamada externa cede el control antes de que tu función complete sus actualizaciones de estado, permitiendo que quien llama vuelva a entrar y repita acciones como retiros antes de que los balances se actualicen. Está en el puesto #5 de los principales riesgos de Web3 según OWASP y estuvo detrás de algunos de los hackeos más grandes en cripto como el infame hackeo de The DAO (pérdida de $60M).

Los atacantes pueden explotar esta ventana de reentrancy entre el envío de fondos y la actualización del estado para drenar contratos llamando recursivamente a funciones de retiro. Siempre debes asumir que los contratos externos son hostiles.

Ejemplo vulnerable:

solidity
Copied
contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 bal = balances[msg.sender]; require(bal > 0); // DANGER: Sends Ether before updating balance! (bool sent,) = msg.sender.call{value: bal}(""); require(sent); balances[msg.sender] = 0; // Too late - already reentered! } } contract Attacker { VulnerableBank bank; fallback() external payable { if (address(bank).balance >= 1 ether) { bank.withdraw(); // Calls withdraw again! } } function attack() external payable { bank.withdraw(); // Starts the loop } }

Implementación segura:

solidity

`import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances;

solidity
Copied
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw() public nonReentrant { uint256 bal = balances[msg.sender]; require(bal > 0); // Update state BEFORE external call balances[msg.sender] = 0; (bool sent,) = msg.sender.call{value: bal}(""); require(sent); } }

Prácticas recomendadas: Usa el modificador ReentrancyGuard de OpenZeppelin, sigue el patrón Checks-Effects-Interactions (validar → actualizar estado → llamadas externas), y considera patrones pull-over-push donde los usuarios retiran los fondos por sí mismos.

3. Usa msg.sender en lugar de tx.origin para autenticación

La llamada tx.origin traza de vuelta hasta el iniciador original de la transacción, mientras que msg.sender se refiere a quien llama inmediatamente; esta distinción importa mucho para la seguridad. En cadenas de llamadas entre múltiples contratos, tx.origin permanece constante, pero puede ser explotado por contratos intermediarios maliciosos que engañan a tu código haciéndole creer que el usuario original está autorizado.

Un atacante puede crear un contrato de phishing que llame a tu función, y si verificas tx.origin, este apunta a la víctima que inició la transacción, evadiendo por completo tu autenticación. msg.sender siempre es quien llama directamente, lo que lo hace confiable para el control de acceso. Usa tx.origin solo en casos raros donde específicamente necesites al originador de la transacción, y nunca para autenticación. Esta vulnerabilidad se relaciona directamente con las fallas de control de acceso, el riesgo #1 de OWASP.

Ejemplo vulnerable:

solidity
Copied
contract VulnerableWallet { address public owner; constructor() { owner = msg.sender; } function transfer(address payable to, uint256 amount) public { require(tx.origin == owner, "Not owner"); // BAD! to.transfer(amount); } } // Attacker tricks owner into calling this contract MaliciousContract { function attack(address wallet) public { // When owner calls this, tx.origin == owner// So the wallet thinks the call is authorized! VulnerableWallet(wallet).transfer(payable(msg.sender), 1 ether); } }

Implementación segura:

solidity
Copied
contract SecureWallet { address public owner; constructor() { owner = msg.sender; } function transfer(address payable to, uint256 amount) public { require(msg.sender == owner, "Not owner"); // GOOD! to.transfer(amount); } }

Por qué importa: Si un propietario interactúa accidentalmente con un contrato malicioso (haciendo clic en un enlace de phishing, usando una app comprometida), ese contrato puede drenar la billetera vulnerable porque tx.origin sigue apuntando al propietario. Con msg.sender, solo la dirección del propietario en sí puede autorizar transferencias. Asegúrate de usar siempre msg.sender para autenticación y validaciones de control de acceso.

4. Usa correctamente los modificadores de visibilidad de Solidity

Los modificadores de visibilidad definen los límites de acceso: public (llamable por cualquiera, interna o externamente), external (solo desde fuera del contrato), internal (este contrato más los contratos que heredan), y private (estrictamente este contrato). En versiones anteriores de Solidity, omitir un modificador tenía por defecto public, exponiendo accidentalmente funciones sensibles a atacantes externos.

Una visibilidad incorrecta ha llevado a exploits donde hackers llamaron funciones de administrador o manipularon variables de estado críticas que deberían haber estado restringidas. Siempre declara la visibilidad explícitamente: es tanto una práctica de seguridad como una mejora en la legibilidad.

solidity

`contract VulnerableBank { mapping(address => uint) balances;

solidity
Copied
contract VulnerableBank { mapping(address => uint) balances; // BAD: accidentally public in old Solidity function resetBalance(address user) { balances[user] = 0; // anyone can call this! } // GOOD: explicit visibility function deposit() external payable { balances[msg.sender] += msg.value; } function _updateInternal() internal { // only this contract + children } }

Usa herramientas de análisis estático como Slither para detectar modificadores de visibilidad faltantes o incorrectos antes del despliegue.

5. Evita la manipulación del timestamp del bloque

Los validadores pueden manipular block.timestamp dentro de una ventana pequeña (aproximadamente ±15 segundos en Ethereum), lo que lo hace poco confiable para una temporización precisa o como fuente de aleatoriedad. Los mineros o validadores pueden ajustar los timestamps a su favor, activando pagos, ganando loterías, o evadiendo verificaciones basadas en tiempo.

Por eso, nunca uses block.timestamp para lógica crítica donde los segundos importan, y jamás lo uses para generar números aleatorios. Está bien para estimaciones aproximadas de tiempo (como "¿pasaron 24 horas?"), pero es peligroso para condiciones exactas.

Ejemplo vulnerable:

solidity
Copied
contract TimestampLottery { uint public lastPlay; fallback() external payable { require(msg.value == 10 ether); require(block.timestamp != lastPlay); lastPlay = block.timestamp; // BAD: validator can manipulate timestamp to win if (block.timestamp % 15 == 0) { payable(msg.sender).transfer(address(this).balance); } } }

Mejor enfoque para temporización:

solidity
Copied
contract BlockBasedTiming { uint public startBlock = block.number; // Use block numbers instead (~12s per block on Ethereum) function isTimePassed(uint blocks) public view returns (bool) { return block.number >= startBlock + blocks; } }

Para aleatoriedad, nunca la implementes por tu cuenta. En su lugar usa Chainlink VRF o oráculos de función aleatoria verificable similares que proporcionan aleatoriedad criptográficamente segura y a prueba de manipulaciones.

6. Evita el desbordamiento y subdesbordamiento aritmético

En versiones de Solidity anteriores a la 0.8, los enteros se desbordan silenciosamente cuando exceden sus valores máximos o mínimos. Es decir, un uint8 en 255 se incrementa a 0, o de 0 se decrementa a 255.

Esto ha causado exploits catastróficos donde los atacantes acuñaron tokens infinitos, crearon balances negativos que se desbordaron hacia cantidades enormes, o evadieron verificaciones críticas.

Ejemplo vulnerable (pre-0.8):

solidity
Copied
pragma solidity 0.7.0; contract UnsafeToken { mapping(address => uint8) public balances; function transfer(address to, uint8 amount) public { balances[msg.sender] -= amount; // Underflow: 0 - 1 = 255 balances[to] += amount; // Overflow: 255 + 1 = 0 } }

Actualiza a Solidity >=0.8 para protección automática contra desbordamientos:

solidity
Copied
pragma solidity ^0.8.0; contract SafeToken { mapping(address => uint8) public balances; function transfer(address to, uint8 amount) public { balances[msg.sender] -= amount; // Reverts on underflow balances[to] += amount; // Reverts on overflow } }

Si estás atascado en una versión anterior de Solidity, usa la librería SafeMath de OpenZeppelin. Con 0.8+, las verificaciones están incorporadas y revierten automáticamente, pero puedes usar bloques unchecked \{\} cuando quieras intencionalmente un comportamiento de desbordamiento para optimización de gas.

7. Implementa un control de acceso robusto

Un control de acceso roto permite que usuarios no autorizados ejecuten funciones privilegiadas; es la vulnerabilidad #1 en el top 10 de Web3 de OWASP y causó más de $1.600 millones en pérdidas solo en la primera mitad de 2025. Sin los resguardos adecuados, los atacantes pueden drenar fondos, acuñar tokens, pausar contratos, o cambiar la propiedad.

Los errores comunes incluyen modificadores faltantes, depender de tx.origin para autenticación, o usar verificaciones simples de require\(msg.sender == owner\) que se pasan por alto durante actualizaciones. La solución es el control de acceso basado en roles con el principio de mínimo privilegio: dale a cada dirección solo los permisos que absolutamente necesita.

Implementación segura con OpenZeppelin:

solidity
Copied
pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; contract SecureVault is AccessControl { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); } function withdrawFunds() public onlyRole(ADMIN_ROLE) { // Only admins can withdraw } function updateConfig() public onlyRole(OPERATOR_ROLE) { // Operators handle config, not full admin } }

Audita regularmente las asignaciones de roles, implementa acciones de administrador con retraso de tiempo para cambios de alto riesgo, y usa billeteras multi-sig para roles críticos. Nunca uses tx.origin para autenticación. Aprovecha librerías probadas en batalla como OpenZeppelin AccessControl en lugar de crear la tuya propia.

8. Asegura las integraciones de oráculos contra manipulación

Los oráculos fusionan la blockchain con datos del mundo real, pero si están centralizados o son fáciles de manipular, los atacantes pueden alimentar información falsa para explotar tus contratos. Este tipo de exploit está en el puesto #2 de los riesgos de Web3 de OWASP y ha drenado cientos de millones de protocolos DeFi.

Los ataques de flash loans a menudo manipulan oráculos de precios onchain (como usar un solo DEX como fuente de precios), permitiendo a los hackers inflar o desplomar artificialmente los precios para liquidar posiciones, drenar pools de liquidez, o acuñar préstamos sin suficiente colateral. Nunca dependas de una sola fuente de precios o precios spot que puedan ser manipulados dentro de una transacción.

Uso seguro de oráculos con Chainlink:

solidity
Copied
pragma solidity ^0.8.20; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract SecurePriceFeed { AggregatorV3Interface internal priceFeed; uint256 private constant STALENESS_THRESHOLD = 3600; // 1 hour constructor(address _aggregator) { priceFeed = AggregatorV3Interface(_aggregator); } function getLatestPrice() public view returns (int) { ( uint80 roundId, int price, , uint256 updatedAt, uint80 answeredInRound ) = priceFeed.latestRoundData(); require(price > 0, "Invalid price"); require(answeredInRound >= roundId, "Stale price"); require(block.timestamp - updatedAt < STALENESS_THRESHOLD, "Price too old"); return price; } }

Prácticas recomendadas: usa redes de oráculos descentralizadas como Chainlink con múltiples fuentes de datos, implementa Precios Promedio Ponderados por Tiempo (TWAPs) para operaciones críticas, valida la frescura y verifica que los precios estén dentro de rangos razonables, y agrega múltiples oráculos cuando sea posible. Revisa la documentación de Chainlink para feeds específicos de cada red y consideraciones de seguridad.

9. Valida todas las entradas exhaustivamente

No validar las entradas del usuario permite que los atacantes inyecten datos maliciosos, activen comportamientos inesperados, o exploten casos extremos en el código. Es el #4 en las vulnerabilidades de Web3 de OWASP y un vector común para drenar fondos o romper la lógica del contrato.

Sin las verificaciones adecuadas, los usuarios pueden pasar valores cero para evadir comisiones, cantidades negativas para explotar la aritmética, direcciones que apuntan a cero o a contratos maliciosos, o índices de arreglos que causan accesos fuera de los límites. Toda entrada externa es no confiable hasta que se demuestre que es segura. La defensa en profundidad significa validar en cada límite: verificar rangos, valores nulos, longitudes de arreglos, y restricciones de lógica de negocio antes de procesar cualquier dato proporcionado por el usuario.

Validación de entradas adecuada:

solidity
Copied
pragma solidity ^0.8.20; contract SecureDeposit { mapping(address => uint256) public balances; uint256 public constant MAX_DEPOSIT = 1000 ether; function deposit(uint256 amount) public payable { require(amount > 0, "Amount must be positive"); require(amount == msg.value, "Amount mismatch"); require(amount <= MAX_DEPOSIT, "Exceeds max deposit"); require( amount <= type(uint256).max - balances[msg.sender], "Balance overflow" ); balances[msg.sender] += amount; } function transfer(address to, uint256 amount) public { require(to != address(0), "Invalid recipient"); require(to != address(this), "Cannot transfer to contract"); require(amount <= balances[msg.sender], "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; } }

}`

Valida siempre que las cantidades sean distintas de cero y estén dentro de los límites, que las direcciones no sean nulas o inválidas, que los índices de arreglos estén en rango, y que se cumplan las restricciones de lógica de negocio.

Usa errores personalizados en Solidity 0.8.4+ para reversiones eficientes en gas con mensajes detallados.

10. Maneja las llamadas externas de forma segura

Las llamadas externas a otros contratos pueden fallar silenciosamente si no verificas sus valores de retorno. Este problema es el #6 en los riesgos de Web3 de OWASP y ha llevado a fondos atascados o contratos que asumen éxito cuando las operaciones en realidad fallaron.

Las llamadas de bajo nivel como call\(\), delegatecall\(\), y send\(\) devuelven indicadores booleanos de éxito en lugar de revertir automáticamente. Si ignoras estos valores de retorno, tu contrato podría continuar ejecutándose con supuestos falsos, pensando que los fondos se transfirieron cuando no fue así, o que una operación crítica se completó cuando en realidad falló. El transfer\(\) de ERC-20 también tiene este problema en algunas implementaciones que devuelven false en lugar de revertir.

Patrón vulnerable:

solidity
Copied
function unsafeTransfer(address payable recipient) public { recipient.send(1 ether); // Returns false on failure, but continues!// Contract thinks transfer succeeded }

Implementación segura:

solidity
Copied
contract SecureCaller { event CallExecuted(address target, bool success); function safeCall(address target, bytes memory data) public returns (bytes memory) { (bool success, bytes memory result) = target.call(data); require(success, "External call failed"); emit CallExecuted(target, success); return result; } function safeTransfer(address payable recipient, uint256 amount) public { (bool success, ) = recipient.call{value: amount}(""); require(success, "Transfer failed"); } // For ERC-20 tokens, use SafeERC20 from OpenZeppelin }

}`

Siempre captura y verifica los valores de retorno de las llamadas externas. Para transferencias de Ether, prefiere call\{value: x\}\(""\) sobre los ya obsoletos send\(\) o transfer\(\). Para transferencias de tokens, usa el wrapper SafeERC20 de OpenZeppelin, que maneja implementaciones no estándar de ERC-20 que no revierten al fallar.

11. Mitiga los ataques de flash loans

Los flash loans permiten pedir prestadas cantidades masivas sin colateral, siempre y cuando se paguen dentro de la misma transacción. Los atacantes pueden explotar esta lógica para manipular precios, drenar pools, o explotar la lógica del protocolo, lo que lo hace el #7 en los riesgos de Web3 de OWASP, con miles de millones perdidos en todo DeFi.

Los hackers usan capital de flash loans para distorsionar artificialmente los precios de oráculos, explotar errores de redondeo a escala, o crear condiciones de mercado temporales que activen lógica de contrato vulnerable. Un patrón clásico que se ha repetido una y otra vez en hackeos: pedir prestados millones, manipular un oráculo de precios o pool de liquidez, explotar el error de precio en tu protocolo, pagar el préstamo, y quedarse con la diferencia – todo de forma atómica en una sola transacción.

Patrón vulnerable:

solidity
Copied
contract VulnerablePool { function getPrice() public view returns (uint) { // BAD: using spot price that can be manipulated return tokenReserve / ethReserve; } function borrow(uint amount) public { uint collateral = getPrice() * userCollateral[msg.sender]; require(collateral >= amount, "Insufficient collateral"); // Attacker manipulates price in same transaction! } }

Mejor enfoque:

solidity
Copied
contract SecureLending { mapping(address => uint256) public lastBorrow; function borrow(uint256 amount) public { // Add cooldown between borrows require(block.number > lastBorrow[msg.sender] + 2, "Too soon"); lastBorrow[msg.sender] = block.number; // Use TWAP or Chainlink instead of spot price uint256 price = chainlinkOracle.getPrice(); uint256 collateral = price * userCollateral[msg.sender]; require(collateral * 150 / 100 >= amount, "Need 150% collateral"); } }

Estrategias de defensa: usa Precios Promedio Ponderados por Tiempo (TWAPs) o Chainlink en lugar de precios spot. Añade periodos de enfriamiento de varios bloques para operaciones críticas, exige sobrecolateralización, y verifica la salud de tu protocolo después de los cambios de estado. Si no necesitas flash loans, considera bloquearlos por completo.

12. Evita errores de lógica en funciones críticas

Los errores de lógica en funciones críticas rompen los supuestos de seguridad centrales de tu contrato y pueden ser catastróficos. Esta categoría de error es el #3 en los riesgos de Web3 de OWASP porque estas fallas socavan todo el sistema a pesar de ser código técnicamente "correcto".

A diferencia de los errores de sintaxis que detecta el compilador, los errores de lógica pasan todas las verificaciones pero producen resultados incorrectos: errores de desfase por uno en bucles, orden incorrecto de operaciones, manejo faltante de casos extremos, o condiciones defectuosas. Ejemplos clásicos incluyen olvidar actualizar balances después de transferencias, usar &gt;= en lugar de &gt;, o calcular comisiones en el orden equivocado causando pérdida de precisión.

Errores de lógica comunes:

solidity
Copied
contract LogicErrors { mapping(address => uint256) public balances; // ERROR 1: Balance updated AFTER transfer (reentrancy risk!) function withdraw(uint256 amount) public { payable(msg.sender).transfer(amount); balances[msg.sender] -= amount; // Too late! } // ERROR 2: Off-by-one lets loop access invalid index function distribute(address[] memory users) public { for(uint i = 0; i <= users.length; i++) { // Should be // Crashes on last iteration! } } }

Versión corregida:

solidity
Copied
contract SecureLogic { mapping(address => uint256) public balances; function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); // Update state BEFORE external call balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function distribute(address[] memory users) public { for(uint i = 0; i < users.length; i++) { // Correct boundary// Safe iteration } } }

Estrategias de defensa: Sigue el patrón checks-effects-interactions (validar → actualizar estado → llamadas externas), escribe pruebas unitarias para casos extremos (cero, valores máximos, arreglos vacíos), usa fuzzing con Foundry para probar entradas aleatorias, y define invariantes como "el total distribuido nunca debe exceder el balance del pool". Siempre haz revisiones por pares para funciones críticas.

6 herramientas populares de seguridad para smart contracts

Las prácticas recomendadas mencionadas pueden ayudarte a aumentar la resiliencia, pero también necesitas herramientas que puedan detectar errores que el ojo humano podría pasar por alto. Aquí una mezcla de herramientas clásicas y modernas para auditar tu código:

  1. Slither: Un analizador estático con más de 40 detectores de fallas. Excelente para escaneos rápidos e imprime detalles del contrato. GitHub.
  2. Mythril: Un analizador de bytecode de EVM para múltiples cadenas que puede detectar problemas simbólicos. Parte de MythX. GitHub.
  3. Securify: Un escáner respaldado por la Ethereum Foundation para más de 37 fallas, con análisis estático preciso. Sitio web (ya no disponible).
  4. Foundry: Un framework moderno de pruebas/fuzzing para Solidity que ofrece pruebas de invariantes, ideal para bugs de lógica. Book.
  5. Certora: Una herramienta de verificación formal que demuestra matemáticamente que el código coincide con las especificaciones. Website.
  6. Echidna: Un fuzzer basado en propiedades para detectar vulnerabilidades. GitHub.

Junto con estas 6 herramientas, también puedes asociarte con plataformas como Code4rena para auditorías con recompensas.

Asegura tus smart contracts: reflexiones finales

Como desarrollador, tu objetivo es simple: construir contratos que no sean hackeados, manteniendo seguros los fondos de los usuarios y tu aplicación funcionando sin problemas. En ese proceso, deberías adoptar las prácticas recomendadas anteriores y adoptar la mentalidad de ser seguro por diseño.

Usa proxies actualizables (mediante OpenZeppelin Upgrades), código modular, y abstracción de cuentas (como ERC-4337 para mejor UX/seguridad – consulta nuestra guía de ERC-4337 para más detalles). Ejecuta pruebas unitarias, verificaciones formales, auditorías profesionales, monitoreo en tiempo de ejecución (por ejemplo, mediante Fortress), y programas de recompensas por bugs en Immunefi. Tus usuarios (y el éxito de tu proyecto) dependen de ello.

Para más recursos sobre seguridad de contratos, revisa lo siguiente:

Preguntas frecuentes

¿Cuáles son los patrones de seguridad más importantes a seguir al escribir smart contracts en Solidity?

Los patrones más críticos incluyen usar checks-effects-interactions, implementar guards de reentrancy, validar todas las entradas, establecer modificadores de visibilidad explícitos, y seguir un control de acceso adecuado con permisos basados en roles.

¿Cómo puedo prevenir ataques de reentrancy en mis contratos?

Usa el modificador ReentrancyGuard de OpenZeppelin, sigue el patrón checks-effects-interactions actualizando el estado del contrato antes de hacer llamadas externas, y considera patrones pull-over-push donde los usuarios retiran los fondos por sí mismos.

¿Por qué debería usar msg.sender en lugar de tx.origin para autenticación?

tx.origin puede ser explotado por contratos intermediarios maliciosos que engañan a tu código haciéndole creer que el usuario original está autorizado, mientras que msg.sender siempre se refiere a quien llama directamente, lo que lo hace confiable para el control de acceso.

¿Cómo me protejo contra el desbordamiento y subdesbordamiento aritmético en Solidity?

Actualiza a Solidity 0.8.x o superior, que tiene verificaciones incorporadas de desbordamiento/subdesbordamiento que revierten automáticamente ante errores. Para versiones anteriores, usa librerías bien probadas como SafeMath de OpenZeppelin.

¿Qué papel juegan las pruebas y las auditorías de seguridad en la seguridad de smart contracts?

Las pruebas unitarias exhaustivas, el fuzzing con herramientas como Foundry, y el análisis estático con Slither ayudan a descubrir casos extremos y vulnerabilidades, mientras que las auditorías de seguridad independientes identifican fallas de lógica antes del despliegue en mainnet.

¿Cómo debo manejar delegatecall de forma segura en Solidity?

Usa delegatecall solo con implementaciones confiables y auditadas, nunca con direcciones controladas por usuarios, mantén diseños de almacenamiento idénticos entre contratos, y prefiere patrones de proxy establecidos como los contratos actualizables de OpenZeppelin.

¿Cómo puedo asegurar mis smart contracts contra ataques de flash loans?

Usa Precios Promedio Ponderados por Tiempo (TWAPs) u oráculos de Chainlink en lugar de precios spot, implementa periodos de enfriamiento de varios bloques para operaciones críticas, exige sobrecolateralización, y verifica la salud del protocolo después de los cambios de estado.

¿Qué herramientas debería usar para las pruebas de seguridad de smart contracts?

Las herramientas populares incluyen Slither para análisis estático, Foundry para pruebas y fuzzing, Mythril para análisis de bytecode, y Securify para un escaneo integral de vulnerabilidades antes del despliegue.

Background gradient

Construye magia blockchain

Alchemy combina los productos y herramientas de desarrollo Web3 más potentes con recursos, comunidad y un soporte legendario.