Back to Blog

Resumen semanal de incidentes de seguridad Web3 | 9 – 15 de mar. de 2026

March 18, 2026
26 min read

Durante la semana pasada (2026/03/09 - 2026/03/15), BlockSec detectó y analizó ocho incidentes de ataque, con pérdidas totales estimadas de ~$1.66M. La tabla a continuación resume estos incidentes, y los análisis detallados de cada caso se presentan en las subsecciones siguientes.

Fecha Incidente Tipo Pérdida Estimada
2026/03/09 Incidente EtherFreakers Lógica de negocio defectuosa ~$25K
2026/03/10 Incidente Alkemi Lógica de negocio defectuosa ~$89K
2026/03/10 Incidente MT Lógica de negocio defectuosa ~$242K
2026/03/11 Incidente de Liquidación AAVE Configuración incorrecta ~$1.01M
2026/03/11 Incidente Planet Finance Lógica de negocio defectuosa ~$10K
2026/03/12 Incidente AM Lógica de negocio defectuosa ~$131K
2026/03/12 Incidente DBXen Lógica de negocio defectuosa ~$149K
2026/03/15 Incidente Goose Finance Lógica de negocio defectuosa ~$8K

Incidente EtherFreakers

Resumen Breve

El 9 de marzo de 2026, EtherFreakers, un juego NFT en Ethereum, fue explotado debido a un doble conteo incorrecto, resultando en una pérdida de ~$25K. Cada NFT en el juego mantiene un saldo ETH retirable (llamado "energía"). Como mecánica del juego, los jugadores pueden usar attack() para que un NFT capture a otro y reclame la energía del objetivo. Sin embargo, el contrato paga el saldo del objetivo pero transfiere el NFT antes de liquidar su contabilidad. Un hook de transferencia entonces lee datos obsoletos previos al pago y alimenta parte de ellos de vuelta a un grupo de dividendos global, inflando el grupo sin respaldo de nuevo ETH. El explotador repitió en bucle esta mecánica de captura para inflar el índice global y luego drenó el saldo inflado de un lote de NFTs.

Antecedentes

EtherFreakers es un juego NFT en cadena donde cada NFT (llamado "Freaker") mantiene un saldo de ETH retirable llamado energy. El sistema funciona como un grupo de dividendos: cuando ocurren ciertas acciones, una fracción de ETH se distribuye proporcionalmente entre todos los Freakers. El ETH reclamable de cada Freaker se rastrea mediante un acumulador global freakerIndex combinado con un peso de participación por token fortune.

Concretamente, la fórmula de contabilidad es: energyOf = basic + (freakerIndex - index) * fortune. El freakerIndex aumenta cuando se ejecuta _dissipateEnergyIntoPool(amount), distribuyendo el 80% de amount a todos los Freakers y el 20% a los creadores. Los depósitos directos mediante charge() solo incrementan basic sin afectar freakerIndex. Por lo tanto, los incrementos en freakerIndex siempre deberían estar respaldados por Ether real que ingresa al sistema. Si freakerIndex crece sin la entrada de ETH correspondiente, los Freakers pueden canjear más Ether del que el contrato realmente posee.

Análisis de Vulnerabilidad

La causa raíz es un orden de ejecución incorrecto en el contrato EtherFreak (0x3A27...c0f33). Cuando una captura tiene éxito, la función attack() ejecuta estos pasos en orden:

  1. Línea 237: Pagar targetCharge (la energía total del NFT objetivo) al defensor como una transferencia directa de ETH. La energía ya ha sido gastada.
  2. Línea 240: Llamar a _transfer(defender, capturer, targetId) para mover el NFT. Internamente, _transfer() invoca el hook ERC-721 _beforeTokenTransfer(), que llama a _dissipateEnergyIntoPool() con el 0.1% de energyOf(targetId). Esta es la primera llamada a _dissipateEnergyIntoPool(), y lee un valor obsoleto porque el paso 5 aún no ha ocurrido.
  3. Línea 241: Llamar a _dissipateEnergyIntoPool(sourceSpent) explícitamente. Esta es la segunda llamada, que forma parte de la lógica normal del juego.
  4. Líneas 244-251: Actualizar energyBalances tanto para sourceId como para targetId.

El error está en el paso 2: dado que energyBalances[targetId] aún no ha sido actualizado, el hook todavía ve el saldo previo al pago y alimenta parte de la energía ya gastada al grupo de dividendos. El pago directo de ETH en el paso 1 y la entrada al grupo en el paso 2 se originan de la misma energía, inflando freakerIndex sin respaldo de nuevo Ether.

