Back to Blog

~9,4 M USD perdidos: Exploits de Injective y Aquifer | BlockSec Weekly

Code Auditing
11 de septiembre de 2026
30 min read
Key Insights
  • Cuatro incidentes esta semana causaron pérdidas aproximadas de $9.4M en Injective, Solana, Ethereum y Flow EVM.

  • Los exploits de Injective, Aquifer y Notional Finance no requirieron manipulación de precios ni préstamos flash. Cada uno simplemente hizo que la contabilidad propia del protocolo produjera un número que nunca fue real: en Injective, un fondo de seguro cuyo identificador coincidió con el de un mercado liquidó un déficit fabricado de 12,744 USDC con un saldo no verificado en un token diferente que valía una fracción de centavo, y en Notional Finance una deuda de exactamente -2^128 fue valorada en cero.

  • En tres de los cuatro casos, el protocolo ya había escrito la verificación correcta, solo que no en la ruta que importaba. Aquifer implementó una lista de permitidos del Token Program que su punto de entrada de intercambio nunca llamó, Ankr FLOW protegió un punto de entrada de staking con un modificador de pausa y dejó su homólogo abierto, y Notional Finance usó una conversión verificada para una conversión en una función mientras que la conversión anterior seguía siendo una conversión directa sin verificar.

Durante la semana pasada (2026/08/31 - 2026/09/06), observamos 4 incidentes de seguridad con una pérdida total estimada de aproximadamente $9.4M.

Fecha Incidente Tipo Pérdida Estimada
2026/08/31 Ankr FLOW Validación de Estado Defectuosa ~$410K
2026/08/31 Aquifer Validación de Entrada Defectuosa ~$2.47M
2026/08/31 Injective Validación de Denominación Faltante ~$4.8M
2026/09/03 Notional Finance Conversión Insegura ~$1.73M

El Mejor Auditor de Seguridad para Web3

Valide el diseño, el código y la lógica de negocio antes del lanzamiento

Destacado de la Semana: Injective

Este incidente es el destacado de la semana por la complejidad de su cadena de ataque y el tamaño de la pérdida: dos defectos independientes tuvieron que alinearse antes de que se pagara una sola liquidación. Ambos se encontraban en la propia lógica de intercambio de la cadena, en lugar de en un contrato de aplicación. Muestra cómo un identificador ensamblado a partir de campos concatenados sin separadores puede fusionar silenciosamente dos objetos que nunca estuvieron destinados a estar relacionados, y cuánto cuesta esa fusión cuando la ruta de liquidación nunca verifica que un fondo posea la denominación del mercado que respalda.

El 31/08/2026, la lógica de opciones binarias dentro del módulo exchange de Injective fue explotada por aproximadamente $4.8M en USDC. Injective es una cadena de capa 1 que integra un intercambio de libro de órdenes directamente en la cadena, por lo que el código afectado forma parte del software del nodo en lugar de un contrato desplegado por alguien. Un fondo de seguro que poseía INJ, el token nativo de Injective, terminó vinculado a un mercado de opciones binarias cotizado en USDC porque los dos identificadores colisionaron, y la ruta de liquidación que recurre a un fondo de seguro nunca comparó los dos. El atacante negoció contra sus propias subcuentas dentro de dicho mercado para fabricar un déficit, que el protocolo luego cubrió con un saldo de INJ que valía una fracción de centavo. Cada posición fue reembolsada en su totalidad y el atacante retiró mucho más de lo que había depositado.

Antecedentes

El módulo exchange de Injective lista mercados de opciones binarias, que son apuestas totalmente colateralizadas sobre un resultado sí-o-no. Cualquiera puede listar uno pagando una tarifa de listado, eligiendo el oráculo que lo resolverá y las marcas de tiempo en las que vence y se liquida. El oráculo se nombra como un proveedor más un símbolo: convertirse en proveedor requiere una votación de gobernanza, mientras que el símbolo es cualquier cadena que proporcione quien lo lista. Un operador primero deposita tokens de cotización como USDC en una subcuenta, luego toma una posición colocando una orden. Una orden BUY apuesta a que el evento ocurre y bloquea P * Q como margen; una orden SELL apuesta a que no ocurre y bloquea (1 - P) * Q, donde Q es la cantidad de contratos y P es el precio de entrada en el rango [0, 1]. El lado que apuesta por el resultado más probable, por lo tanto, deposita el margen mayor. El EndBlocker, el gancho que la cadena ejecuta al final de cada bloque, empareja una orden BUY y una SELL al mismo precio en una posición LONG y una SHORT. Debido a que los dos bloqueos siempre suman exactamente Q, un libro recién emparejado está totalmente financiado por construcción.

Al vencimiento, el oráculo publica un precio de liquidación S en [0, 1] y a cada posición se le paga margin ± (S - entry) * Q desde el fondo del mercado. Los pagos entre participantes son estrictamente de suma cero, y cada pago tiene un piso de cero, por lo que una posición nunca puede volverse negativa ni necesita liquidación. Una posición también puede cerrarse anticipadamente colocando una orden opuesta con margin = 0, lo que libera su margen más su ganancia realizada, pagada del margen que bloquea quien abre la posición entrante. El margen de cada posición se mantiene en lo que bloqueó al entrar, por lo que después de un cierre anticipado los márgenes en el libro ya no necesitan sumar el total del fondo.

