A solo 111 bytes del fallo: cómo una vulnerabilidad en HOT Omni Bridge provocó una pérdida de $3.86M

A solo 111 bytes del fallo: cómo una vulnerabilidad en HOT Omni Bridge provocó una pérdida de $3.86M

Los hechos apuntan a una posible discrepancia entre el código fuente público corregido y la versión del contrato que realmente se está ejecutando en la red.

12 oct 2026

El atacante utilizó un saldo grande, acumulado previamente, de un token auxiliar especial para solicitar reembolsos muy superiores al monto de un nuevo depósito. Al registrar los enormes valores numéricos resultantes, el evento de reembolso superó el límite de registro de NEAR de 16,384 bytes. Debido a esto, el manejador de depósitos de Intents no pudo completar la operación. Tras el fallo, HOT devolvió los activos originales que respaldaban el depósito, incluido USDT, pero el saldo interno de Intents acreditado por una operación exitosa anterior permaneció intacto.

Direcciones y sus roles

RedDirección o cuentaRol
BSC0x09fd1f5d9f185067a92493e43aa259ea4ab3ad37El atacante, que recibió alrededor de $3.865M
BSC0x233c5370CCfb3cD7409d9A3fb98ab94dE94Cb4CdTesorería de HOT Bridge
NEAR20e6bfe4685d99004fc2095d3a0fc6158cd810ce8f801196141e183ed6e19d65La cuenta del atacante, desde la cual se iniciaron los depósitos por lotes y las solicitudes de retiro; también el receptor de los activos devueltos que respaldaban el depósito
NEARdd1fe58ec316ada5b673b88c14fe2fa2b7ede82080440901dfbd64b994241757El contrato del atacante, receptor del saldo interno y contrato de retorno de llamada para las notificaciones
NEAR873518f0f5cf120d7a8e2a416337bba16d7ae622e91f7da3862f07e89dc2ba9eEl contrato del atacante que se convirtió en el receptor del saldo interno en las secuencias de operaciones posteriores del 1 de octubre que pudieron confirmarse. Forma abreviada: 8735…
NEARv2_1.omni.hot.tgEl contrato del puente HOT y el registro de representaciones multi-token NEP-245
NEARintents.nearEl contrato verificador de Intents y el registro interno de saldos de usuarios

Contexto

HOT Bridge, también llamado HOT Omni Bridge, conecta activos de blockchains externas con la red NEAR y está integrado con NEAR Intents para el intercambio de tokens. El SDK público del proyecto es @hot-labs/omni-sdk.

El sistema involucra tres niveles de contabilidad: el activo real en la blockchain externa, custodiado en la tesorería de HOT → la representación emitida por HOT de ese activo en NEAR → el saldo interno del usuario en intents.near.

Un depósito entrante normal pasa por las siguientes etapas:

  • El usuario deposita un activo real en la blockchain de origen.
  • HOT verifica el depósito y autoriza la representación correspondiente del activo en NEAR.
  • HOT emite los tokens correspondientes en NEAR. Estos tokens se transfieren a Intents, donde el monto correspondiente se refleja en el saldo interno del usuario.

El código de manejo de depósitos de Intents también admite una notificación opcional al receptor. El sistema primero acredita los fondos en la cuenta y luego llama a la aplicación receptora. Esa aplicación puede solicitar el reembolso de parte del monto depositado.

En un reembolso normal, Intents debe primero eliminar el monto correspondiente del saldo interno y luego devolver los activos que respaldan ese saldo. El manejador quema el crédito no utilizado, es decir, anula la parte correspondiente del saldo interno, e informa a HOT qué montos quedaron sin usar. HOT entonces devuelve los tokens correspondientes al remitente original.

Durante el incidente, se rompió el vínculo entre el segundo y el tercer nivel de contabilidad: HOT devolvió el respaldo del depósito en forma de tokens de representación en NEAR, pero Intents mantuvo el saldo interno del usuario. Esto permitió más tarde solicitar retiros de la tesorería a través del mecanismo normal de retiro del puente.