freakerIndex crece cada vez que se llama a _dissipateEnergyIntoPool():

Análisis del Ataque

El siguiente análisis se basa en la transacción 0x89e24d...9abd2942.

  • Paso 1: Tomar prestado 1,700 WETH.

  • Paso 2: Acuñar dos nuevos Freakers, token 590 y token 591, bajo direcciones controladas por el explotador.

  • Paso 3: Llamar repetidamente a la función attack(590, 591) del juego y mantener las ejecuciones en la rama de captura exitosa.

  • Paso 4: Después de cada éxito, transferir el token 591 de vuelta al asistente para que el mismo par pueda reutilizarse.

  • Paso 5: Cada bucle exitoso infla freakerIndex más allá del Ether realmente conservado por el sistema.

  • Paso 6: Una vez que el índice es suficientemente alto, descargar un lote de Freakers previamente controlados. Los IDs de token 496 al 520 son descargados individualmente por 0.278052246002402082 Ether cada uno.

  • Paso 7: Envolver el Ether drenado en WETH, repagar el préstamo flash de 1,700 WETH, y conservar aproximadamente 7.498 WETH como ganancia.

Conclusión

La causa raíz está en el flujo de captura exitosa de attack(): EtherFreakers paga targetCharge antes de que el estado de energía del token objetivo sea liquidado. _transfer() luego activa _beforeTokenTransfer(), que lee el obsoleto energyOf(targetId) previo al pago y disipa parte de él en el grupo. Esto incrementa freakerIndex sin respaldo de nuevo Ether, por lo que la misma energía objetivo se cuenta tanto como pago como entrada al grupo. Este es un error de inflación de lógica de negocio, no un error de reentrada.

Para reducir riesgos similares en el futuro:

  • Evitar recalcular valores económicos a partir de estado mutable dentro de hooks de transferencia mientras la misma transacción aún se está liquidando.

  • Si el hook de transferencia lee una variable de estado, asegurarse de que el orden de ejecución no afecte el resultado (por ejemplo, liquidar el estado antes de que se ejecute el hook, no después).


Incidente Alkemi

Resumen Breve

El 10 de marzo de 2026, el protocolo Alkemi en Ethereum fue explotado, resultando en una pérdida de ~$89K. La causa raíz es un error de contabilidad y lógica de negocio defectuosa. La lógica de liquidación defectuosa permite que cualquiera liquide su propia posición dentro de la misma transacción y obtenga ganancias. Además, un error de contabilidad provoca que la deducción de la garantía propia del atacante durante la liquidación sea sobreescrita, permitiendo al atacante obtener recompensas de liquidación sin asumir el costo previsto.

Antecedentes

Alkemi es un protocolo de préstamos. Cuando la posición de un prestatario se vuelve insuficientemente garantizada, cualquiera puede llamar a liquidateBorrow() para repagar parte de la deuda y tomar la garantía con descuento. Para evitar liquidaciones excesivas, el protocolo limita el monto reembolsable por transacción al mínimo de tres valores:

  1. El saldo de préstamo actual del prestatario (currentBorrowBalance_TargetUnderwaterAsset).
  2. El máximo reembolso que puede cubrir la garantía del prestatario después de aplicar el descuento de liquidación (calculateDiscountedBorrowDenominatedCollateral()).
  3. El monto de reembolso necesario para devolver la cuenta al límite de liquidación (calculateDiscountedRepayToEvenAmount()), verificado solo cuando el mercado isSupported.

Análisis de Vulnerabilidad

La causa raíz es la lógica de negocio defectuosa y un error de contabilidad en el protocolo Alkemi (0x4822...a888). Dado que currentBorrowBalance_TargetUnderwaterAsset es necesariamente mayor que 0 siempre que el prestatario tenga una deuda pendiente, y el valor devuelto por calculateDiscountedBorrowDenominatedCollateral() es también necesariamente mayor que 0 siempre que el prestatario tenga garantía, el protocolo AlkemiEarnPublic depende efectivamente de calculateDiscountedRepayToEvenAmount() para determinar si un préstamo dado puede ser liquidado. En esta función, el monto de la deuda que necesita ser liquidada debería calcularse en base a una variable llamada accountShortfall_TargetUser.

Sin embargo, en la implementación real, la función usa en cambio una variable global closeFactorMantissa para calcular un límite superior sobre el monto de deuda que se permite repagar, y devuelve este valor. Como resultado, un atacante puede pedir prestado e inmediatamente liquidar su propia posición dentro de la misma transacción.