Un mercado cuyo símbolo ningún proveedor publica llega a la liquidación sin ningún precio en absoluto. El módulo tiene un mecanismo de respaldo para ese caso: la liquidación cae en una ruta de reembolso implementada por getBinaryOptionsSocializedLossDataWithRefundFlag(), que deshace el mercado en lugar de resolverlo. A cada posición simplemente se le reembolsa su margen, porque la ruta cierra cada una a su propio precio de entrada, por lo que ninguna posición registra una ganancia o una pérdida, siempre que el libro aún contenga lo que las posiciones reclaman. Los reembolsos se financian con el saldo del mercado y, para cualquier déficit, con el fondo de seguro asociado a ese mercado.

Un mercado y un fondo de seguro son objetos separados, cada uno creado por su propio mensaje y cada uno con una denominación: un mercado se cotiza en un token, y un fondo posee el token con el que fue creado. Se emparejan por identidad: un fondo respalda al mercado cuyo ID sea igual al suyo. Ambos ID son resúmenes keccak256 de campos de identidad. El módulo exchange en sí es una única cuenta bancaria omnibús cuyos saldos por mercado y por fondo son una contabilidad de enteros sin procesar.

Análisis de la Vulnerabilidad

El componente defectuoso es el manejo de opciones binarias en el módulo exchange de injective-core, corregido en el commit b994d6b6 [1]. Dos defectos encadenados permiten que un fondo que posee una denominación respalde a un mercado cotizado en otra.

Defecto 1: los identificadores se derivan de una concatenación sin separadores. NewBinaryOptionsMarketID(), que calcula el ID de un mercado cuando ese mercado se lanza, y CreateInsuranceFund(), que calcula el ID del mercado que un nuevo fondo debe respaldar (para expiry = BinaryOptionsExpiryFlag = -2), ambos derivan ese ID de la misma expresión:

return crypto.Keccak256Hash([]byte((BINARY_OPTIONS_MARKET_ID_PREFIX +
    oracleType.String() + ticker + quoteDenom + oracleSymbol + oracleProvider)))

No hay separadores de campo ni prefijos de longitud, por lo que los límites entre los campos no dejan ningún rastro en los bytes hasheados. CreateInsuranceFund() mapea los propios campos de un fondo de seguro en estos espacios, con oracle_base ocupando el espacio de oracleSymbol y oracle_quote el de oracleProvider. Una tupla de fondo y una tupla de mercado pueden, por lo tanto, producir preimágenes idénticas byte a byte mientras dividen esos bytes entre campos de manera diferente, y los dos objetos comparten entonces un ID. El registro acepta ese ID compartido como el vínculo entre ellos, por lo que un fondo puede convertirse en el fondo de seguro de un mercado cuyo quoteDenom no posee.

Defecto 2: los pagos nunca se verifican contra la denominación del mercado. PayDeficitFromInsuranceFund() mueve monedas sin procesar fuera del fondo en la denominación que el fondo posee, y luego acredita el mismo entero sin procesar al saldo del mercado, sin comparar nunca insuranceFund.DepositDenom contra la denominación de cotización del mercado. Dado que la contabilidad del módulo son enteros simples, una unidad sin procesar de INJ y una unidad sin procesar de USDC son indistinguibles en esta ruta, aunque el mismo entero represente valores que difieren por un factor del orden de 10^11. El commit de corrección agrega la verificación que le faltaba a esta ruta, tanto en el lado de entrada como en el de salida, de modo que un fondo solo pueda respaldar mercados cotizados en la denominación que posee:

El mismo commit también deshabilita por completo el comercio y la liquidación de opciones binarias en la mainnet de Injective, eliminando la ruta de reembolso como superficie de ataque.

Análisis del Ataque

Todos los pasos fueron ejecutados por una sola billetera a través de tres de sus propias subcuentas (...037c, ...037d y ...037e), con Q = 15,930.

El par colisionante se construyó desplazando dónde caen los límites de los campos mientras se mantenían idénticos los bytes concatenados. El ticker y quoteDenom del fondo (X e inj) forman el ticker del mercado Xinj, y el oracle_base del fondo (una dirección de contrato y un símbolo de oráculo pegados) forma el quoteDenom del mercado seguido de su oracleSymbol:

Espacio en la concatenación Fondo (MsgCreateInsuranceFund) Mercado (MsgInstantBinaryOptionsMarketLaunch)
prefijo -BINARY-OPTIONS-MARKET- -BINARY-OPTIONS-MARKET-
oracleType.String() Provider Provider
ticker X Xinj
quoteDenom inj erc20:0xa00C...235a
oracleSymbol erc20:0xa00C...235aNO_PRICE_FOR_REFUND...297, rellenado a partir del oracle_base del fondo con ambas partes empaquetadas en ese único campo NO_PRICE_FOR_REFUND...297
oracleProvider Frontrunner, rellenado a partir del oracle_quote del fondo Frontrunner

Ambos se resuelven en 0x9793a39f82993cdedb0c82c614102d5aba2451f29709dc9a6f7df191020b4efc. El oráculo llamado NO_PRICE_FOR_REFUND fue configurado para nunca publicar un precio, lo que fuerza la liquidación hacia la ruta de reembolso.