1. Preparación

El 29 de septiembre, el atacante recibió 10 USDT de BSC en NEAR en la transacción 9SiSBv….

El atacante también transfirió desde la red Gonka 97 × 10³⁶ + 97 unidades mínimas del activo auxiliar especial ctfhote6ae2a90d1836ef1. El depósito original coincide con la recepción posterior del activo en NEAR en la transacción Fmwao…. Como resultado, se emitieron tokens de representación de HOT a la dirección 20e6….

Luego, en la transacción 4wf2…, el atacante depositó 97 × 10³⁶ unidades del activo auxiliar en Intents para el contrato receptor del ataque dd1…. 97 unidades mínimas quedaron fuera de Intents. Esto creó el gran saldo interno necesario para las posteriores solicitudes de reembolso sobredimensionadas.

2. Explotación de la vulnerabilidad

La primera operación de explotación confirmada, 3k91…, comenzó el 29 de septiembre a las 16:14:19.280 UTC.

El atacante llamó a la función de HOT mt_batch_transfer_call, depositando en Intents 97 entradas del token auxiliar de una unidad cada una, junto con 10 USDT. Intents acreditó exitosamente los fondos al saldo interno de dd1… antes de notificar al contrato receptor controlado por el atacante. El contrato receptor solicitó reembolsos de 97 montos del token auxiliar, de 10³⁶ unidades cada uno, y luego indicó que no se necesitaba ningún reembolso de USDT.

El manejador de reembolsos vulnerable limitaba el monto del reembolso al saldo disponible, pero no tenía en cuenta el tamaño del depósito actual. Como resultado, el saldo preparado previamente permitió incluir montos de reembolso enormes en el evento de quema de tokens.

Aquí, la variable balance_left representa todo el saldo interno del destinatario en un activo determinado, incluyendo depósitos anteriores. La verificación determinaba si el destinatario tenía fondos suficientes en el saldo, pero no limitaba el reembolso al monto depositado en la operación actual.

Como resultado, una entrada de depósito de tan solo una unidad podía generar una solicitud de reembolso de 10³⁶ unidades si el saldo acumulado previamente por el destinatario podía cubrir ese monto. Cada monto de reembolso distinto de cero calculado se añadía luego al evento de quema de tokens, en las líneas 198-199 del código anterior.

El tamaño del evento resultante fue de 16,495 bytes:

ComponenteCálculoBytes
97 ID completos de tokens, de 125 caracteres cada uno97 × (125 caracteres + 2 comillas) + 96 comas12,415
97 montos de reembolso, cada uno una cadena entre comillas de 37 dígitos97 × (37 dígitos + 2 comillas) + 96 comas3,879
Prefijo EVENT_JSON, metadatos, ID del propietario, memo y otros caracteres JSONTamaño fijo201
Total12,415 + 3,879 + 20116,495

El error al formar el evento revirtió los cambios de saldo anteriores realizados por el manejador de reembolsos. HOT devolvió entonces todo el lote de depósito original, incluyendo los 10 USDT. El manejador de depósito había llamado a self.deposit(...) antes de programar la notificación al destinatario y la posterior llamada a mt_resolve_deposit.

Por lo tanto, el fallo de una llamada posterior no deshace automáticamente los cambios realizados exitosamente dentro de una llamada anterior. Este comportamiento está descrito en la documentación de NEAR. El saldo interno de Intents acreditado durante la etapa exitosa anterior permaneció intacto.

3. Escalando la operación

En una de las operaciones posteriores, 3mst2uZ32KPuCs7wc8rJqEtCrZqugCy4yDSZ3df8KPm3, el atacante depositó 649,984 USDT y solicitó un reembolso de exactamente el mismo monto al remitente.

El historial de transacciones reconstruido contiene 23 llamadas por lotes vinculadas a fallos del manejador de reembolsos. El patrón completo, en el que se retuvo el saldo interno mientras se devolvía el respaldo del depósito, ha sido confirmado de manera confiable en seis casos.