Además, en la función liquidateBorrow(), cuando el liquidador y el prestatario son la misma dirección, las variables supplyBalance_TargetCollateralAsset y supplyBalance_LiquidatorCollateralAsset apuntan al mismo slot de almacenamiento. La función entonces calcula un "saldo reducido" y un "saldo recompensado" por separado en base al mismo saldo inicial, y posteriormente los escribe de vuelta al mismo slot de almacenamiento en secuencia. Debido a que el saldo reducido se escribe primero y el saldo recompensado se escribe después, el efecto de reducción es sobreescrito y se pierde, dejando solo el resultado recompensado. Esto permite al atacante amplificar aún más su ganancia.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0xa170...6d9d.

  • Paso 1: El atacante toma un préstamo flash de 51e18 WETH de Balancer.

  • Paso 2: El atacante desenvuelve 51e18 WETH en ETH y lo suministra al protocolo Alkemi.

  • Paso 3: El atacante toma prestado 39.5e18 ETH de Alkemi.

  • Paso 4: El atacante liquida su propia posición usando 39.5395e18 ETH.

  • Paso 5: El atacante retira 93.5e18 ETH de Alkemi.

  • Paso 6: El atacante repaga el préstamo flash y obtiene una ganancia de 43.4e18 ETH.

Conclusión

La causa raíz es que la lógica de liquidación defectuosa permite al atacante pedir prestado y luego liquidar su propia posición dentro de la misma transacción para obtener ganancias, mientras que la lógica de contabilidad incorrecta amplifica aún más las ganancias del atacante.

Para reducir riesgos similares en el futuro:

  • Para las actualizaciones de saldo del prestatario y del liquidador, el protocolo debería operar directamente sobre las variables de almacenamiento en lugar de copiar los saldos en variables de memoria temporal para cálculo y escritura por separado.

Incidente MT

Resumen Breve

El 10 de marzo de 2026, MT Token, un token deflacionario en BNB Chain, fue explotado, resultando en una pérdida de ~$242K. La causa raíz es una lógica de restricción de trading defectuosa combinada con un manejo inconsistente de condiciones especiales de transferencia. Durante la fase de deflación, el contrato restringe las operaciones de compra cuando la reserva del pool supera un umbral fijo. Sin embargo, el contrato trata las transferencias de montos exactos (por ejemplo, 2e17 MT) como acciones de vinculación de referidos, lo que permite al atacante eludir la restricción de compra y adquirir tokens iniciales. Además, la lógica de restricción depende de una detección de ruta incompleta (isBuy) y no cubre rutas de intercambio indirectas como Pair a Router, mientras que las verificaciones de lista blanca cortocircuitan aún más las validaciones críticas. El atacante acumuló tokens MT sin activar restricciones ni comisiones, manipuló pendingBurnAmount mediante operaciones de liquidez controladas y trades, y forzó al pool a un estado anormal donde el precio del token fue artificialmente inflado.

Antecedentes

MT Token es un token deflacionario en BNB Chain con restricciones de trading incorporadas. Durante la fase de deflación, el contrato bloquea las operaciones de compra cuando la reserva de MT en el pool supera 21,000e18. Una vez que la reserva cae por debajo de este umbral, la fase de deflación termina y las compras se rehabilitan. MT Token también incluye un mecanismo de referidos: una transferencia de exactamente 2e17 MT o 1e17 MT se trata como una acción de vinculación de referidor en lugar de un trade normal.

Análisis de Vulnerabilidad

La causa raíz fue un diseño defectuoso de restricción de compras en el contrato MT (0x037E...b449). En condiciones normales, los atacantes no deberían poder adquirir MT como capital semilla durante la fase restringida. Sin embargo, el contrato trata una transferencia de exactamente 2e17 MT como una acción de vinculación de referidos en lugar de una compra, lo que permite a un atacante comprar 2e17 MT eludiendo la restricción de compra.