El siguiente análisis se basa en la transacción 0x6ae9cb...51dcf8.

  • Paso 1: En una transacción atómica en el bloque 181024772, el atacante creó el fondo de seguro colisionante denominado en INJ y el mercado de opciones binarias denominado en USDC, sembró el fondo con 12,744,000,000 unidades sin procesar de INJ (que valen aproximadamente $0.000000063), y depositó 30,267.02 USDC a través de las tres subcuentas. El depósito ínfimo se dimensionó de manera que su entero sin procesar coincidiera con el déficit que el atacante planeaba fabricar. Cada subcuenta recibió exactamente el margen que necesitaría más tarde:
Subcuenta Depósito (USDC) Margen que financia
037d 1,593.001593 BUY de 15,930 a 0.10, bloqueando 1,593 (Paso 2)
037c 14,337.001593 SELL de 15,930 a 0.10, bloqueando 14,337 (Paso 2)
037e 14,337.014337 BUY de 15,930 a 0.90, bloqueando 14,337 (Paso 3)
Total 30,267.017523 -
  • Paso 2: En la misma transacción, 037d colocó una orden BUY de 15,930 a 0.10 y 037c colocó una orden SELL de 15,930 a 0.10. El EndBlocker las emparejó en una posición LONG para 037d con 1,593 de margen y una posición SHORT para 037c con 14,337. El margen total en el libro es 1.0Q = 15,930, exactamente lo que posee el fondo del mercado, por lo que el libro es indistinguible de cualquier mercado normal totalmente colateralizado.

  • Paso 3: Dos bloques y 1.1 segundos después, en la transacción 0x012c17...2af694 en el bloque 181024774, 037d cerró su posición larga a 0.90 con margin = 0 y recibió 1,593 + (0.90 - 0.10) * 15,930 = 14,337. Ese pago provino del margen bloqueado por 037e, quien abría la posición entrante, y que colocó una orden BUY de 15,930 a 0.90 y bloqueó 14,337. La ganancia realizada de 0.8Q = 12,744 ahora se encuentra en el saldo disponible de 037d fuera del fondo del mercado, mientras que los márgenes de las dos posiciones restantes permanecen en el libro: los pasivos ascienden a 1.8Q = 28,674 frente a un fondo que todavía posee 1.0Q.

  • Paso 4: La liquidación es impulsada por el propio reloj del mercado, no por una transacción. Al comienzo de cada bloque, el módulo detecta cualquier mercado cuya marca de tiempo de liquidación haya pasado y lo liquida en su totalidad, en lugar de posición por posición. Este se activó 18 segundos después de la creación del mercado, con ambas posiciones todavía en el libro: la SHORT de 037c y la LONG de 037e, 14,337 de margen cada una. El oráculo permaneció en silencio, por lo que la ruta de reembolso calculó pasivos de 1.8Q = 28,674 frente a activos idealizados de 1.0Q = 15,930 y reportó un déficit de 0.8Q = 12,744. Ese déficit existe únicamente dentro de la contabilidad de la ruta de reembolso. Bajo un único precio S, el pago de cada lado depende solo de S y no de dónde entró: el lado corto recibe (1 - S) * Q y el largo S * Q, sumando exactamente la Q del fondo. La ruta de reembolso paga en cambio según el precio de entrada de cada lado, por lo que las entradas no se cancelan: al lado corto se le pagó (1 - 0.10) * Q y al largo 0.90 * Q, 14,337 cada uno. El lado corto cobró todo su margen como si el precio nunca se hubiera movido de 0.10, la misma 0.8Q que el atacante ya había retirado en el Paso 3.

  • Paso 5: PayDeficitFromInsuranceFund() liquidó el déficit moviendo 12,744,000,000 unidades sin procesar de INJ fuera del fondo colisionante y acreditando 12,744 USDC al fondo del mercado. Con el déficit reportado como cubierto, se omitió el recorte de pérdida socializada en las posiciones restantes y a cada posición se le reembolsó su margen completo. El déficit en sí no le cuesta nada a nadie: se cubre con el fondo de seguro del mercado o, en su defecto, con el recorte. Lo que hizo rentable a este caso es que el fondo vinculado al mercado poseía polvo de INJ en lugar de USDC.

  • Paso 6: En la transacción 0xcb33ad...152eff en el bloque 181024803, el atacante retiró 43,010,985,663 unidades sin procesar de USDC, o 43,010.99 USDC, frente a los 30,267.02 USDC depositados. La ganancia neta es de 12,743.97 USDC, aproximadamente 21 segundos desde la primera transacción hasta la última.

El ciclo anterior es una ronda representativa. El atacante lo repitió a través de 299 mercados de opciones binarias de corta duración creados durante una ventana de 19 horas, cada uno vinculado a un oráculo configurado para nunca publicar un precio y cada uno con marcas de tiempo de vencimiento y liquidación separadas por segundos [2]. Las ganancias netas de esas rondas suman los aproximadamente $4.8M perdidos en el incidente.

Conclusión

Este incidente combina una colisión de identificadores con una verificación de denominación faltante en la ruta de pago. Un fondo está destinado a respaldar el mercado cuya identidad coincide con la suya. Pero un ID construido uniendo campos de identidad de extremo a extremo ya no registra dónde termina un campo y comienza el siguiente, por lo que dos conjuntos diferentes de campos pueden producir el mismo ID, y el emparejamiento vincula a un fondo con un mercado con el que en realidad no coincide. Nada aguas abajo detecta la discrepancia, porque la ruta que recurre a un fondo de seguro para cubrir un déficit de mercado compara cantidades y nunca denominaciones. El atacante usó los dos defectos juntos para fabricar un déficit fantasma dentro de un mercado que controlaba, liquidarlo con un saldo de fondo que valía una fracción de centavo, y llevarse un reembolso completo de márgenes que el fondo nunca poseyó.