Problema de sincronización: cómo el protocolo Verus perdió $7.5M debido a diferencias entre blockchains3 ago 2026Leer más

4. Retirando los fondos

El atacante luego envió solicitudes de retiro desde NEAR a través de HOT, especificando la dirección de BSC 0x09fd…ad37 como destinatario. Cinco solicitudes corresponden a pagos exitosos de USDT desde la tesorería de HOT en BSC en 0x233c…4Cd.

La vulnerabilidad

1. El saldo interno se acredita antes de que el destinatario responda a la solicitud de reembolso

La implementación de Intents para gestionar los depósitos entrantes primero llama a:

self.deposit( receiver_id.clone(), core_token_ids .clone() .zip(amounts.iter().map(|amount| amount.0)), Some(«deposit»), ) .unwrap_or_else(|err| err.panic());

Para un depósito con notificación, el sistema luego programa una llamada al contrato destinatario y una llamada posterior a mt_resolve_deposit. La etapa que crea el saldo interno puede completarse con éxito antes de que el manejador de reembolso posterior arroje un error. El resultado final de la ejecución le indica a HOT que la operación posterior falló, aunque el saldo interno creado anteriormente ya existe.

2. El cálculo vulnerable permite un reembolso mayor que el depósito actual

El commit de NEAR del 1 de octubre 35044fd…, titulado «chore: TEMP change that reflects deployed state», reproduce la lógica de reembolso de la siguiente manera:

let requested_refund = requested_refund.unwrap_or(deposited); let balance_left = receiver.token_balances.amount_for(&token_id); let refund_amount = balance_left.min(requested_refund);*

Fuente: resolve_deposit_internal.

Este cálculo tiene en cuenta el saldo disponible, pero no limita el reembolso al monto del depósito actual. El bucle agrega cada monto de reembolso distinto de cero al evento de quema, restándolo del saldo del destinatario y del suministro total interno del token. Después de eso, se realiza la siguiente llamada:

MtEvent::MtBurn([burn_event].as_slice().into()).emit();

Estos cambios contables se realizan dentro del recibo del manejador de reembolso. Si falla la escritura del evento, todos los cambios realizados en ese recibo se revierten junto con él.

3. Una posible discrepancia entre el código publicado y la versión desplegada

La falta de límite en el monto del reembolso según el tamaño del depósito actual se corrigió en el repositorio público el 23 de marzo de 2026, a las 12:47:07 UTC. La corrección se publicó en el PR #240 / commit 015fa811…, titulado «fix: cap notify refunds by the original amounts». En la versión corregida, el cálculo se ve así:

let refund_amount = requested_refund.min(deposited).min(balance_left);*

Para las entradas auxiliares de una unidad utilizadas en el ataque, esto limita cada reembolso a como máximo una unidad, independientemente del saldo acumulado previamente. Este mecanismo evita que los montos de reembolso se multipliquen con números excesivamente grandes. También restaura la regla fundamental de la contabilidad de depósitos: un reembolso para la operación actual no puede tomar fondos depositados en depósitos anteriores.

El commit temporal de octubre 1 elimina este límite para reproducir el comportamiento de la versión desplegada. La nota de la prueba de regresión de octubre de NEAR indica explícitamente que los eventos de reembolso de tamaño excesivo provocaron que la devolución de llamada fallara «tal como ocurrió en la versión desplegada».

Respuesta y recuperación

Después del incidente, se llevó a cabo una actualización, reflejada en la transacción FPLaCtd…. La transacción principal, ejecutada el 2 de octubre a las 20:31:41.711 UTC, llama a ctl-intents.near.upgrade para el contrato intents.near. El evento del controlador indica la versión de lanzamiento 0.4.4.

Un recibo de implementación anidada exitoso, registrado a las 20:31:45.442 UTC, desplegó el código y ejecutó la función state_migrate. Alex Shevchenko declaró públicamente que se había devuelto el $3.86M completo.

#Tether#Hackeo#DeFi
El USDT fue devuelto, pero el saldo nunca se debitó: cómo fue atacado HOT Omni Bridge