Adicionalmente, la restricción de trading depende de la rama isBuy para bloquear compras, pero no cubre la ruta "Pair a Router". Dado que tanto el Router como el Pair son direcciones de lista blanca, tales transferencias cortocircuitan en la verificación de lista blanca y nunca alcanzan la lógica de restricción de compras, permitiendo a un atacante adquirir MT enrutando compras al Router y posteriormente extrayendo tokens de vuelta hacia sí mismos eliminando liquidez.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0xfb57...fca6.

  • Paso 1: El atacante tomó un préstamo flash de ~358,681e18 WBNB.

  • Paso 2: El atacante compró 2e17 MT, eludiendo así la restricción de compra.

  • Paso 3: El atacante suministró 4e12 WBNB y 2e17 MT al Pair para agregar liquidez. Esta transferencia eludió la lógica de cobro de comisiones por la misma razón mencionada anteriormente.

  • Paso 4: El atacante compró ~10,000,000e18 tokens MT del Pair al Router, eludiendo tanto la restricción de compra como la lógica de cobro de comisiones.

  • Paso 5: El atacante eliminó la mitad de su posición de liquidez, extrayendo todos los tokens MT en poder del Router en el proceso, y luego vendió el MT recuperado por WBNB. En este paso, pendingBurnAmount fue manipulado a aproximadamente 9,000,000e18.

  • Paso 6: El atacante compró nuevamente ~10,000,000e18 tokens MT, reduciendo la reserva de MT del pool a ~6,756,516e18, que era menor que pendingBurnAmount.

  • Paso 7: El atacante eliminó la mitad restante de su posición de liquidez, retiró los tokens MT comprados, y luego llamó a distributeDailyRewards() para quemar MT del pool. Como resultado, la reserva de MT se redujo a 21,000e18.

  • Paso 8: El atacante intercambió todo el MT de vuelta por ~1,198e18 WBNB, repagó el préstamo flash y finalizó la ganancia.

Conclusión

Este exploit fue causado por restricciones de trading incorrectas, que permitieron al atacante eludir la prohibición de compra adquiriendo exactamente BINDING_AMOUNT de tokens MT. Tras obtener tokens MT, el atacante pudo además eludir tanto la lógica de cobro de comisiones como la restricción de compra agregando primero liquidez, luego comprando MT hacia el Router, y finalmente eliminando liquidez para recuperar los tokens. El atacante luego acumuló pendingBurnAmount mediante operaciones de venta y ejecutó la quema para llevar las reservas del pool a un estado anormal, lo que le permitió vender MT a un precio artificialmente inflado y obtener ganancias.

Para reducir riesgos similares en el futuro:

  • Aplicar una separación estricta entre la semántica de transferencia y la lógica de trading.

Incidente de Liquidación AAVE

Resumen Breve

El 11 de marzo de 2026, AAVE sufrió $21M en liquidaciones incorrectas en Ethereum, resultando en una pérdida de ~$1.01M. La causa raíz fue un precio oracle incorrecto para wstETH, que causó que posiciones originalmente saludables se volvieran insuficientemente garantizadas. Como resultado, las posiciones de los usuarios fueron liquidadas, causando pérdidas financieras.

Antecedentes

AAVE usa adaptadores oracle para valorar activos envueltos como wstETH. El adaptador CAPO (Oracle de Precio con Tope) deriva el precio de wstETH multiplicando el precio base ETH/USD por una tasa de conversión (getRatio(), es decir, cuánto ETH vale un wstETH). Para prevenir la manipulación de la tasa, CAPO aplica un tope de crecimiento basado en instantáneas:

maxRatio = snapshotRatio + maxGrowthPerSecond x (currentTime - snapshotTimestamp)

y limita la salida de getRatio() durante la valoración (si currentRatio > maxRatio, usa maxRatio). Este mecanismo limita efectivamente la deriva máxima ascendente de la tasa y el precio resultante.

Análisis de Vulnerabilidad

La causa raíz fue una discrepancia tiempo-ratio en la configuración del ancla oracle CAPO (0xe1D9...61Ef): el timestamp de la instantánea y el ratio de la instantánea fueron establecidos, pero el ratio de la instantánea fue configurado por debajo del ratio real de wstETH/ETH. Como resultado, el maxRatio calculado por el adaptador cayó por debajo del ratio en vivo y limitó getRatio() hacia abajo, subvalorando sistemáticamente el precio oracle de wstETH/USD. Esta depresión en la valoración de la garantía redujo el factor de salud de las posiciones que usaban wstETH como garantía, causando que cuentas de otro modo saludables fueran clasificadas incorrectamente como no saludables y liquidadas.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0x9064...8a9c.

  • Paso 1: El liquidador tomó un préstamo flash de ~6304e18 WETH y liquidó al prestatario.

  • Paso 2: El liquidador repagó el préstamo flash, completando la liquidación.

Conclusión

Esta liquidación fue causada por una configuración incorrecta del precio oracle, que llevó incorrectamente a los prestatarios que deberían haber permanecido saludables a un estado no saludable, desencadenando así la liquidación de sus posiciones.