De manera más general, un identificador que transmite significado debe derivarse de una codificación que preserve la estructura, con separadores explícitos o prefijos de longitud en cada campo de longitud variable, de modo que dos tuplas de campos distintas no puedan mapearse al mismo resumen.

Comience con Phalcon Explorer

Sumérjase en las Transacciones para Actuar con Sabiduría

Pruébelo ahora gratis

Más Incidentes Esta Semana

Ankr FLOW

El 31/08/2026, el servicio de staking líquido de Ankr en Flow EVM fue explotado. Ankr emite dos tokens diferentes contra el FLOW en staking, el token nativo de la red, y cada uno se acuña a través de su propio punto de entrada. Una de esas rutas había sido desactivada, pero una segunda forma de acceder a ella omitía la verificación que hacía cumplir la pausa, y la tasa de conversión en esa ruta se había vuelto obsoleta en el ínterin. El atacante acuñó a través de ella mucho más barato de lo que los mismos tokens podían redimirse, y luego hizo circular la brecha a través del búfer de redención de Ankr, un pool de Uniswap V3 y el protocolo de préstamos MORE Markets. Alrededor de 15.5M WFLOW (FLOW envuelto), que valían aproximadamente $410K en ese momento, fueron drenados de la reserva de MORE Markets, y el atacante obtuvo aproximadamente $246K después del deslizamiento [3].

Antecedentes

Ankr FLOW es un servicio de staking líquido en Flow EVM. FlowStakingPool reenvía FLOW a Cadence para el staking de validadores y representa las posiciones resultantes a través de dos tokens: el token de certificado sin rebase ankrFLOW, y el token portador con rebase aFLOWEVMb, que a su vez está respaldado por ankrFLOW.

Los dos tokens tienen puntos de entrada separados. La ruta de certificados pasa por stakeCerts() y unstakeCerts() hacia _stakeCerts() y _unstakeCertsFor(); la ruta portadora pasa por stakeBonds() y unstakeBonds() hacia _stakeBonds() y _unstakeBondsFor(). La acuñación en la ruta portadora tiene un segundo punto de entrada externo, stakeBondsWithCode(), que toma un código de socio para el programa de referidos de Ankr y luego llama al mismo _stakeBonds() interno. Cada ruta lee su propia entrada del InternetBondRatioFeed, el contrato que publica la relación de conversión entre FLOW y cada token.

FlowStakingPool también mantiene un búfer de FLOW para redenciones inmediatas. Más allá del pool, ankrFLOW se negociaba en un pool de Uniswap V3 ankrFLOW/WFLOW y era aceptado como colateral en MORE Markets, un protocolo de préstamos al estilo Aave V3. MORE Markets limita el préstamo a una relación préstamo-valor (LTV) establecida por activo y ofrece categorías de e-mode (modo de eficiencia): grupos de activos que se espera que se muevan juntos en precio, para los cuales se aplica un LTV más alto una vez que un prestatario habilita la categoría.

Análisis de la Vulnerabilidad

El contrato defectuoso es FlowStakingPool (0xfe81...287a), que acuña el token de certificado ankrFLOW (0x1b97...14bdb) y el token portador aFLOWEVMb (0xd6fd...f8d4a) contra relaciones leídas del InternetBondRatioFeed (0x3201...de38f).

Dos defectos se superponen. Primero, stakeBondsWithCode() llega a _stakeBonds() sin el modificador bondStakingUnpaused que stakeBonds() hace cumplir, por lo que la ruta del token portador permaneció accesible después de que fue deshabilitada. Segundo, a través de 71 lotes de actualización semanal de relaciones entre el 29 de abril de 2025 y el 27 de agosto de 2026, solo se actualizó la entrada activa de ankrFLOW, dejando la entrada de aFLOWEVMb en 1.0.

Las dos relaciones, por lo tanto, valoraban el mismo staking subyacente de manera diferente:

Dirección Funciones Relación Conversión
Acuñación _stakeCerts() / stakeCerts() 0.833437 1 FLOW a 0.833437 ankrFLOW
Acuñación _stakeBonds() / stakeBondsWithCode() 1.0 1 FLOW a 1 aFLOWEVMb
Redención _unstakeCertsFor() / unstakeCerts() 0.833437 1 ankrFLOW a ~1.19985 FLOW
Redención _unstakeBondsFor() / unstakeBonds() 1.0 1 aFLOWEVMb a 1 FLOW

