Seguridad de los contratos inteligentes
Última actualización de la página: 14 de septiembre de 2025
Los contratos inteligentes son extremadamente flexibles y capaces de controlar grandes cantidades de valor y datos, además de ejecutar lógica inmutable con base en el código implementado en la cadena de bloques. Esto ha creado un vibrante ecosistema de aplicaciones que no necesitan confianza (trustless) y descentralizadas que ofrecen muchas ventajas sobre los sistemas antiguos. También representan oportunidades para los atacantes que buscan obtener beneficios explotando las vulnerabilidades de los contratos inteligentes.
Las cadenas de bloques públicas como Ethereum complican aún más la cuestión de la seguridad de los contratos inteligentes. El código de un contrato ya implementado normalmente no se puede cambiar para reparar fallos de seguridad, mientras que los activos robados de los contratos inteligentes son extremadamente difíciles de rastrear y, en su mayoría, irrecuperables debido a la inmutabilidad.
Aunque las cifras varían, se estima que la cantidad total de valor robado o perdido debido a defectos de seguridad en los contratos inteligentes supera fácilmente los USD 1000 millones de dólares. Esto incluye incidentes de alto perfil, como el hackeo de la DAOopens in a new tab (3,6 millones de ETH robados, con un valor de más de 1000 millones de USD a precios actuales), el hackeo de la billetera multifirma de Parityopens in a new tab (30 millones de USD perdidos por los hackers) y el problema de la billetera congelada de Parityopens in a new tab (más de 300 millones de USD en ETH bloqueados para siempre).
Los problemas mencionados anteriormente hacen que sea imperativo que los desarrolladores inviertan esfuerzos en la creación de contratos inteligentes seguros, robustos y resistentes. La seguridad de los contratos inteligentes es un asunto serio que todo desarrollador hará bien en aprender. Esta guía abordará consideraciones de seguridad para los desarrolladores de Ethereum y explorará recursos para mejorar la seguridad de los contratos inteligentes.
Requisitos previos
Asegúrese de estar familiarizado con los fundamentos del desarrollo de contratos inteligentes antes de abordar la seguridad.
Directrices para crear contratos inteligentes de Ethereum seguros
1. Diseñar controles de acceso adecuados
En los contratos inteligentes, las funciones marcadas como public o external pueden ser llamadas por cualquier cuenta de propiedad externa (EOA) o cuenta de contrato. Especificar la visibilidad pública de las funciones es necesario si quiere que otros interactúen con su contrato. Sin embargo, las funciones marcadas como private solo pueden ser llamadas por funciones dentro del contrato inteligente y no por cuentas externas. Dar a todos los participantes de la red acceso a las funciones del contrato puede causar problemas, especialmente si esto significa que cualquiera pueda realizar operaciones sensibles (por ejemplo, mintear nuevos tokens).
Para evitar el uso no autorizado de las funciones de los contratos inteligentes, es necesario implementar controles de acceso seguros. Los mecanismos de control de acceso restringen la capacidad de utilizar ciertas funciones de un contrato inteligente a las entidades aprobadas, como las cuentas responsables de la gestión del contrato. El patrón Ownable y el control basado en roles son dos patrones útiles para implementar el control de acceso en los contratos inteligentes:
Patrón Ownable
En el patrón Ownable, se establece una dirección como "propietaria" del contrato durante el proceso de creación del contrato. A las funciones protegidas se les asigna un modificador OnlyOwner, que asegura que el contrato autentique la identidad de la dirección que llama antes de ejecutar la función. Las llamadas o invocaciones a funciones protegidas desde otras direcciones distintas a la del propietario del contrato siempre se revierten, lo que impide el acceso no deseado.
Control de acceso basado en roles
Registrar una única dirección como Owner en un contrato inteligente introduce el riesgo de centralización y representa un único punto de fallo. Si las claves de la cuenta del propietario se ven comprometidas, los atacantes pueden atacar el contrato. Es por ello que utilizar un patrón de control de acceso basado en roles con múltiples cuentas administrativas puede ser una mejor opción.
En el control de acceso basado en roles, el acceso a funciones sensibles se distribuye entre un conjunto de participantes de confianza. Por ejemplo, una cuenta puede encargarse de mintear tokens, mientras que otra cuenta realiza actualizaciones o pausa el contrato. Al descentralizar el control de acceso de esta manera, se eliminan los puntos únicos de falla y se reducen los supuestos de confianza para los usuarios.
Uso de billeteras multifirma
Otro enfoque para implementar un control de acceso seguro es utilizar una cuenta multifirma para gestionar un contrato. A diferencia de una EOA normal, las cuentas multifirma son propiedad de varias entidades y requieren las firmas de un número mínimo de cuentas, por ejemplo, de 3 a 5, para ejecutar las transacciones.
El uso de una cuenta multifirma para el control de acceso introduce una capa adicional de seguridad, ya que las acciones en el contrato de destino requieren el consentimiento de varias partes. Esto es particularmente útil si es necesario utilizar el patrón Ownable, ya que hace más difícil que un atacante o un insider deshonesto manipule las funciones sensibles del contrato con fines maliciosos.
2. Use las sentencias require(), assert() y revert() para proteger las operaciones del contrato
Como se ha mencionado, cualquiera puede invocar funciones públicas de su contrato inteligente una vez que se implementa en la cadena de bloques. Dado que no se puede saber de antemano cómo van a interactuar las cuentas externas con un contrato, lo ideal es implementar protecciones internas contra las operaciones problemáticas antes de la implementación. Puede imponer un comportamiento correcto en los contratos inteligentes utilizando las sentencias require(), assert() y revert() para activar excepciones y revertir los cambios de estado si la ejecución no satisface ciertos requisitos.
require(): require se define al inicio de las funciones y garantiza que se cumplan las condiciones predefinidas antes de que se ejecute la función llamada. Una sentencia require puede utilizarse para validar las entradas del usuario, comprobar las variables de estado o autenticar la identidad de la cuenta que llama antes de progresar con una función.
assert(): assert() se utiliza para detectar errores internos y comprobar las violaciones de «invariantes» en su código. Una invariante es una afirmación lógica sobre el estado de un contrato que debe ser cierta para todas las ejecuciones de la función. Un ejemplo de invariante es el total suministro máximo o el saldo de un contrato de tokens. El uso de assert() asegura que su contrato nunca alcance un estado vulnerable, y si lo hace, todos los cambios en las variables de estado se revierten.
revert(): revert() puede utilizarse en una sentencia if-else que desencadene una excepción si no se cumple la condición requerida. El contrato de ejemplo que se muestra a continuación utiliza revert() para proteger la ejecución de las funciones:
1pragma solidity ^0.8.4;23contract VendingMachine {4 address owner;5 error Unauthorized();6 function buy(uint amount) public payable {7 if (amount > msg.value / 2 ether)8 revert("No se ha proporcionado suficiente Ether.");9 // Realizar la compra.10 }11 function withdraw() public {12 if (msg.sender != owner)13 revert Unauthorized();1415 payable(msg.sender).transfer(address(this).balance);16 }17}Mostrar todo3. Probar contratos inteligentes y verificar la corrección del código
La inmutabilidad del código que se ejecuta en la Máquina virtual de Ethereum implica que los contratos inteligentes exijan un mayor nivel de evaluación de la calidad durante la fase de desarrollo. Probar el contrato de forma exhaustiva y observar cualquier resultado inesperado mejorará mucho la seguridad y protegerá a los usuarios a largo plazo.
El método habitual es escribir pruebas de unidades pequeñas utilizando datos simulados que se espera que el contrato reciba de los usuarios. Las pruebas unitarias son adecuadas para probar la funcionalidad de ciertas funciones y garantizar que un contrato inteligente funcione como se espera.
Desafortunadamente, las pruebas unitarias son poco efectivas para mejorar la seguridad de los contratos inteligentes cuando se utilizan de forma aislada. Una prueba unitaria puede demostrar que una función se ejecuta correctamente para datos simulados, pero son tan eficaces como las pruebas que se escriben. Esto dificulta la detección de casos límite y vulnerabilidades que podrían romper la seguridad de su contrato inteligente.
Un mejor enfoque es combinar las pruebas unitarias con pruebas basadas en propiedades realizadas mediante análisis estáticos y dinámicos. El análisis estático se basa en representaciones de bajo nivel, como gráficos de flujo de controlopens in a new tab y árboles sintácticos abstractosopens in a new tab para analizar los estados alcanzables de un programa y las rutas de ejecución. Mientras tanto, las técnicas de análisis dinámico, como el fuzzing de contratos inteligentesopens in a new tab, ejecutan código de contrato con valores de entrada aleatorios para detectar operaciones que violan las propiedades de seguridad.
La verificación formal es otra técnica para verificar las propiedades de seguridad en los contratos inteligentes. A diferencia de las pruebas habituales, la verificación formal puede demostrar de forma concluyente la ausencia de errores en un contrato inteligente. Esto se consigue creando una especificación formal que capture las propiedades de seguridad deseadas y demostrando que un modelo formal de los contratos se adhiera a esta especificación.
4. Pida una revisión independiente de su código
Después de probar su contrato, es bueno pedir a otros que comprueben el código fuente para detectar cualquier problema de seguridad. Las pruebas no descubrirán todas las fallas de un contrato inteligente, pero conseguir una revisión independiente aumenta la posibilidad de detectar vulnerabilidades.
Auditorías
Encargar una auditoría de un contrato inteligente es una forma de realizar una revisión independiente del código. Los auditores desempeñan un papel importante a la hora de garantizar que los contratos inteligentes sean seguros y estén libres de defectos de calidad y errores de diseño.
Dicho esto, hay que evitar tratar las auditorías como una bala de plata. Las auditorías no detectarán todos los errores y están diseñadas principalmente para proporcionar una ronda adicional de revisiones, que puede ayudar a detectar los problemas que los desarrolladores pasaron por alto durante el desarrollo y las pruebas iniciales. También es necesario cumplir con buenas prácticas para trabajar con los auditores, como documentar el código adecuadamente y añadir comentarios en línea, para maximizar el beneficio de una auditoría del contrato inteligente.
- Consejos y trucos de auditoría de contratos inteligentesopens in a new tab - @tinchoabbate
- Aproveche al máximo su auditoríaopens in a new tab - Inference
Recompensas por errores
La creación de un programa de recompensas por errores es otro enfoque para implementar revisiones de código externas. Un bug bounty es una recompensa económica que se da a las personas (normalmente hackers de sombrero blanco) que descubren vulnerabilidades en una aplicación.
Cuando se utilizan correctamente, las recompensas por errores ofrecen a los miembros de la comunidad de hackers un incentivo para inspeccionar su código en busca de fallas críticas. Un ejemplo de la vida real es el «error del dinero infinito» que habría permitido a un atacante crear una cantidad ilimitada de ether en Optimismopens in a new tab, un protocolo de capa 2 que se ejecuta en Ethereum. Afortunadamente, un hacker de sombrero blanco descubrió el falloopens in a new tab y se lo notificó al equipo, ganando una gran recompensa en el procesoopens in a new tab.
Una estrategia útil es establecer el pago de un programa de recompensas por fallas en proporción a la cantidad de fondos en juego. Descrito como la «recompensa por error de escalabilidadopens in a new tab», este enfoque proporciona incentivos financieros para que los individuos revelen responsablemente las vulnerabilidades en lugar de explotarlas.
5. Siga las mejores prácticas durante el desarrollo de contratos inteligentes
La existencia de auditorías y recompensas por errores no lo exime de la responsabilidad de escribir código de alta calidad. La sólida seguridad en los contratos inteligentes empieza por seguir procesos de diseño y desarrollo adecuados:
-
Almacenar todo el código en un sistema de control de versiones, como git
-
Hacer todas las modificaciones del código a través de pull requests (solicitudes pull)
-
Asegurarse de que las pull requests tengan al menos un revisor independiente; si trabaja en solitario en un proyecto, considere la posibilidad de buscar a otros desarrolladores e intercambiar revisiones de código
-
Utilice un entorno de desarrollo para probar, compilar e implementar contratos inteligentes.
-
Ejecute su código mediante herramientas básicas de análisis de código, como Cyfrin Aderynopens in a new tab, Mythril y Slither. En principio, debería hacer esto antes de combinar cada pull request y comparar las diferencias en el resultado
-
Asegurarse de que el código se compile sin errores y que el compilador de Solidity no emita advertencias
-
Documente adecuadamente su código (utilizando NatSpecopens in a new tab) y describa los detalles de la arquitectura del contrato en un lenguaje fácil de entender. Esto facilitará que otros auditen y revisen su código
6. Implemente planes sólidos de recuperación de desastres
El diseño de controles de acceso seguros, la implementación de modificadores de funciones y otras sugerencias pueden mejorar la seguridad de los contratos inteligentes, pero no pueden descartar la posibilidad de explotaciones maliciosas. Crear contratos inteligentes seguros requiere “prepararse para el fracaso” y tener un plan de respaldo para responder eficazmente a los ataques. Un plan adecuado de recuperación de desastres incorporará algunos o todos los siguientes componentes:
Actualizaciones de contrato
Si bien los contratos inteligentes de Ethereum son inmutables de forma predeterminada, es posible lograr cierto grado de mutabilidad usando patrones de actualización. La actualización de contratos es necesaria en los casos en que una falla crítica haga inutilizable su viejo contrato e implementar nueva lógica sea la opción más viable.
Los mecanismos de actualización de contratos funcionan de forma diferente, pero el “patrón de proxy” es una de las formas más populares. Los patrones de proxyopens in a new tab dividen el estado y la lógica de una aplicación entre dos contratos. El primer contrato (llamado “contrato proxy”) almacena variables de estado (p. ej., los balances de los usuarios), mientras que el segundo contrato (llamado “contrato de lógica”) contiene el código para la ejecución de las funciones del contrato.
Las cuentas interactúan con el contrato de proxy, que envía todas las llamadas de función al contrato de lógica utilizando la llamada de bajo nivel delegatecall()opens in a new tab. A diferencia de una llamada de mensaje normal, delegatecall() asegura que el código que se ejecuta en la dirección del contrato de lógica se ejecute en el contexto del contrato que realiza la llamada. Esto significa que el contrato de lógica siempre escribirá en el almacenamiento del proxy (en lugar de su propio almacenamiento) y que los valores originales de msg.sender y msg.value se conservarán.
Delegar llamadas al contrato de lógica requiere almacenar su dirección en el almacenamiento del contrato proxy. Por lo tanto, actualizar la lógica del contrato es solo cuestión de implementar otro contrato de lógica y almacenar la nueva dirección en el contrato proxy. A medida que las llamadas subsiguientes al contrato proxy se enruten automáticamente al nuevo contrato de lógica, habrá “actualizado” el contrato sin modificar realmente el código.
Más sobre la actualización de contratos.
Paradas de emergencia
Como se ha mencionado, las auditorías y pruebas exhaustivas no pueden descubrir todos los errores en un contrato inteligente. Si aparece una vulnerabilidad en su código después de la implementación, corregirla es imposible, ya que no puede cambiar el código que se ejecuta en la dirección del contrato. Además, los mecanismos de actualización (p. ej. los patrones proxy) pueden tardar en implementarse (suelen requerir la aprobación de diferentes partes), lo que solo da a los atacantes más tiempo para causar más daño.
La opción nuclear es implementar una función de “parada de emergencia” que bloquee las llamadas a funciones vulnerables en un contrato. Las paradas de emergencia típicamente constan de los siguientes componentes:
-
Una variable booleana global que indica si el contrato inteligente está en un estado de detenimiento o no. Esta variable se establece en
falseal configurar el contrato, pero volverá atrueuna vez que se detenga el contrato. -
Funciones que hagan referencia a la variable booleana en su ejecución. Tales funciones son accesibles cuando el contrato inteligente no se detiene, y se vuelven inaccesibles cuando se activa la función de parada de emergencia.
-
Una entidad que tiene acceso a la función de parada de emergencia, que establece la variable booleana en
true. Para evitar acciones maliciosas, las llamadas a esta función se pueden restringir a una dirección de confianza (por ejemplo, el propietario del contrato).
Una vez que el contrato active la parada de emergencia, no se podrán invocar ciertas funciones. Esto se logra envolviendo funciones selectas en un modificador que haga referencia a la variable global. A continuación, se muestra un ejemploopens in a new tab que describe una implementación de este patrón en contratos:
1// Este código no ha sido auditado profesionalmente y no ofrece garantías sobre su seguridad o corrección. Úselo bajo su propio riesgo.23contract EmergencyStop {45 bool isStopped = false;67 modifier stoppedInEmergency {8 require(!isStopped);9 _;10 }1112 modifier onlyWhenStopped {13 require(isStopped);14 _;15 }1617 modifier onlyAuthorized {18 // Compruebe aquí la autorización de msg.sender19 _;20 }2122 function stopContract() public onlyAuthorized {23 isStopped = true;24 }2526 function resumeContract() public onlyAuthorized {27 isStopped = false;28 }2930 function deposit() public payable stoppedInEmergency {31 // Lógica de depósito aquí32 }3334 function emergencyWithdraw() public onlyWhenStopped {35 // Retiro de emergencia aquí36 }37}Mostrar todoEste ejemplo muestra las características básicas de las paradas de emergencia:
-
isStoppedes un booleano que se evalúa comofalseal principio ytruecuando el contrato entra en modo de emergencia. -
Los modificadores de funciones
onlyWhenStoppedystoppedInEmergencycomprueban la variableisStopped.stoppedInEmergencyse utiliza para controlar las funciones que deberían ser inaccesibles cuando el contrato es vulnerable (p. ej.,deposit()). Las llamadas a estas funciones simplemente se revertirán.
onlyWhenStopped se utiliza para funciones que deben poder ser llamadas durante una emergencia (p. ej., emergencyWithdraw()). Tales funciones pueden ayudar a resolver la situación, de ahí su exclusión de la lista de "funciones restringidas".
El uso de una funcionalidad de parada de emergencia proporciona un recurso efectivo para hacer frente a vulnerabilidades graves en su contrato inteligente. No obstante, aumenta la necesidad de que los usuarios confíen en que los desarrolladores no lo activen por razones de su interés. Con este fin, existen varias soluciones, como descentralizar el control de la parada de emergencia, ya sea sometiéndolo a un mecanismo de votación en cadena, a un bloqueo temporal, o a la aprobación de una cartera multifirma.
Supervisión de eventos
Los eventosopens in a new tab le permiten realizar un seguimiento de las llamadas a las funciones de un contrato inteligente y supervisar los cambios en las variables de estado. Es ideal programar su contrato inteligente para que emita un evento cada vez que alguna parte realice una acción crítica para la seguridad (por ejemplo, retirar fondos).
Registrar eventos y supervisarlos fuera de la cadena proporciona información sobre las operaciones del contrato y facilita una detección más rápida de acciones maliciosas. Esto significa que su equipo puede responder más rápido a los hackeos y tomar medidas para mitigar el impacto en los usuarios, como pausar funciones o realizar una actualización.
También puede optar por una herramienta de monitoreo lista para usar que reenvíe automáticamente alertas cada vez que alguien interactúe con sus contratos. Estas herramientas le permitirán crear alertas personalizadas basadas en diferentes activadores, como el volúmen de transacciones, la frecuencia de las llamadas a funciones o las funciones específicas involucradas. Por ejemplo, podría programar una alerta que llegue cuando la cantidad retirada en una misma transacción supere un umbral en particular.
7. Diseñar sistemas de gobernanza seguros
Es posible que desee descentralizar su aplicación entregando el control de los contratos inteligentes básicos a los miembros de la comunidad. En este caso, el sistema de contratos inteligentes incluirá un módulo de gobernanza—un mecanismo que permite a los miembros de la comunidad aprobar acciones administrativas a través de un sistema de gobernanza en cadena. Por ejemplo, los titulares de tokens pueden votar por una propuesta para actualizar un contrato proxy a una nueva implementación.
La gobernanza descentralizada puede ser beneficiosa, especialmente porque alinea los intereses de los desarrolladores y los usuarios finales. A pesar de todo, los mecanismos de gobernanza de contratos inteligentes pueden introducir nuevos riesgos si se implementan incorrectamente. Un escenario plausible es que un atacante adquiera un enorme poder de voto (medido en número de tókenes en su poder) mediante la obtención de un préstamo flash e impulse una propuesta maliciosa.
Una forma de evitar problemas relacionados con la gobernanza en cadena es usar un bloqueo de tiempoopens in a new tab. Un bloqueo de tiempo impide que un contrato inteligente ejecute ciertas acciones hasta que pase una cantidad específica de tiempo. Otras estrategias incluyen asignar un "peso de votación" a cada token en función de cuánto tiempo ha estado bloqueado, o medir el poder de voto de una dirección en un período histórico (por ejemplo, 2-3 bloques en el pasado) en lugar del bloque actual. Ambos métodos reducen la posibilidad de acumular rápidamente poder de voto para manipular las votaciones en cadena.
Puede encontrar más información sobre cómo diseñar sistemas de gobernanza segurosopens in a new tab, sobre los diferentes mecanismos de votación en las DAOopens in a new tab y sobre los vectores de ataque comunes a las DAO que aprovechan las DeFiopens in a new tab en los enlaces compartidos.
8. Reducir la complejidad del código al mínimo
Los desarrolladores de software tradicionales están familiarizados con el principio KISS ("mantenlo simple, estúpido") (Keep it simple stupid), que desaconseja introducir complejidad innecesaria en el diseño de software. Esto sigue la idea de pensamiento de hace tiempo de que "los sistemas complejos fallan de maneras complejas" y son más susceptibles a errores costosos.
Mantener las cosas simples es de particular importancia a la hora de escribir contratos inteligentes, dado que los contratos inteligentes están controlando potencialmente grandes cantidades de valor. Un consejo para lograr la simplicidad al escribir contratos inteligentes es reutilizar las bibliotecas existentes, como los Contratos de OpenZeppelinopens in a new tab, siempre que sea posible. Debido a que estas bibliotecas han sido ampliamente auditadas y probadas por los desarrolladores, su uso reduce las posibilidades de introducir errores al escribir nuevas funcionalidades desde cero.
Otro consejo común es escribir pequeñas funciones y mantener los contratos modulares dividiendo la lógica empresarial entre múltiples contratos. Escribir código más simple no solo reduce la superficie de ataque en un contrato inteligente, sino que también hace que sea más fácil razonar sobre la corrección del sistema general y detectar posibles errores de diseño temprano.
9. Defenderse de las vulnerabilidades comunes de los contratos inteligentes
Reentrada
La EVM no permite la concurrencia, lo que significa que dos contratos involucrados en una llamada de mensaje no pueden ejecutarse simultáneamente. Una llamada externa pausa la ejecución y la memoria del contrato de llamada hasta que la llamada regresa, momento en el que la ejecución procede normalmente. Este proceso se puede describir formalmente como la transferencia del flujo de controlopens in a new tab a otro contrato.
Aunque en su mayor parte resulta inofensivo, la transferencia del flujo de control a contratos sin confianza puede causar problemas, como el reingreso o reentrada. Un ataque de reentrada ocurre cuando un contrato malicioso vuelve a llamar a un contrato vulnerable antes de que se complete la invocación de la función original. Este tipo de ataque se explica mejor con un ejemplo.
Piense en un contrato inteligente sencillo («víctima») que permite a cualquiera depositar y retirar ether:
1// Este contrato es vulnerable. No lo use en producción23contract Victim {4 mapping (address => uint256) public balances;56 function deposit() external payable {7 balances[msg.sender] += msg.value;8 }910 function withdraw() external {11 uint256 amount = balances[msg.sender];12 (bool success, ) = msg.sender.call.value(amount)("");13 require(success);14 balances[msg.sender] = 0;15 }16}Mostrar todoEste contrato expone una función withdraw() para permitir a los usuarios retirar el ETH previamente depositado en el contrato. Al procesar un retiro, el contrato realiza las siguientes operaciones:
- Comprueba el saldo de ETH del usuario
- Envía fondos a la dirección de llamada
- Restablece su saldo a 0, evitando retiros adicionales del usuario
La función withdraw() en el contrato Victim sigue un patrón de «comprobaciones-interacciones-efectos». Comprueba si se cumplen las condiciones necesarias para la ejecución (es decir, que el usuario tiene un saldo de ETH positivo) y realiza la interacción enviando ETH a la dirección de la persona que llama, antes de aplicar los efectos de la transacción (es decir, reduciendo el saldo del usuario).
Si se llama a withdraw() desde una cuenta de propiedad externa (EOA), la función se ejecuta como se espera: msg.sender.call.value() envía ETH a la persona que llama. Sin embargo, si msg.sender es una cuenta de contrato inteligente que llama a withdraw(), el envío de fondos usando msg.sender.call.value() también activará el código almacenado en esa dirección para que se ejecute.
Imagine que este es el código implementado en la dirección del contrato:
1 contract Attacker {2 function beginAttack() external payable {3 Victim(victim_address).deposit.value(1 ether)();4 Victim(victim_address).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(victim_address).withdraw();10 }11 }12}Mostrar todoEste contrato está diseñado para hacer tres cosas:
- Aceptar un depósito de otra cuenta (probablemente la EOA del atacante)
- Depositar 1 ETH en el contrato de la víctima
- Retirar el 1 ETH almacenado en el contrato inteligente
No hay nada malo aquí, excepto que Attacker tiene otra función que vuelve a llamar a withdraw() en Victim si el gas que queda de la entrada msg.sender.call.value es superior a 40 000. Esto le da a Attacker la capacidad de volver a entrar en Victim y retirar más fondos antes de que se complete la primera invocación de withdraw. El ciclo se ve así:
1- La EOA del atacante llama a `Attacker.beginAttack()` con 1 ETH2- `Attacker.beginAttack()` deposita 1 ETH en `Victim`3- `Attacker` llama a `withdraw()` en `Victim`4- `Victim` comprueba el saldo de `Attacker` (1 ETH)5- `Victim` envía 1 ETH a `Attacker` (lo que activa la función predeterminada)6- `Attacker` vuelve a llamar a `Victim.withdraw()` (tenga en cuenta que `Victim` no ha reducido el saldo de `Attacker` desde el primer retiro)7- `Victim` comprueba el saldo de `Attacker` (que sigue siendo 1 ETH porque no ha aplicado los efectos de la primera llamada)8- `Victim` envía 1 ETH a `Attacker` (lo que activa la función predeterminada y permite a `Attacker` volver a entrar en la función `withdraw`)9- El proceso se repite hasta que `Attacker` se queda sin gas, momento en el que `msg.sender.call.value` regresa sin activar retiros adicionales10- `Victim` finalmente aplica los resultados de la primera transacción (y las posteriores) a su estado, por lo que el saldo de `Attacker` se establece en 0Mostrar todoEl resumen es que, debido a que el saldo de la persona que llama no se establece en 0 hasta que se complete la ejecución de la función, las invocaciones posteriores tendrán éxito y permitirán que la persona que llame retire su saldo varias veces. Este tipo de ataque puede usarse para vaciar los fondos de un contrato inteligente, como ocurrió en el hackeo de la DAO en 2016opens in a new tab. Los ataques de reentrada siguen siendo un problema crítico para los contratos inteligentes hoy en día, como muestran las listas públicas de explotaciones de reentradaopens in a new tab.
Cómo prevenir los ataques de reentrada
Un enfoque para tratar la reentrada es seguir el patrón de comprobaciones-efectos-interaccionesopens in a new tab. Este patrón ordena la ejecución de las funciones de manera que lo primero sea el código que realiza las comprobaciones necesarias antes de progresar con la ejecución, luego venga el código que manipula el estado del contrato y finalmente venga el código que interactúa con otros contratos o EOA.
El patrón de comprobaciones-efecto-interacción se utiliza en una versión revisada del contrato Victim que se muestra a continuación:
1contract NoLongerAVictim {2 function withdraw() external {3 uint256 amount = balances[msg.sender];4 balances[msg.sender] = 0;5 (bool success, ) = msg.sender.call.value(amount)("");6 require(success);7 }8}Este contrato realiza una comprobación del saldo del usuario, aplica los efectos de la función withdraw() (restableciendo el saldo del usuario a 0) y procede a realizar la interacción (enviando ETH a la dirección del usuario). Esto garantiza que el contrato actualice su almacenamiento antes de la llamada externa, eliminando la condición de reentrada que permitió el primer ataque. El contrato Attacker todavía podría volver a llamar a NoLongerAVictim, pero dado que balances[msg.sender] se ha establecido en 0, los retiros adicionales generarán un error.
Otra opción es usar un bloqueo de exclusión mutua (comúnmente descrito como "mutex") que bloquee una parte del estado de un contrato hasta que se complete la invocación de una función. Esto se implementa utilizando una variable booleana que se establece en true antes de que la función se ejecute y se revierte a false después de realizar la invocación. Como se ve en el siguiente ejemplo, el uso de un mutex protege una función contra las llamadas recursivas mientras la invocación original todavía se está procesando, deteniendo efectivamente la reentrada.
1pragma solidity ^0.7.0;23contract MutexPattern {4 bool locked = false;5 mapping(address => uint256) public balances;67 modifier noReentrancy() {8 require(!locked, "Bloqueado por reentrada.");9 locked = true;10 _;11 locked = false;12 }13 // Esta función está protegida por un mutex, por lo que las llamadas reentrantes desde `msg.sender.call` no pueden volver a llamar a `withdraw`.14 // La sentencia `return` se evalúa como `true`, pero aun así evalúa la sentencia `locked = false` en el modificador15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {16 require(balances[msg.sender] >= _amount, "No hay saldo que retirar.");1718 balances[msg.sender] -= _amount;19 (bool success, ) = msg.sender.call{value: _amount}("");20 require(success);2122 return true;23 }24}Mostrar todoTambién puede utilizar un sistema de pagos pullopens in a new tab que requiera que los usuarios retiren fondos de los contratos inteligentes, en lugar de un sistema de «pagos push» que envíe fondos a las cuentas. Esto elimina la posibilidad de activar inadvertidamente el código en direcciones desconocidas (y también puede prevenir ciertos ataques de denegación de servicio).
Subdesbordamientos y desbordamientos de enteros
Un desbordamiento de enteros, o valor superior al aceptable (en inglés, overflow), se produce cuando los resultados de una operación aritmética sobrepasan el rango aceptable de valores, lo que hace que el resultado se corra al valor representable más bajo. Por ejemplo, un uint8 solo puede almacenar valores de hasta 2^8-1=255. Las operaciones aritméticas que resulten en valores superiores a 255 se desbordarán y restablecerán uint a 0, de manera similar a como el odómetro de un coche se restablece a 0 una vez que alcanza el kilometraje máximo (999999).
Los subdesbordamientos de enteros se producen por razones similares: los resultados de una operación aritmética caen por debajo del rango aceptable. Digamos que intentó disminuir 0 en un uint8; el resultado simplemente se revertiría al valor máximo representable (255).
Tanto los desbordamientos de enteros hacia arriba o hacia abajo (overflows y undeflows) pueden conducir a cambios inesperados en las variables de estado de un contrato y dar lugar a una ejecución no planificada. A continuación se muestra un ejemplo que muestra cómo un atacante puede explotar el desbordamiento aritmético de overflow en un contrato inteligente para realizar una operación no válida:
1pragma solidity ^0.7.6;23// Este contrato está diseñado para actuar como una bóveda de tiempo.4// El usuario puede depositar en este contrato pero no puede retirar durante al menos una semana.5// El usuario también puede ampliar el tiempo de espera más allá del período de espera de 1 semana.67/*81. Implemente TimeLock92. Implemente Attack con la dirección de TimeLock103. Llame a Attack.attack enviando 1 ether. Podrá retirar su ether11 de inmediato.1213¿Qué ha ocurrido?14Attack provocó el desbordamiento de TimeLock.lockTime y pudo retirar15antes del período de espera de 1 semana.16*/1718contract TimeLock {19 mapping(address => uint) public balances;20 mapping(address => uint) public lockTime;2122 function deposit() external payable {23 balances[msg.sender] += msg.value;24 lockTime[msg.sender] = block.timestamp + 1 weeks;25 }2627 function increaseLockTime(uint _secondsToIncrease) public {28 lockTime[msg.sender] += _secondsToIncrease;29 }3031 function withdraw() public {32 require(balances[msg.sender] > 0, "Fondos insuficientes");33 require(block.timestamp > lockTime[msg.sender], "El tiempo de bloqueo no ha expirado");3435 uint amount = balances[msg.sender];36 balances[msg.sender] = 0;3738 (bool sent, ) = msg.sender.call{value: amount}("");39 require(sent, "Error al enviar Ether");40 }41}4243contract Attack {44 TimeLock timeLock;4546 constructor(TimeLock _timeLock) {47 timeLock = TimeLock(_timeLock);48 }4950 fallback() external payable {}5152 function attack() public payable {53 timeLock.deposit{value: msg.value}();54 /*55 si t = tiempo de bloqueo actual, entonces necesitamos encontrar x tal que56 x + t = 2**256 = 057 así que x = -t58 2**256 = type(uint).max + 159 así que x = type(uint).max + 1 - t60 */61 timeLock.increaseLockTime(62 type(uint).max + 1 - timeLock.lockTime(address(this))63 );64 timeLock.withdraw();65 }66}Mostrar todoCómo prevenir los desbordamientos hacia arriba y hacia abajo
A partir de la versión 0.8.0, el compilador de Solidity rechaza el código que da como resultado desbordamientos de enteros, tanto overflows como underflows. Sin embargo, los contratos compilados con una versión inferior del compilador deben realizar comprobaciones de funciones que impliquen operaciones aritméticas o utilizar una biblioteca (p. ej., SafeMathopens in a new tab) que compruebe los subdesbordamientos/desbordamientos.
Manipulación de oráculos
Los oráculos obtienen información fuera de la cadena y la envían a la cadena para que los contratos inteligentes la utilicen. Con los oráculos, puedes diseñar contratos inteligentes que interoperan con sistemas fuera de la cadena, como los mercados de capitales, ampliando enormemente sus aplicaciones.
No obstante, si el oráculo se corrompe y envía información incorrecta a la cadena, los contratos inteligentes se ejecutarán en base a datos erróneos, lo que puede causar problemas. Esta es la base del "problema del oráculo", que se refiere a la tarea de asegurarse de que la información de un oráculo de cadena de bloques sea precisa, actualizada y oportuna.
Una cuestión inquietante relativa a la seguridad es usar un oráculo en cadena, como un intercambio descentralizado, para obtener el precio al contado de un activo. Las plataformas de préstamos en la industria de las finanzas descentralizadas (DeFi) a menudo hacen esto para determinar el valor del colateral de un usuario y así establecer cuánto puede pedir prestado.
Los precios de los DEX suelen ser precisos, en gran parte debido a que los arbitradores restauran la paridad en los mercados. Sin embargo, están expuestos a manipulaciones, especialmente si el oráculo en cadena calcula los precios de los activos en función de patrones históricos de negociación (como suele ser el caso).
Por ejemplo, un atacante podría inflar artificialmente el precio al contado de un activo obteniendo un préstamo flash justo antes de interactuar con su contrato de préstamo. Consultar el precio del activo en el DEX devolvería un valor más alto de lo normal (debido a la gran "orden de compra" del atacante que sesga la demanda del activo), lo que le permitiría pedir prestado más de lo permitido. Tales "ataques de préstamos flash" se han utilizado para explotar la dependencia de los oráculos de precios entre las aplicaciones DeFi, lo que ha costado a los protocolos millones en fondos perdidos.
Cómo prevenir la manipulación de los oráculos
El requisito mínimo para evitar la manipulación de oráculosopens in a new tab es utilizar una red de oráculos descentralizada que consulte información de múltiples fuentes para evitar puntos únicos de fallo. En la mayoría de los casos, los oráculos descentralizados tienen incentivos criptoeconómicos incorporados para alentar a los nodos de oráculos a que pasen información correcta, lo que los hace más seguros que los oráculos centralizados.
Si planea consultar a un oráculo en cadena para obtener precios de activos, piense en usar uno que implemente un mecanismo de precio medio ponderado en el tiempo (TWAP). Un oráculo TWAPopens in a new tab consulta el precio de un activo en dos puntos diferentes en el tiempo (que usted puede modificar) y calcula el precio al contado en función del promedio obtenido. La elección de períodos de tiempo más largos protege su protocolo contra la manipulación de precios, ya que los pedidos grandes ejecutados recientemente no pueden afectar a los precios de los activos.
Recursos de seguridad de contratos inteligentes para desarrolladores
Herramientas para analizar contratos inteligentes y verificar la corrección del código
-
Herramientas y bibliotecas de prueba - Colección de herramientas y bibliotecas estándar de la industria para realizar pruebas unitarias y análisis estáticos y dinámicos en contratos inteligentes.
-
Herramientas de verificación formal - Herramientas para verificar la corrección funcional en los contratos inteligentes y comprobar las invariantes.
-
Servicios de auditoría de contratos inteligentes - Listado de organizaciones que prestan servicios de auditoría de contratos inteligentes para proyectos de desarrollo de Ethereum.
-
Plataformas de recompensas por errores - Plataformas para coordinar recompensas por errores y premiar la divulgación responsable de vulnerabilidades críticas en los contratos inteligentes.
-
Fork Checkeropens in a new tab - Una herramienta en línea gratuita para comprobar toda la información disponible sobre un contrato bifurcado.
-
ABI Encoderopens in a new tab - Un servicio en línea gratuito para codificar las funciones de su contrato de Solidity y los argumentos del constructor.
-
Aderynopens in a new tab - Analizador estático de Solidity, que recorre los Árboles de sintaxis abstracta (AST) para señalar posibles vulnerabilidades e imprimir los problemas en un formato Markdown fácil de consumir.
Herramientas para supervisar contratos inteligentes
- Alertas en tiempo real de Tenderlyopens in a new tab - Una herramienta para recibir notificaciones en tiempo real cuando se produzcan eventos inusuales o inesperados en sus contratos inteligentes o billeteras.
Herramientas para la administración segura de contratos inteligentes
-
Safeopens in a new tab - Billetera de contrato inteligente que se ejecuta en Ethereum y que requiere que un número mínimo de personas apruebe una transacción antes de que pueda ocurrir (M de N).
-
Contratos de OpenZeppelinopens in a new tab - Bibliotecas de contratos para implementar funciones administrativas, que incluyen la propiedad del contrato, las actualizaciones, los controles de acceso, la gobernanza, la capacidad de pausa y mucho más.
Servicios de auditoría de contratos inteligentes
-
ConsenSys Diligenceopens in a new tab - Servicio de auditoría de contratos inteligentes que ayuda a los proyectos de todo el ecosistema de cadenas de bloques a garantizar que sus protocolos estén listos para su lanzamiento y creados para proteger a los usuarios.
-
CertiKopens in a new tab - Firma de seguridad de cadenas de bloques pionera en el uso de tecnología de verificación formal de vanguardia en contratos inteligentes y redes de cadena de bloques.
-
Trail of Bitsopens in a new tab - Empresa de ciberseguridad que combina la investigación de seguridad con una mentalidad de atacante para reducir el riesgo y fortalecer el código.
-
PeckShieldopens in a new tab - Empresa de seguridad de cadena de bloques que ofrece productos y servicios para la seguridad, privacidad y facilidad de uso de todo el ecosistema de la cadena de bloques.
-
QuantStampopens in a new tab - Servicio de auditoría que facilita la adopción general de la tecnología de cadena de bloques a través de servicios de seguridad y evaluación de riesgos.
-
OpenZeppelinopens in a new tab - Empresa de seguridad de contratos inteligentes que proporciona auditorías de seguridad para sistemas distribuidos.
-
Runtime Verificationopens in a new tab - Empresa de seguridad especializada en el modelado formal y la verificación de contratos inteligentes.
-
Hackenopens in a new tab - Auditor de ciberseguridad de Web3 que aporta un enfoque de 360 grados a la seguridad de la cadena de bloques.
-
Nethermindopens in a new tab - Servicios de auditoría de Solidity y Cairo que garantizan la integridad de los contratos inteligentes y la seguridad de los usuarios en Ethereum y Starknet.
-
HashExopens in a new tab - HashEx se centra en la auditoría de cadena de bloques y contratos inteligentes para garantizar la seguridad de las criptomonedas, proporcionando servicios como el desarrollo de contratos inteligentes, las pruebas de penetración y la consultoría de cadenas de bloques.
-
Code4renaopens in a new tab - Plataforma de auditoría competitiva que incentiva a los expertos en seguridad de contratos inteligentes a encontrar vulnerabilidades y ayudar a que la web3 sea más segura.
-
CodeHawksopens in a new tab - Plataforma de auditorías competitivas que aloja licitaciones de auditorías de contratos inteligentes para investigadores de seguridad.
-
Cyfrinopens in a new tab - Potencia de seguridad de Web3, que incuba la seguridad criptográfica a través de productos y servicios de auditoría de contratos inteligentes.
-
ImmuneBytesopens in a new tab - Firma de seguridad de Web3 que ofrece auditorías de seguridad para sistemas de cadena de bloques a través de un equipo de auditores experimentados y las mejores herramientas de su clase.
-
Oxorioopens in a new tab - Auditorías de contratos inteligentes y servicios de seguridad de cadena de bloques con experiencia en EVM, Solidity, ZK y tecnología de cadena cruzada para empresas criptográficas y proyectos DeFi.
-
Inferenceopens in a new tab - Empresa de auditoría de seguridad especializada en auditoría de contratos inteligentes para cadenas de bloques basadas en EVM. Gracias a sus auditores expertos, identifican posibles problemas y sugieren soluciones prácticas para solucionarlos antes de la implementación._
Plataformas de recompensas por errores
-
Immunefiopens in a new tab - Plataforma de recompensas por errores para contratos inteligentes y proyectos DeFi donde los investigadores de seguridad revisan el código, revelan vulnerabilidades, obtienen incentivos económicos y hacen que las criptomonedas sean más seguras.
-
HackerOneopens in a new tab - Plataforma de coordinación de vulnerabilidades y recompensas por errores que conecta a las empresas con evaluadores de penetración e investigadores de ciberseguridad.
-
HackenProofopens in a new tab - Plataforma experta de recompensas por errores para proyectos criptográficos (DeFi, contratos inteligentes, billeteras, CEX y más), donde profesionales de seguridad proporcionan servicios de triaje y a los investigadores se les paga por informes de errores relevantes y verificados.
-
Sherlockopens in a new tab - Asegurador en Web3 para la seguridad de los contratos inteligentes, con pagos para auditores gestionados a través de contratos inteligentes para garantizar que los errores relevantes se paguen de manera justa.
-
CodeHawksopens in a new tab - Plataforma de recompensas por errores competitiva donde los auditores participan en concursos y desafíos de seguridad, y (pronto) en sus propias auditorías privadas.
Publicaciones de vulnerabilidades y explotaciones conocidas de contratos inteligentes
-
ConsenSys: ataques conocidos de contratos inteligentesopens in a new tab - Explicación para principiantes de las vulnerabilidades de contrato más importantes, con código de ejemplo para la mayoría de los casos.
-
Registro SWCopens in a new tab - Lista seleccionada de elementos de la enumeración de debilidades comunes (CWE) que se aplican a los contratos inteligentes de Ethereum.
-
Rektopens in a new tab - Publicación actualizada regularmente de hackeos y explotaciones de criptomonedas de alto perfil, junto con informes post-mortem detallados.
Desafíos para aprender sobre seguridad de contratos inteligentes
-
Awesome BlockSec CTFopens in a new tab - Lista seleccionada de wargames de seguridad de la cadena de bloques, desafíos y competiciones de Capture The Flagopens in a new tab y soluciones.
-
Damn Vulnerable DeFiopens in a new tab - Juego de guerra para aprender sobre seguridad ofensiva en contratos inteligentes de DeFi y desarrollar habilidades en la búsqueda de errores y la auditoría de seguridad.
-
Ethernautopens in a new tab - Juego de guerra basado en Web3/Solidity donde cada nivel es un contrato inteligente que necesita ser «hackeado».
-
HackenProof x HackTheBoxopens in a new tab - Desafío de piratería de contrato inteligente, ambientado en una aventura de fantasía. La finalización exitosa del desafío también da acceso a un programa privado de recompensas por errores._
Mejores prácticas para proteger los contratos inteligentes
-
ConsenSys: mejores prácticas de seguridad de contratos inteligentes de Ethereumopens in a new tab - Lista exhaustiva de directrices para proteger contratos inteligentes de Ethereum.
-
Nascent: kit de herramientas de seguridad simpleopens in a new tab - Colección de guías prácticas centradas en la seguridad y listas de verificación para el desarrollo de contratos inteligentes.
-
Patrones de Solidityopens in a new tab - Recopilación útil de patrones seguros y de mejores prácticas para el lenguaje de programación de contratos inteligentes Solidity.
-
Documentos de Solidity: consideraciones de seguridadopens in a new tab - Directrices para escribir contratos inteligentes seguros con Solidity.
-
Estándar de verificación de seguridad de contratos inteligentesopens in a new tab - Lista de verificación de catorce partes creada para estandarizar la seguridad de los contratos inteligentes para desarrolladores, arquitectos, revisores y proveedores de seguridad.
-
Aprenda sobre seguridad y auditoría de contratos inteligentesopens in a new tab - El curso definitivo de seguridad y auditoría de contratos inteligentes, creado para desarrolladores de contratos inteligentes que buscan mejorar sus prácticas de seguridad y convertirse en investigadores de seguridad.