Para reducir riesgos similares en el futuro:

  • Asegurarse de que los parámetros críticos sean verificados en cuanto a su corrección antes de cada actualización.

  • Agregar verificaciones de validación en la implementación para rechazar parámetros incorrectos y evitar que configuraciones incorrectas se apliquen con éxito.


Incidente Planet Finance

Resumen Breve

El 11 de marzo de 2026, Planet Finance fue explotado en BNB Chain, resultando en una pérdida estimada de ~$10K. La causa raíz fue que el protocolo trató erróneamente los incrementos en el saldo de préstamo almacenado de un prestatario como intereses acumulados, permitiendo a un atacante pedir prestado repetidamente y activar la liquidación de descuentos para subestimar su deuda registrada.

Antecedentes

Planet Finance es un protocolo de préstamos que permite a los prestatarios repagar con un descuento de intereses. El descuento está escalonado y es determinado por la relación entre el GAMMA apostado por un usuario y su valor apostado en otros activos: cuanto mayor sea esta relación, mayor será el descuento de reembolso. El esquema de descuentos comprende tres niveles, que van del 0% (mínimo) al 50% (máximo).

Análisis de Vulnerabilidad

La causa raíz fue que al liquidar el descuento de un prestatario en changeUserBorrowDiscount(), el protocolo (0x4c9E...F467) trató erróneamente el incremento en el saldo de préstamo almacenado del prestatario como intereses recién acumulados. Como resultado, el descuento destinado a aplicarse solo a los intereses acumulados fue incorrectamente aplicado al principal recién prestado, reduciendo indebidamente la deuda registrada del prestatario. Un atacante podría realizar repetidamente el bucle de borrow seguido de changeUserBorrowDiscount para acumular descuentos excesivos, causando que el pasivo registrado en cadena sea consistentemente menor que el monto real prestado, y finalmente obtener ganancias de la discrepancia.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0x5f45...5ec9.

  • Paso 1: El atacante tomó un préstamo flash de 200,000e18 USDT.

  • Paso 2: El atacante usó 5,000e18 USDT para comprar WBNB, y luego usó el WBNB adquirido para comprar ~8,726,524e18 GAMMA.

  • Paso 3: El atacante primero apostó todo el GAMMA adquirido en el mercado gGAMMA, luego suministró el USDT restante como garantía, lo que aumentó su descuento de reembolso al 5% y habilitó el préstamo posterior.

  • Paso 4: El atacante llamó repetidamente a borrow y luego a updateUserDiscount para reducir continuamente su deuda registrada.

  • Paso 5: El atacante finalmente repagó la deuda, redimió la garantía y realizó la ganancia.

Conclusión

Este incidente fue causado por la lógica defectuosa de liquidación de descuentos de Planet Finance en changeUserBorrowDiscount(), que erróneamente trata el incremento en el saldo de préstamo almacenado de un prestatario como intereses recién acumulados y aplica el descuento de intereses a ese delta. Un atacante puede llamar repetidamente a borrow seguido de updateUserDiscount para subestimar su deuda registrada y finalmente repagar menos que el pasivo real para extraer ganancias.

Para reducir riesgos similares en el futuro:

  • Distinguir intereses y nuevos préstamos en el protocolo de préstamos.

Incidente AM

Resumen Breve

El 12 de marzo de 2026, AM Token, un token deflacionario en BNB Chain, fue explotado con una pérdida estimada de ~$131K. AM Token implementa un mecanismo deflacionario donde cada venta activa una quema adicional del pool de liquidez, eliminando permanentemente tokens para reducir el suministro total. Sin embargo, la quema no se ejecuta de inmediato: en cambio, el monto total de la venta se registra como toBurnAmount y la quema real se difiere hasta la siguiente venta. Este retraso crea una ventana entre el registro y la ejecución, durante la cual un atacante puede recomprar AM para reducir la reserva de AM del pool hasta toBurnAmount. Cuando la siguiente venta activa la quema diferida, toda la reserva de AM es eliminada, llevando el precio a un nivel extremo y permitiendo al atacante vender AM para obtener ganancias.

Antecedentes

AM Token es un token deflacionario en BNB Chain. En cada venta, el contrato registra el monto de AM involucrado en el intercambio como toBurnAmount, y quema ese monto registrado del pool de liquidez durante la siguiente venta. En efecto, las ventas activan una quema diferida que reduce la reserva de AM del pool. Además, antes de ejecutar la quema, el protocolo intercambia el totalTokenFee acumulado en USDT y lo distribuye de acuerdo con su lógica de asignación de comisiones.