Debido a que aFLOWEVMb está respaldado por ankrFLOW, la acuñación a través de la ruta portadora produjo un ankrFLOW respaldado por cada FLOW depositado, mientras que la ruta de certificados producía 0.833437. La redención a través de la ruta de certificados todavía pagaba ~1.19985 FLOW por ankrFLOW.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0x2b2e6e...3f66c9.

  • Paso 1: El atacante obtuvo capital inicial con un viaje de ida y vuelta entre las dos rutas. Tomó un préstamo flash de 5,000 ankrFLOW del pool de Uniswap V3 ankrFLOW/WFLOW, lo redimió a través de unstakeCerts() por ~5,999.25 FLOW, depositó 5,000.50 FLOW a través de stakeBondsWithCode() para acuñar 5,000.50 aFLOWEVMb respaldado por la misma cantidad de ankrFLOW, y llamó a unlockShares() para liberar ese ankrFLOW y reembolsar el préstamo y su prima de 0.50 ankrFLOW. Aproximadamente 998.75 FLOW permaneció como capital de trabajo.

  • Paso 2: El atacante repitió un intercambio de Uniswap V3 50 veces. Cada ciclo intercambiaba ankrFLOW por WFLOW con un límite de precio, acuñaba el ankrFLOW adeudado al pool dentro del callback de intercambio enrutando FLOW a través de stakeBondsWithCode() y unlockShares(), y luego desenvolvía el WFLOW resultante para el siguiente ciclo. A través de los 50 ciclos, el pool recibió ~38,634,755.38 ankrFLOW y pagó ~46,265,167.78 WFLOW, elevando el saldo del atacante de ~998.75 FLOW a ~7,631,411.14 FLOW.

  • Paso 3: El atacante convirtió ~38,601.95 FLOW a través de la ruta portadora y redimió el ankrFLOW resultante a través de unstakeCerts(), drenando el búfer de redención de ~46,316.57 FLOW que FlowStakingPool todavía poseía y agregando ~7,714.62 FLOW.

  • Paso 4: El atacante se dirigió a MORE Markets. Habilitó la categoría e-mode 1 ("Wrapped native tokens"), que trataba a ankrFLOW y WFLOW como activos de FLOW correlacionados y elevó el LTV de ankrFLOW del 78.5% al 97%. Depositó ~7,639,125.76 FLOW a través de stakeBondsWithCode(), desbloqueó el ankrFLOW correspondiente, lo suministró como colateral, y tomó prestado ~5,668,483.10 WFLOW. Desenvolver ese préstamo y enviarlo de vuelta a través de la misma ruta (una sola ronda de préstamo en bucle) agregó una cantidad igual de ankrFLOW, elevando el colateral a ~13,307,608.86 ankrFLOW y respaldando un segundo préstamo de ~9,819,641.05 WFLOW, para una deuda total de ~15,488,124.15 WFLOW, aproximadamente el 97% del valor del colateral a la relación de oráculo de ~1.19985 WFLOW.

El colateral del Paso 4 devolvió más de lo que costó acuñar: un FLOW acuñaba un ankrFLOW en la ruta portadora, el oráculo valoraba ese ankrFLOW en ~1.19985 WFLOW, y el e-mode permitía prestar el 97% contra él, por lo que los ~13.31M ankrFLOW suministrados respaldaron ~15.49M WFLOW de deuda, ~1.16 WFLOW por cada FLOW invertido. El atacante recicló solo una vez porque el segundo préstamo dejó vacía la reserva de WFLOW.

Ese ~15.49M WFLOW es el préstamo bruto, y es el 15.5M WFLOW reportado como drenado de la reserva de MORE Markets. Aproximadamente 5.67M WFLOW de esa cantidad fue desenvuelta y reciclada en colateral adicional en lugar de conservarse como ganancias líquidas; una vez que el segundo préstamo de ~9,819,641.05 WFLOW fue desenvuelto, el atacante retuvo ~9,819,641.05 FLOW como el retiro final. Atribuido según de dónde provino el valor, ~7,630,412.39 FLOW de ese retiro final provino del pool de Uniswap V3, ~8,713.37 FLOW de FlowStakingPool, y ~2,180,515.29 FLOW de MORE Markets.

Conclusión

Una verificación de estado que protege un punto de entrada pero no a su hermano es la causa raíz aquí: una ruta de acuñación que había sido deshabilitada permaneció invocable, y su relación quedó sin actualizar durante dieciséis meses de actualizaciones semanales. Cada FLOW enrutado a través de ella, por lo tanto, producía ankrFLOW a un precio que la ruta de certificados nunca habría ofrecido, y el atacante hizo circular esa discrepancia a través de un pool de Uniswap V3, el búfer de redención del pool de staking, y un mercado de préstamos, convirtiendo ankrFLOW barato en liquidez de WFLOW en cada parada.

Las protecciones de pausa deben aplicarse en cada punto de entrada que llegue a la lógica deshabilitada, no solo en aquel que se espera que sea llamado, y un feed de relación que sirve a una ruta inactiva debe mantenerse actualizado o hacerse revertir. Los protocolos de préstamos también deberían evitar otorgar un LTV de e-mode alto a un token de staking líquido cuyo precio de acuñación se establece independientemente de su precio de oráculo; monitorear el costo de acuñación, el valor de redención y el precio de oráculo juntos revelaría esa divergencia.


Aquifer

El 31/08/2026, Aquifer, un AMM de creador de mercado propietario en Solana, fue explotado por aproximadamente $2.47M a través de 212 intercambios exitosos, distribuidos en USDC, USDT, HYPE, cbBTC, CASH y trece tokens más [4]. Cada intercambio se liquida como dos transferencias de tokens, una en cada dirección, y Aquifer permitía que quien llamaba eligiera qué programa realizaría cada una de ellas sin verificar nunca esa elección. El atacante nombró un programa propio para la transferencia que debería haber pagado a Aquifer, y este reportó éxito sin mover nada; la transferencia en la otra dirección se ejecutó a través del Token Program genuino y entregó activos reales de las bóvedas de Aquifer.

Antecedentes

