Saltar al contenido

Ataque de reentrada

Un ataque de reentrada usa una llamada externa o callback para volver a entrar en la lógica de un contrato mientras su estado es incoherente. Explica rutas, defensas y límites de revisión.

Actualizado

Solo con fines educativos; no constituye asesoramiento de inversión. Un fallo de reentrada puede causar una pérdida rápida e irreversible de activos del contrato, y ninguna defensa o auditoría aislada demuestra que un contrato sea seguro.

Respuesta directa

Un ataque de reentrada puede ocurrir cuando la lógica de un contrato realiza una llamada externa y el destinatario vuelve a llamarlo antes de que termine la ejecución original y se restauren sus invariantes. Si siguen visibles saldos, participaciones, permisos o precios antiguos, el callback puede repetir una acción o afectar otra usando un estado incoherente.

El callback no tiene que entrar en la misma función ni transferir moneda nativa. Los hooks de tokens, callbacks de receptores NFT, llamadas a bóvedas o estrategias y callbacks de préstamos flash pueden transferir el control a código no confiable. La reentrada puede cruzar funciones, contratos y módulos; la reentrada de solo lectura también puede hacer que otro protocolo consuma un valor transitorio.

Cómo funciona

Una explotación típica sigue esta secuencia:

  1. El atacante entra en una función que cambia el estado y supera sus comprobaciones iniciales.
  2. El contrato vulnerable llama a una dirección, token o protocolo antes de completar su contabilidad.
  3. El código controlado por el atacante entra en el contrato original o conectado mientras el estado antiguo sigue visible.
  4. La llamada anidada repite un efecto o cambia estado compartido, y la ejecución retorna con una invariante rota.

La transferencia de control externa puede ser explícita, como una llamada de bajo nivel, u ocultarse tras una transferencia de tokens, hook de receptor, acuñación segura, adaptador de estrategia o interfaz de llamada arbitraria. Un revert suele deshacer el árbol de llamadas afectado, pero el atacante puede construir una secuencia anidada exitosa que retorne normalmente después de extraer valor o corromper la contabilidad.

Las defensas deben combinarse:

  • Aplicar comprobaciones-efectos-interacciones: validar primero, confirmar todos los efectos internos relevantes y dejar la interacción externa para el final.
  • Usar una protección en cada punto de entrada que comparta la invariante protegida, no solo en la función con la llamada evidente.
  • Preferir cobros iniciados por el receptor cuando corresponda y reducir llamadas a tokens, receptores, hooks, proxies e integraciones no confiables.
  • Definir invariantes entre contratos y probar callbacks maliciosos, rutas entre funciones, multicalls anidadas y consumidores de solo lectura.
  • Combinar la revisión de implementación con monitoreo, pausa y respuesta a incidentes cuando el diseño lo permita.

Ejemplo

Considere una bóveda que envía valor y borra el saldo del usuario solo después de la llamada:

function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "empty balance");

    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");

    balances[msg.sender] = 0;
}

El receptor obtiene el control mientras balances[msg.sender] aún contiene el importe anterior. Su función receptora puede volver a llamar a withdraw(), superar la misma comprobación y pedir otra transferencia. Actualizar el saldo antes de la llamada cierra esta ventana; una protección puede rechazar la entrada anidada. Ningún cambio demuestra que todo el sistema sea seguro, pues otro punto de entrada o contrato conectado puede exponer la misma invariante incompleta.

Riesgos

  • Una protección cubre una función mientras otra expone el mismo estado.
  • El orden local es correcto, pero una invariante entre contratos sigue incoherente durante el callback.
  • Un token, receptor, callback de préstamo flash o adaptador de estrategia ejecuta código externo inesperadamente.
  • Una función view publica un precio o tipo de cambio transitorio que otro protocolo usa en la misma transacción.
  • Una actualización, módulo o cambio de distribución de almacenamiento elude o corrompe el bloqueo original.
  • Las pruebas cubren el retiro recursivo, pero omiten rutas entre funciones, contratos y de solo lectura.

Para los usuarios, un sello de auditoría o protección de reentrada demuestra que existe un control, no que haya una garantía. Las actualizaciones, integraciones y acciones de emergencia privilegiadas pueden cambiar la superficie de ataque. Limite permisos y exposición, verifique la implementación desplegada y no suponga que las pérdidas se pueden revertir.

Errores comunes

  • La reentrada solo repite una función de retiro. Puede entrar en otra función o contrato, o mostrar datos incoherentes a un lector.
  • Solo las transferencias de moneda nativa activan callbacks. Los estándares de tokens, hooks de receptores e integraciones también pueden ejecutar código externo.
  • Una protección o comprobaciones-efectos-interacciones hacen seguro el contrato. Su alcance, todos los puntos de entrada compartidos y las invariantes entre contratos aún requieren revisión.
  • Una auditoría satisfactoria descarta la reentrada. Las auditorías tienen alcance limitado; cambios posteriores e integraciones no revisadas pueden crear rutas nuevas.

Temas relacionados

Fuentes

Navegación

Buscar en la wiki...