Análisis de Vulnerabilidad

La causa raíz fue que la lógica de venta del token (0x27f9...213f) acumula el monto total de AM intercambiado como toBurnAmount y solo ejecuta la quema en la siguiente venta eliminando tokens del par AM/USDT y llamando a pair.sync() para actualizar las reservas. Este diseño permite a un atacante manipular las reservas de AM del pool, distorsionar el precio en cadena y obtener ganancias mediante arbitraje.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0xd0d1...f859.

  • Paso 1: El atacante tomó un préstamo flash de ~27,265,119e18 USDC y ~361,710e18 WBNB, luego los intercambió por ~100,423,811e18 USDT.

  • Paso 2: El atacante intercambió ~5,062e18 tokens AM por USDT, lo que manipuló el toBurnAmount registrado por el contrato a ~4,303e18.

  • Paso 3: El atacante intercambió USDT por AM Token, reduciendo la reserva de AM del pool a ~4,303e18.

  • Paso 4: El atacante transfirió 6 wei de AM al pool, activando la lógica de quema de la ruta de venta. Como resultado, el contrato quemó todo el saldo de AM del pool, reduciendo la reserva de AM a 0. Nota: el protocolo primero intenta intercambiar las comisiones acumuladas en USDT antes de la quema. Esta ruta de conversión de comisiones también activa la lógica de quema de la rama de venta. Después de que la quema se ejecuta y la reserva de AM llega a 0, el intercambio de comisiones falla. Dado que está envuelto en un try/catch, el fallo no revierte la transacción. En cambio, la ejecución continúa y el acumulador de comisiones se reinicia a 0.

  • Paso 5: El atacante llamó a pool.sync() y transfirió el USDT restante y 1 wei de AM al pool. Dado que ambos tokens fueron transferidos simultáneamente, el contrato trató esto como addLiquidity, por lo que toBurnAmount no fue acumulado. La reserva de AM fue actualizada a 7.

  • Paso 6: El atacante intercambió los tokens AM restantes por USDT. Durante este intercambio, la transferencia de AM al Pair activó la lógica de quema de la ruta de venta, reduciendo la reserva de AM a 1. Además, dado que totalFeeAmount había sido reiniciado a 0 en el Paso 4, la conversión de comisiones a USDT ya no fue ejecutada, permitiendo al atacante vender AM a un precio artificialmente inflado.

  • Paso 7: El atacante repagó el préstamo flash y realizó la ganancia restante.

Conclusión

Este incidente fue causado por el mecanismo de quema defectuoso de AM Token, que acumula el AM involucrado en el intercambio de cada venta como toBurnAmount y luego quema esa cantidad del Par AM/USDT en la siguiente venta mientras llama a pool.sync(). Esto permite a un atacante manipular las reservas de AM del Par hasta un nivel extremo y vender AM a un precio artificialmente inflado para drenar USDT.

Para reducir riesgos similares en el futuro:

  • Limitar el monto máximo de quema por transacción y limitar la frecuencia con la que se puede activar la quema, evitando que los atacantes consuman una gran parte de las reservas de tokens del pool en un corto período de tiempo.

Incidente DBXen

Resumen Breve

El 12 de marzo de 2026, DBXen, un protocolo burn-to-earn en Ethereum y BNB Chain, fue explotado con una pérdida total de ~$149K. La causa raíz fue una inconsistencia entre _msgSender() y msg.sender. Cuando se llama a burnBatch() a través del forwarder, el monto de XEN quemado se registra bajo _msgSender() (controlado por el atacante), pero los registros de ciclo se actualizan en msg.sender (el forwarder). Esta división permite al atacante reclamar recompensas y comisiones contra registros de ciclo obsoletos, resultando en pagos anormalmente grandes.

Antecedentes

DBXen es un protocolo burn-to-earn: los usuarios queman tokens XEN a cambio de recompensas DXN y una parte de las comisiones del protocolo acumuladas. El mecanismo clave funciona en ciclos. Cuando un usuario llama a burnBatch(), ocurren dos cosas: (1) el monto de XEN quemado se registra bajo la dirección del caller (identificado por _msgSender()), y (2) el contrato XEN devuelve la llamada a DBXen mediante onTokenBurned() para actualizar los registros de ciclo del caller (ciclo de quema y lastFeeUpdateCycle) al ciclo actual.