Aquifer es un Prop AMM, lo que significa que un creador de mercado profesional aporta su propio inventario y mantiene cotizaciones de compra y venta, en lugar de fijar precios de intercambios a lo largo de una curva de producto constante como lo hacen los pools al estilo Uniswap V2. Aquifer deriva un precio de su cotización y estado de riesgo, y luego liquida la operación entre las Token Accounts del usuario y sus propias bóvedas.

En Solana, un Token Program es un programa ejecutable que implementa operaciones como transferencia, acuñación y quema. Tokenkeg es el Token Program SPL original y Token-2022 es su sucesor extensible; cada uno gestiona muchos tokens diferentes en lugar de un solo activo. Una Mint Account identifica un tipo de token y almacena su suministro, decimales y autoridades, mientras que una Token Account almacena el saldo de un titular para una Mint; ambas son propiedad del Token Program que las gestiona. Las bóvedas de Aquifer son Token Accounts controladas por PDAs de Aquifer.

Un operador negocia invocando la instrucción swap de Aquifer y pasando las cuentas que tocará, incluyendo, para cada una de las dos transferencias, el Token Program que debería ejecutarla. Aquifer mueve esos tokens mediante invocación entre programas (CPI): construye una instrucción de transferencia y la entrega al programa nombrado por Instruction.program_id. Los datos con forma de instrucción de transferencia solo mueven tokens SPL reales cuando el Token Program correcto la ejecuta.

Análisis de la Vulnerabilidad

El programa defectuoso es Aquifer (AQU1FR...Tz45). No hay ningún código fuente publicado que coincida con su bytecode desplegado, por lo que el análisis a continuación se recupera del desensamblado del programa: los nombres fn_ etiquetan funciones internas por sus desplazamientos de código, y ningún nombre usado aquí, incluido swap, proviene de los desarrolladores. El programa contiene una función de lista de permitidos, fn_49740(), que solo acepta Tokenkeg y Token-2022, pero nada en la ruta swap la llama. Al manejar un swap, el programa construye ambas instrucciones de transferencia a través de fn_45f20() y fn_46bf8() usando una constante Tokenkeg fija, que necesariamente satisface su verificación interna, y luego sobrescribe el program_id de cada instrucción con el Token Program suministrado por quien llama antes de invocarla:

// Reconstrucción semántica del código que el programa ejecuta para un swap; construct_transfer()
// representa fn_45f20() y fn_46bf8(), y la lista de permitidos fn_49740() nunca se alcanza.
let checked_program = TOKENKEG_ID;

let mut output_instruction = construct_transfer(checked_program, output_accounts);
let mut input_instruction = construct_transfer(checked_program, input_accounts);

// Entradas del que llama sin verificar reemplazan el valor que fue verificado.
output_instruction.program_id = caller.token_program_b.key;
input_instruction.program_id = caller.token_program_a.key;

// Cada invoke() entrega la instrucción de transferencia a cualquier programa que el que llama haya nombrado.
invoke(output_instruction)?;
invoke(input_instruction)?;

Aquí TOKENKEG_ID denota TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA. El valor que pasa la validación, por lo tanto, nunca es el valor que se ejecuta. Agravando esto, el programa no verifica el monto recibido por la bóveda después de que la CPI de entrada regresa, por lo que una CPI que tiene éxito sin transferir nada se acepta como pago.

Análisis del Ataque

El incidente consta de 212 transacciones de ataque exitosas. El siguiente análisis se basa en la transacción 4pBV1G...T3bf, un ejemplo representativo.

  • Paso 1: El atacante invocó swap con una entrada nominal de 4,957.497101 USDC, para la cual Aquifer calculó una salida de 195,849.433667 KMNO. Para el tramo de KMNO suministraron Tokenkeg; para el tramo de USDC suministraron su propio programa DMBpPM...NRgb68 junto con una cuenta de entrada falsa 9gsKJc..., una cuenta propiedad de ese programa cuyos 165 bytes imitan una Token Account SPL que lleva la Mint de USDC, la autoridad del atacante, y un saldo de u64::MAX.

  • Paso 2: La CPI de salida llegó a Tokenkeg y transfirió 195,849.433667 KMNO fuera de la bóveda de KMNO de Aquifer a la Token Account del atacante EUjkGc..., que está controlada por el firmante 7fTe9p...4gRk7J.

  • Paso 3: La CPI de entrada llegó a DMBpPM...NRgb68 con datos con forma de transferencia de USDC. Ese programa devolvió éxito sin mover ningún USDC desde la fuente falsa 9gsKJc... a la bóveda genuina de USDC de Aquifer 7ULN1Y....
  • Paso 4: Aquifer aceptó ambos retornos de CPI, por lo que el intercambio se confirmó atómicamente.

Los cambios de saldo resultantes para esta transacción son los siguientes.

Cuenta Antes Después Cambio
Bóveda de KMNO de Aquifer 9BHsZp...FHSqG 604,968.018277 KMNO 409,118.584610 KMNO -195,849.433667 KMNO
Cuenta de KMNO del atacante EUjkGc... 0 KMNO 195,849.433667 KMNO +195,849.433667 KMNO
Bóveda de USDC de Aquifer 7ULN1Y... 1,620,342.341679 USDC 1,620,342.341679 USDC 0 USDC

Estas cifras describen únicamente esta transacción de ejemplo, no la pérdida total del incidente.

Conclusión