Las recompensas y comisiones se liquidan mediante updateStats(). La recompensa es proporcional a la participación del usuario en el total de XEN quemado en su ciclo de quema. La comisión se basa en las comisiones del protocolo acumuladas desde el último ciclo registrado del usuario. Ambos cálculos dependen de que los registros de ciclo del usuario estén actualizados.

Análisis de Vulnerabilidad

La causa raíz es la lógica de negocio defectuosa en el protocolo DBXen (0xf5c8...2abd). La función _msgSender() verifica si msg.sender es el forwarder. Si es así, devuelve los últimos 20 bytes del calldata, y este valor devuelto puede ser controlado arbitrariamente en el contexto del forwarder. Sin embargo, burnBatch() quema directamente el XEN en poder de msg.sender. Como resultado, un atacante puede invocar burnBatch() a través del forwarder, causando que el protocolo queme el XEN en poder del forwarder y actualice tanto el registro del ciclo de quema del forwarder como el registro del ciclo de actualización de comisiones al ciclo actual. Sin embargo, al mismo tiempo, el protocolo registra el monto de XEN quemado bajo la dirección correspondiente a _msgSender().

Después de eso, el atacante llama a claimFees(), que invoca updateStats(). Dado que los registros de ciclo de la dirección _msgSender() nunca fueron actualizados (tanto el ciclo de quema como lastFeeUpdateCycle permanecen en 0), updateStats() calcula las recompensas en el ciclo actual y computa las comisiones acumuladas desde el ciclo 0, abarcando toda la historia de comisiones del protocolo. El atacante luego obtiene ganancias llamando a claimFees() y claimRewards().

Análisis del Ataque

El siguiente análisis se basa en la transacción 0x914a5a...b808bc37.

  • Paso 1: El atacante primero llamó a la función registerDomainSeparator() del contrato Forwarder, habilitando las llamadas posteriores a Forwarder.execute().

  • Paso 2: El atacante intercambió 0.14e18 ETH por 13,900,000,000e18 XEN en un pool de Uniswap V2.

  • Paso 3: El atacante transfirió 13,900,000,000e18 XEN al contrato Forwarder.

  • Paso 4: El atacante usó Forwarder.execute() para aprobar que DBXen gaste los 13,900,000,000e18 XEN en poder del Forwarder.

  • Paso 5: El atacante usó Forwarder.execute() para llamar a DBXen.burnBatch() y quemó 13,900,000,000e18 XEN. El monto quemado fue registrado bajo la dirección 0x425D3eC2DCeBE2c04bA1687504D43AFC6be7328d, mientras que durante la ejecución de la quema, XEN devolvió la llamada a DBXen mediante onTokenBurned(), actualizando los registros de ciclo relevantes en el Forwarder.

  • Paso 6: El atacante usó Forwarder.execute() para llamar a DBXen.claimFees() y obtuvo 65.36e18 ETH.

  • Paso 7: El atacante usó Forwarder.execute() para llamar a DBXen.claimRewards() y acuñó 2,305.4e18 DXN.

Conclusión

La causa raíz de este incidente fue que el protocolo DBXen usó inconsistentemente _msgSender() y msg.sender. Dado que estos dos valores podían diferir, la contabilidad interna del protocolo se volvió inconsistente, lo que permitió al atacante explotar la discrepancia para obtener ganancias.

Para reducir riesgos similares en el futuro:

  • Usar _msgSender() de manera consistente en todas las rutas de lógica, o asegurarse de que las operaciones dependientes de msg.sender y la contabilidad dependiente de _msgSender() siempre referencien la misma dirección.

Incidente Goose Finance

Resumen Breve

El 15 de marzo de 2026, Goose Finance, un protocolo de yield-farming en BNB Chain, fue explotado por aproximadamente $8K. La causa raíz fue un defecto en el orden de valoración de participaciones en StrategyGooseEgg: deposit() acuña participaciones antes de liquidar las recompensas cosechadas en la contabilidad, por lo que el denominador de activos totales utilizado para la valoración de participaciones excluye estas recompensas y es menor que el valor real. Esto significa que los depositantes reciben más participaciones de las que deberían. Cuando se llama a withdraw(), activa una cosecha de recompensas que aumenta los activos totales, haciendo que cada participación valga más. Al repetir en bucle depósito y retiro en una sola transacción, el atacante acuñó repetidamente participaciones sobrevaloradas y las redimió al valor corregido (mayor), extrayendo la diferencia como ganancia.

Antecedentes

Goose Finance es un protocolo de yield-farming en BNB Chain donde los fondos de los usuarios fluyen desde una bóveda a una estrategia, que apuesta activos en MasterChef para ganar recompensas EGG.

Los componentes relevantes para este incidente son:

  • VaultChef (0x3f64...): rastrea las posiciones de los usuarios y reenvía capital a StrategyGooseEgg.

  • StrategyGooseEgg (0x0980...): mantiene la contabilidad a nivel de estrategia con sharesTotal y wantLockedTotal.

  • MasterChef (0xe70e...): recibe activos apostados y paga recompensas EGG.

  • WrappedEgg (0xb815...): envuelve EGG 1:1 en WEGG para apostar.

Operacionalmente, los depósitos se enrutan desde VaultChef a StrategyGooseEgg, luego se apuestan en MasterChef. Los retiros se inician desde VaultChef y son ejecutados por la estrategia.

Una expectativa contable clave es que la valoración de participaciones debería reflejar los activos totales de la estrategia en el momento de la valoración (principal apostado más recompensas inactivas ya en poder de la estrategia). En StrategyGooseEgg, sin embargo, deposit() acuña participaciones antes de que _farm() liquide los activos inactivos en wantLockedTotal, mientras que withdraw() puede activar la cosecha de recompensas desde MasterChef. Este orden es la base de la vulnerabilidad analizada a continuación.

Análisis de Vulnerabilidad

La causa raíz es una desincronización contable en StrategyGooseEgg (0x0980...b26b) entre la cosecha de recompensas y la valoración de participaciones.

En StrategyGooseEgg, la valoración de participaciones usa wantLockedTotal como denominador: shares = deposit * sharesTotal / wantLockedTotal. Para que esto sea justo, wantLockedTotal debe reflejar todos los activos que la estrategia realmente posee, incluyendo cualquier recompensa EGG inactiva que esté en el contrato. Sin embargo, deposit() acuña participaciones antes de que _farm() liquide las recompensas inactivas en wantLockedTotal. Esto significa que el denominador excluye las recompensas no contabilizadas y es menor que los activos totales reales, causando que el depositante reciba más participaciones de las que debería.

Además, withdraw() llama a MasterChef.withdraw(), que devuelve el principal apostado más las recompensas EGG pendientes a la estrategia. La contabilidad de la estrategia solo resta el _wantAmt solicitado de wantLockedTotal, por lo que las recompensas cosechadas permanecen en el saldo de la estrategia sin estar reflejadas en wantLockedTotal. Esto amplía la brecha entre los activos reales en poder y el wantLockedTotal registrado, haciendo que cualquier valoración de participaciones posterior en deposit() sea aún más inexacta.

Análisis del Ataque

El siguiente análisis se basa en la transacción 0x86efdf...ce316223.

  • Paso 1: El atacante toma prestado EGG en un préstamo flash de dos pares de Pancake.

  • Paso 2: Primer depósito en VaultChef/StrategyGooseEgg (10,170,000e18 EGG).

  • Paso 3: Primer retiro (12,593,884e18 EGG) cosecha recompensas de MasterChef; 359,561e18 EGG es transferido a StrategyGooseEgg y permanece como valor inactivo/no contabilizado (R > 0).

  • Paso 4: El segundo depósito reutiliza el capital retirado (12,593,884e18 EGG). Las participaciones se valoran antes de que el valor inactivo sea liquidado, por lo que este es el paso de sobre-acuñación.

  • Paso 5: El segundo retiro (12,826,027e18 EGG) realiza la ganancia de las participaciones sobre-acuñadas (es decir, 232,143 EGG por encima del capital de entrada del paso 4).

  • Paso 6: El atacante repaga los préstamos flash y conserva el diferencial neto.

Conclusión

El exploit surge de un defecto en el orden de valoración de participaciones en StrategyGooseEgg: deposit() acuña participaciones antes de que _farm() actualice wantLockedTotal, mientras que withdraw() puede cosechar recompensas de MasterChef que permanecen temporalmente inactivas y no contabilizadas. Esto permite que los depósitos acuñen contra un denominador obsoleto y luego retiren contra activos actualizados.

Para reducir riesgos similares en el futuro:

  • Liquidar recompensas y actualizar la contabilidad antes de los cálculos tanto de acuñación como de quema de participaciones.

  • Valorar las participaciones contra un único totalAssets (apostado + inactivo) en el punto exacto de cálculo.

  • Agregar pruebas de invariantes para shares_minted <= D * S / (A + R) bajo condiciones de recompensas inactivas distintas de cero.


Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento 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 obligaciones AML/CFT, a lo largo del ciclo de vida completo de protocolos y plataformas.

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