La causa raíz es una entrada del que llama sin validar en la ruta de liquidación, no manipulación de precios u oráculos: la ruta de intercambio validaba una constante de Token Program y luego la reemplazaba con una suministrada por quien llamaba antes de invocar la instrucción, por lo que qué programa ejecutaba la transferencia que debería haber pagado a Aquifer era elección de quien llamaba. Sin una verificación de saldo posterior a la transferencia en la bóveda, un programa que devuelve éxito sin mover tokens satisface el pago mientras que la transferencia en la otra dirección entrega activos reales.

Cada CPI debería vincularse al Token Program propietario de la Mint que se está moviendo, resuelto a partir de la Mint Account en lugar de tomarse de cuentas suministradas por quien llama, y el saldo de la bóveda debería leerse antes y después de la transferencia de entrada para que el intercambio revierta a menos que la bóveda realmente haya recibido el monto cotizado.


Notional Finance

El 2026/09/03-09/04 (UTC), Notional Finance V1 en Ethereum fue explotado por aproximadamente $1.73M, drenados como 69,257.37 DAI y 1,658,524.86 USDC. Antes de permitir que una cuenta asuma deuda, el protocolo valora todo lo que esa cuenta posee y debe, y una conversión numérica insegura en esa ruta colapsó una deuda del tamaño correcto a cero. La verificación, por lo tanto, dejó pasar a una cuenta cuyo pasivo había desaparecido, mientras que el gran reclamo que había creado permanecía intacto en otro contrato que el atacante controlaba. El atacante había programado ese reclamo forjado para que venciera en el siguiente vencimiento del protocolo, medianoche UTC, y lo liquidó minutos después para retirar el DAI y USDC que el protocolo todavía poseía.

Antecedentes

Notional Finance V1 es un protocolo de préstamos de tasa fija en Ethereum. Representa flujos de efectivo en vencimientos predefinidos con fCash: un CASH_RECEIVER es una posición positiva con derecho a recibir activos al vencimiento, y un CASH_PAYER es una posición negativa obligada a pagarlos. El fCash y otras posiciones de cada cuenta se rastrean en su Portfolio, donde un activo se identifica por su grupo de efectivo junto con su vencimiento; el grupo de efectivo fija la moneda en la que se liquida. El nocional de un solo activo, el monto por el que se liquida al vencimiento, es un uint128.

ERC1155Trade.safeTransferFrom() crea un par de fCash entre dos cuentas. La llamada tiene la forma de una transferencia ERC-1155, pero nada cambia de manos: llama a Portfolios.mintfCashPair() para crear dos posiciones que se compensan entre sí, una positiva para el receptor y una negativa igual para el pagador. Cada lado se escribe en el Portfolio correspondiente mediante _upsertAsset(), que combina una nueva posición en una entrada existente solo cuando el grupo de efectivo y el vencimiento coinciden, sumando los dos nocionales mediante una adición verificada con SafeUInt128.

La solvencia se verifica en el pagador mediante freeCollateral(), que combina los saldos de efectivo en Escrow de la cuenta con su valoración de Portfolio, sumando entradas por moneda en un int256 con signo. Cada saldo de moneda se convierte luego a ETH a la tasa de cambio de esa moneda, con la tasa y el escalado de decimales aplicados como divisiones de enteros, y el colateral libre final debe ser no negativo. Al vencimiento, una posición de fCash se liquida en el saldo de efectivo de la cuenta mediante Escrow.portfolioSettleCash(), y un saldo de efectivo positivo puede entonces retirarse del Escrow como el activo subyacente correspondiente.

Análisis de la Vulnerabilidad

Los contratos defectuosos son el punto de entrada ERC1155Trade (0xbba8...ef08), que acuña pares de fCash sin un límite nocional, y el Escrow (0x9abd...f683), cuya valoración de colateral tiene dos defectos aritméticos en _convertToETH() que pueden valorar una deuda en cero.

Primero, una conversión de estrechamiento sin verificar puede truncar una deuda grande a cero. La función convierte balance.abs() directamente a uint128 con un cast sin procesar y nunca verifica que el valor entre en el tipo objetivo. Los dos tipos no están ni cerca uno del otro: un int256 con signo llega hasta 2^255 - 1, mientras que uint128 se detiene en 2^128 - 1. Un saldo de exactamente -2^128 por lo tanto cabe cómodamente dentro de int256, y balance.abs() produce 2^128 intacto. Es el cast el que falla: 2^128 cae un paso más allá de lo que uint128 puede contener y da la vuelta a 0, por lo que la valoración que sigue opera sobre un saldo cero y toda la deuda desaparece del cálculo de colateral libre. La misma función usa SafeCast.toUint128() para la conversión posterior del valor de ETH calculado, que revertiría en un resultado fuera de rango, mientras que la conversión anterior sigue siendo un cast directo y trunca en silencio.

Segundo, la división de enteros puede redondear pequeñas deudas a cero. Las divisiones por er.rateDecimals y baseDecimals truncan sus residuos, por lo que una deuda suficientemente pequeña también se valora en 0 y se omite del colateral libre.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0xe1589a...25d60a.

  • Paso 1: A las 23:58:47 UTC del 3 de septiembre de 2026, el atacante llamó a safeTransferFrom() en ERC1155Trade para acuñar un par de fCash con un monto de 1, usando cashGroupId = 2 y la marca de tiempo de vencimiento 1788480000 (4 de septiembre de 2026, 00:00 UTC), 73 segundos antes y el más cercano de los dos vencimientos que el protocolo tenía abiertos. Durante la acuñación, _upsertAsset() registró el pasivo de fCash negativo en el contrato del atacante y el reclamo positivo en el contrato receptor, y Portfolios verificó inmediatamente el colateral libre del contrato del atacante. Ese contrato no poseía ningún saldo en ninguna moneda, por lo que cualquier valoración positiva del nuevo pasivo habría fallado la verificación. El segundo defecto lo cubrió: a la tasa de cambio actual, una deuda de 1 no sobrevive a las dos divisiones de enteros, por lo que se valoró en cero y la verificación pasó.

  • Paso 2: El atacante llamó a safeTransferFrom() nuevamente para acuñar un segundo par con un monto de uint128.max (340,282,366,920,938,463,463,374,607,431,768,211,455). Este par reutilizó cashGroupId = 2 pero llevaba la marca de tiempo de vencimiento 1796256000 (3 de diciembre de 2026, 00:00 UTC), el otro vencimiento abierto, y envió su lado positivo a un contrato receptor diferente. El lado negativo aterrizó en el contrato del atacante nuevamente, junto al del Paso 1. Una sola acuñación nunca puede exceder 2^128 - 1, siempre una unidad por debajo del valor que trunca, por lo que alcanzarlo requiere dos posiciones. El vencimiento diferente mantuvo a las dos en entradas separadas, fuera del alcance de la adición verificada que habría revertido en una fusión; el grupo de efectivo compartido aún las colocaba en la misma casilla de moneda al momento de la valoración, donde la unidad del Paso 1 llevó el total a exactamente -2^128.

  • Paso 3: Durante la verificación de colateral, el proxy de Escrow llamó a convertBalancesToETH(). El saldo negativo agregado alcanzó exactamente -2^128, se pasó a _convertToETH(), y se truncó a cero, por lo que el pasivo denominado en ETH de la cuenta se reportó como cero.

  • Paso 4: Con la deuda ausente de la contabilidad de colateral, el segundo contrato receptor poseía una gran posición positiva de fCash que el protocolo trataba como colateral disponible. Todavía dentro de la misma transacción, acuñó dos pares de fCash más contra ese colateral y entregó el lado positivo a otros dos contratos receptores: 69,257.37 bajo cashGroupId = 2, que se liquida en DAI, y 1,658,524.86 bajo cashGroupId = 3, que se liquida en USDC. Ambas cifras provinieron de llamadas balanceOf que el atacante había hecho en el Escrow al comienzo de la transacción, por lo que cada reclamo se dimensionó según un saldo que el Escrow realmente poseía. Cada uno llevaba el vencimiento 1788480000, 73 segundos después.

  • Paso 5: A las 00:01:35 UTC del 4 de septiembre de 2026, 95 segundos después de ese vencimiento, el atacante liquidó los dos reclamos vencidos en la transacción 0xc3f3e3...a24efa y retiró ~69,257.37 DAI y ~1,658,524.86 USDC del Escrow. Los fondos fueron reenviados más tarde a 0x8aaf...3be6.

Conclusión

La ruta de acuñación no impone ningún límite nocional, y dos defectos aritméticos en la valoración de colateral cada uno permitió que una verificación de solvencia pasara: el redondeo permitió que una cuenta que no poseía nada asumiera su primer pasivo, y luego una conversión de estrechamiento silenciosa valoró una deuda del tamaño exacto correcto en cero, por lo que la segunda verificación pasó en una cuenta que estaba profundamente insolvente. La posición positiva forjada en el contrato receptor se trató entonces como colateral disponible, lo que permitió al atacante dividirla en reclamos que coincidían con los saldos del Escrow y retirar el DAI y USDC que todavía poseía.

Los saldos con signo deben verificarse en rango antes de cualquier conversión de estrechamiento, y una conversión que no puede representar su entrada debería revertir en lugar de truncar. De manera más general, una verificación de solvencia nunca debería reportar una deuda distinta de cero como cero, ya sea que la aritmética la pierda por un cast o por un paso de redondeo. Aplicar el cast verificado que la misma función ya usa más adelante, o limitar el nocional que un solo par de acuñación puede crear, cada uno habría detenido este exploit por sí solo.

Comience con Phalcon Security

Detecte cada amenaza, alerte lo que importa y bloquee ataques.

Pruébelo ahora gratis

Referencias

[1] https://github.com/InjectiveFoundation/injective-core/commit/b994d6b603eb53a38312e547f7bbe3c69d40495b

[2] https://mpost.io/injective-exploited-for-4-9m-via-market-id-collision-in-binary-options-settlement-logic/

[3] https://x.com/flow_blockchain/status/2094506622429307061

[4] https://bitquery.io/investigations/aquifer-solana-hack-2-5-million

Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento normativo cripto. Construimos productos y servicios que ayudan a los clientes a realizar auditorías de código (incluyendo contratos inteligentes, blockchain y billeteras), interceptar ataques en tiempo real, analizar incidentes, rastrear fondos ilícitos, y cumplir con las obligaciones de AML/CFT, a lo largo de todo el ciclo de vida de los protocolos y plataformas.

BlockSec ha publicado múltiples artículos de seguridad blockchain en conferencias prestigiosas, ha reportado varios ataques de día cero de aplicaciones DeFi, ha bloqueado múltiples ataques para rescatar más de 20 millones de dólares, y ha protegido miles de millones en criptomonedas.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit