Durante la semana pasada (2026/04/20 - 2026/04/26), BlockSec detectó y analizó ocho incidentes de ataques, con pérdidas totales estimadas de ~$7.04M. La tabla a continuación resume estos incidentes, y los análisis detallados de cada caso se proporcionan en las subsecciones siguientes.
| Fecha | Incidente | Tipo | Pérdida Estimada |
|---|---|---|---|
| 2026/04/19* | Contrato Rebalanceador Personalizado | Llamada Arbitraria | ~$64K |
| 2026/04/20 | REVLoans (Juicebox) | Validación Incorrecta | ~$50.7K |
| 2026/04/22 | Volo Vault / Navi | Compromiso de Clave | ~$3.5M |
| 2026/04/22 | Kipseli Router | Validación Incorrecta | ~$72.35K |
| 2026/04/23 | GiddyDefi | Validación de Firma Incompleta | ~$1.3M |
| 2026/04/25 | Purrlend | Compromiso de Clave | ~$1.5M |
| 2026/04/26 | SingularityFinance | Configuración Incorrecta del Oráculo | ~$413K |
| 2026/04/26 | Scallop | Fallo de Contabilidad | ~$142.7K |
*El incidente del Contrato Rebalanceador Personalizado no fue cubierto en el informe de la semana pasada y se incluye aquí por completitud.
Destacado de la Semana: GiddyDefi
El atacante no descifró la firma, no utilizó un préstamo flash y no manipuló ningún precio. Reprodujo una firma legítima con sus campos no firmados sustituidos por su propio contrato. "Usar tu propia firma en tu contra" es la demostración más clara de cómo una cobertura parcial de EIP-712 convierte una firma válida en un permiso genérico.
El 23 de abril de 2026, GiddyVaultV3 en Ethereum fue explotado por aproximadamente $1.3M. El esquema de firma cubría únicamente SwapInfo.data, dejando aggregator, fromToken, toToken y amount fuera del hash EIP-712, por lo que una firma válida podía reproducirse con esos campos manipulados. El atacante apuntó aggregator hacia un contrato malicioso y fromToken hacia el token LP de la estrategia, drenando aproximadamente $1.3M.
Contexto
GiddyVaultV3 (0x5f0a...4318) es un contrato de bóveda de yield farming donde los usuarios depositan y retiran fondos mediante deposit() y withdraw(). Cada operación debe llevar una estructura de autorización VaultAuth firmada por el backend, que incluye una firma EIP-712 y un array SwapInfo[ ] que describe las rutas de intercambio de tokens. Al ejecutar un intercambio, el contrato llama a GiddyLibraryV3.executeSwap(), que realiza un forceApprove sobre swap.fromToken otorgando permiso a swap.aggregator, y luego ejecuta el intercambio mediante aggregator.call(swap.data). El contrato de estrategia gestiona posteriormente los fondos según su estrategia configurada.
EIP-712 es un estándar para firmar datos estructurados fuera de la cadena: el protocolo que consume la firma reconstruye la misma estructura en la cadena, la hashea bajo un separador de dominio acordado y recupera la dirección del firmante. La seguridad de cualquier flujo EIP-712 depende por tanto de que el hash en cadena cubra todos los campos cuyo valor afecta a la ejecución. En el diseño de Giddy, el backend firma un VaultAuth que contiene tanto la intención del usuario como las instrucciones de enrutamiento para cualquier intercambio requerido, y _validateAuthorization() reconstruye esa estructura para verificar la firma antes de que la estrategia pueda mover fondos.
Análisis de Vulnerabilidad
La vulnerabilidad reside en la función _validateAuthorization() de GiddyVaultV3. Al construir el payload firmado, solo se hashea el campo data de cada SwapInfo; aggregator, fromToken, toToken y amount quedan todos excluidos de la firma. Esto significa que cualquier persona en posesión de una firma válida puede sustituir libremente los campos restantes de SwapInfo y aún así superar la verificación de firma.
Cada campo excluido es una palanca independiente que la firma deja libre: aggregator se convierte tanto en el gastador como en el objetivo de la llamada mediante forceApprove y aggregator.call(swap.data); fromToken selecciona qué activo de la estrategia es aprobado; amount establece el límite de la asignación; toToken solo alimenta una comprobación returnAmount > 0. Los data firmados no restringen ninguno de estos porque ninguno de estos objetivos está referenciado en su interior.

Análisis del Ataque
El siguiente análisis se basa en la transacción 0x5edb66...5482e5.
-
Paso 1: El atacante obtuvo una firma
VaultAuthlegitimamente autorizada por el backend a partir de datos en cadena, manteniendo el campodataintacto. Dado que cada llamada anterior adeposit()owithdraw()transmite el payload completo deVaultAuthen cadena, cualquier transacción histórica era una fuente gratuita de una firma reutilizable; el atacante solo necesitaba una cuyo campodatafuera adecuado para la llamada de intercambio prevista. -
Paso 2: Usando la firma obtenida, el atacante mantuvo sin cambios
signature,nonceydataoriginales mientras manipulaba los campos restantes.fromTokense configuró como el Token LP en poder del contrato de estrategia (un activo real), de modo que elforceApproveotorgara permiso sobre un token que el protocolo realmente poseía.aggregatorfue reemplazado por el contrato malicioso del atacante, de modo que tanto la aprobación como elaggregator.call()subsiguiente se dirigían a código propiedad del atacante. Como estos campos estaban fuera del alcance de la validación de la firma,_validateAuthorization()aceptó la estructura manipulada sin cambios. Para eludir la comprobación finalrequire(returnAmount > 0, "SWAP_NO_TOKENS_RECEIVED"), el agregador malicioso implementó una función de acuñación que acuñaba tokens falsos de vuelta al protocolo, satisfaciendo la comprobación sin realizar ningún intercambio real.


- Paso 3: Dado que el agregador malicioso había recibido aprobación en el paso 2, el atacante llamó a
transferFrompara mover los tokens LP de la bóveda directamente al agregador malicioso, completando el robo. Este paso quedaba completamente fuera de la ruta de ejecución protegida del protocolo; en el momento en queexecuteSwap()retornó, la asignación ya había sido registrada y la comprobación del saldo posterior a la llamada ya había superado, por lo que el protocolo no tuvo más oportunidad de intervenir.

Conclusión
La causa raíz de este ataque fue la cobertura incompleta de la firma EIP-712. Los campos centrales de SwapInfo que gobiernan directamente el flujo de fondos quedaron desprotegidos, permitiendo al atacante sustituir la ruta de intercambio y la dirección del agregador mientras presentaba una firma válida. Los desarrolladores que integren agregadores externos deben:
-
Asegurarse de que las firmas EIP-712 cubran todos los campos que afectan a los resultados de ejecución, incluidos
aggregator,fromToken,toTokenyamount. -
Aplicar una lista blanca de agregadores para prevenir llamadas a contratos externos no auditados.
-
Restringir
toTokena los tokens base esperados para evitar que tokens falsos eludan las comprobaciones de saldo.
De forma más general, EIP-712 en cualquier arquitectura de aprobación-luego-llamada debe hashear todos los campos que influyen en el estado resultante en cadena, no solo la intención del usuario. Siempre que una firma del backend sea el único guardián entre los parámetros suministrados por el usuario y una acción privilegiada del contrato, cada parámetro que fluya hacia esa acción (objetivo de la llamada, activo, cantidad, destinatario) debe estar dentro de la estructura firmada. Tratar data como un proxy para la identidad de la llamada es un error categórico: la identidad de la llamada es la tupla de todos sus parámetros, y cualquier parámetro dejado fuera de la firma está, por definición, controlado por quien envía la transacción.
Best Security Auditor for Web3
Validate design, code, and business logic before launch
Más Incidentes de Esta Semana
Contrato Rebalanceador Personalizado
El 19 de abril de 2026, un contrato rebalanceador de sAVAX en Avalanche fue explotado para extraer aproximadamente $64K (~7,000 WAVAX) de la delegación de crédito de Aave V3 de un usuario. Una función pública ejecutaba un target.call(data) arbitrario mientras aún mantenía la delegación del usuario, por lo que el atacante podía invocar borrow() de Aave con onBehalfOf configurado como la víctima. Un bot de whitehat se adelantó al exploit y recuperó los fondos antes de cualquier retiro.
Contexto
El contrato rebalanceador (0x7a7b...a8c9) expone una función b2a13230() diseñada para rebalancear la posición apalancada de un usuario en Aave. La función opera en nombre del usuario mediante la delegación de crédito de Aave V3: el usuario otorga al rebalanceador permiso para pedir prestado en su nombre, y el rebalanceador combina esos préstamos con fondos suministrados por el usuario para ajustar la posición (por ejemplo, un flujo de trabajo de préstamo + suministro).
Análisis de Vulnerabilidad
La causa raíz es que b2a13230() incluye un paso target.call(data) cuyos objetivo y calldata están ambos controlados por el llamante. Esta llamada se ejecuta mientras el contrato todavía opera bajo la delegación de crédito de Aave V3 del usuario, por lo que cualquier lógica invocada durante ese paso hereda el poder de préstamo del usuario. No hay lista blanca de objetivos permitidos ni restricción de forma en el calldata, por lo que la llamada puede invocar cualquier método de contrato, incluido borrow() de Aave con onBehalfOf configurado como el usuario.

Análisis del Ataque
El siguiente análisis se basa en la transacción: 0xaaa1b2...35001b.
-
Paso 1: El atacante realizó un préstamo flash por una cantidad de
sAVAXyUSDC. Luego suministró elUSDCprestado a Aave V3 a través del contrato rebalanceador para establecer suficiente colateral para pedir prestado. Mientras tanto, elsAVAXprestado fue transferido directamente al contrato rebalanceador para preparar el paso de suministro posterior al préstamo. -
Paso 2: El atacante invocó la función
b2a13230(). La función primero realizó una operación de préstamo normal, luego llegó a la sección de llamada arbitraria. En este punto, el atacante elaboró la llamada para invocar directamente la funciónborrow()de Aave V3 cononBehalfOfconfigurado como la dirección de la víctima. Dado que la víctima había otorgado delegación de crédito al contrato rebalanceador, el préstamo tuvo éxito. ElWAVAXprestado fue transferido al contrato rebalanceador.

- Paso 3: El atacante invocó la función
b2a13230()de nuevo, esta vez usando el rebalanceador para pedir prestadoWAVAXen su propio nombre. El contrato luego usó elWAVAXpreviamente prestado (originado de la posición de la víctima) para suministrar a la posición del atacante y reembolsar, permitiendo al atacante extraer ganancias.
Conclusión
El defecto es la combinación de una llamada externa arbitraria dentro de un contexto privilegiado que mantiene crédito delegado. Cualquiera de las capas por sí sola sería segura: una llamada externa restringida no puede hacer un uso indebido de la delegación, y una llamada arbitraria sin delegación no puede mover los fondos del usuario. Los contratos que mantienen delegación de crédito nunca deben exponer una llamada externa arbitraria; si tales llamadas son necesarias, los objetivos deben fijarse en una lista blanca y el calldata debe tener restricciones de forma.
REVLoans (Juicebox)
El 20 de abril de 2026, REVLoans, una extensión de préstamos sobre Juicebox, fue explotada en Ethereum por aproximadamente $50.7K. borrowFrom() aceptaba una fuente de contabilidad suministrada por el llamante sin verificar que estuviera registrada en el protocolo; un contexto forjado de 36 decimales activó un atajo de misma moneda que mal reescaló los saldos en 1e18. Dos transacciones, una para sembrar la entrada de contabilidad inflada y otra para pedir prestado contra el pool legítimo al precio de participación inflado, drenaron 21.77 ETH.
Contexto
Juicebox es un protocolo híbrido de recaudación de fondos y préstamos en Ethereum. Cada proyecto tiene su propio token ERC20 de participación (denominado aquí REV) y un tesoro dividido en uno o más terminales, donde un terminal es el contrato que custodia físicamente un subconjunto de los activos del proyecto y actúa como punto de entrada/salida para el usuario. Un proyecto puede tener múltiples terminales registrados en JBDirectory, y cada triple (terminal, proyecto, token) lleva un JBAccountingContext que declara los (decimales, moneda) utilizados para la contabilidad de ese token dentro de ese terminal. REV es por tanto una reclamación sobre la unión de excedentes de todos los terminales del proyecto, no una reclamación contra ningún terminal individual.
Un usuario puede depositar un activo en un terminal a cambio de REV recién acuñado, o canjear REV en un terminal por una parte proporcional de su excedente (con un impuesto de retiro configurable que deja algo de valor para los tenedores restantes). REVLoans (0x2db6...1846), un contrato separado construido sobre esto, añade una facilidad de préstamo: un usuario quema REV como colateral y obtiene un préstamo contra uno de los terminales del proyecto, con el préstamo reembolsable posteriormente a cambio de volver a acuñar el colateral. El monto del préstamo se calcula con exactamente la misma matemática que un canje, por lo que un préstamo es económicamente equivalente a retirar el mismo colateral.
El precio de participación de REV es (totalSurplus + totalBorrowed) / (REV.totalSupply + totalCollateral). Incluir totalBorrowed en el numerador mantiene la neutralidad de precio préstamo/reembolso; también significa que un totalBorrowed inflado eleva directamente el precio de participación y permite que un pequeño colateral retire una cantidad desproporcionada.
Análisis de Vulnerabilidad
La causa raíz es la entrada no verificada en el parámetro source. borrowFrom() acepta un REVLoanSource source suministrado por el llamante (una estructura con campos .terminal y .token) sin verificar que este par esté registrado para el revnetId dado. Ambos campos fluyen directamente hacia la matemática de retiro, por lo que el contexto de contabilidad devuelto por source.terminal está completamente controlado por el llamante. Cuando el campo currency de ese contexto coincide con el del terminal de destino, el protocolo toma un atajo de misma moneda, omite el oráculo de precios y trata las cifras de decimales y saldo suministradas como autoritativas.

El source no validado es luego escrito en _loanSourcesOf[revnetId] y totalBorrowedFrom[revnetId][source.terminal][source.token] por _addTo(), que tampoco realiza ninguna comprobación de registro.

Una vez que (source, revnetId) está en la contabilidad, _borrowableAmountFrom() es la función que traduce una solicitud de préstamo en un monto pagadero. Construye surplus = totalSurplus + totalBorrowed desde _totalBorrowedFrom(), luego pasa ese excedente a JBCashOuts.cashOutFrom() junto con el conteo de colateral del llamante y el suministro de participaciones.

El error de decimales existe un nivel más profundo, en _totalBorrowedFrom(). Itera _loanSourcesOf y pliega cada entrada mediante mulDiv(tokensLoaned, 10**decimals, pricePerUnit). En la ruta de misma moneda, pricePerUnit = 10**decimals (la precisión de 18 decimales del objetivo), por lo que la fórmula se reduce a tokensLoaned sin cambios, y un saldo almacenado bajo contabilidad de 36 decimales cae en la suma de ETH de 18 decimales 1e18 veces más grande.

La amplificación ocurre en cashOutFrom(). base = mulDiv(surplus, cashOutCount, totalSupply): con surplus dominado por el totalBorrowed inflado, incluso un cashOutCount (colateral) diminuto se mapea a un pago desproporcionadamente grande.

Análisis del Ataque
El ataque usa dos transacciones. La primera contamina la contabilidad de REVLoans: 0xc46cb7...dead1f. La segunda drena el pool contra un terminal legítimo: 0x9adbd6...a8f938.
- Paso 1: El atacante llamó a
borrowFrom()con tantoterminalcomotokenen la fuente del préstamo apuntando a un contrato falso, aportando una pequeña cantidad deREVcomo colateral. REVLoans no verificó si el terminal suministrado está registrado para el revnet, ni si el token es reconocido por él.

- Paso 2: REVLoans consultó al terminal falso por un contexto de contabilidad, que devolvió un
(decimals=36, currency=ETH-code(61166))forjado. Debido a que las monedas de origen y destino coincidían, REVLoans tomó un atajo de misma moneda y omitió el oráculo de precios, luego ejecutó la matemática de retiro sobre los excedentes reales deETHde los terminales legítimos re-expresados en la unidad objetivo de 36 decimales del atacante, inflando la cifra en 1e18.

- Paso 3: REVLoans registró
(terminal falso, token falso)en_loanSourcesOfy escribió la cifra inflada entotalBorrowedFrom. El terminal falso "pagó" simplemente confirmando la recepción; no se movióETHreal. La primera transacción terminó contotalBorrowedmanipulado al alza y solo el pequeño colateralREVquemado.

- Paso 4: El atacante llamó a
borrowFrom()de nuevo, esta vez pasando el terminalETHlegítimo como fuente del préstamo y un colateralREVdiminuto. La matemática de retiro se ejecutó en unidades reales deETHde 18 decimales.

- Paso 5: Al calcular
totalBorrowed, REVLoans iteró_loanSourcesOfy encontró la entrada del paso 3. Debido a que lacurrencyde esa entrada aún coincidía conETH, el atajo de misma moneda se activó de nuevo y el saldo almacenado de 36 decimales fue plegado en la suma deETHde 18 decimales 1e18 veces más grande.totalBorrowedestaba ahora dominado por deuda falsa y el numerador del precio de participación estaba masivamente inflado.

- Paso 6: La matemática de retiro devolvió un monto de préstamo dimensionado al numerador inflado, que el atacante había preajustado para quedar justo por debajo del excedente real del terminal legítimo. El terminal legítimo lo pagó, drenando casi todo el pool al atacante.

Conclusión
La causa raíz son dos brechas compuestas: los pares (terminal, token) se aceptan sin verificar el registro en el revnet, y el atajo de misma moneda pliega los saldos de origen en la suma de destino sin renormalizar las diferencias de decimales. Cualquiera de las brechas por sí sola sería menos peligrosa; juntas permiten a un llamante inyectar una entrada totalBorrowedFrom arbitraria y cobrarla a valor nominal. Mitigación: validar (terminal, token) contra los terminales registrados del revnet, y renormalizar los saldos por la escala decimal almacenada de la fuente antes de plegarlos.
Volo Vault
El 22 de abril de 2026, Volo, una bóveda de rendimiento en Sui que gana rendimiento de préstamos enrutando los depósitos de usuarios al protocolo de préstamos Navi, perdió aproximadamente $3.5M después de que la clave privada del operador fuera filtrada. El contrato de la bóveda no tenía ningún error de código; el atacante simplemente ejecutó la ruta de operador legítima con credenciales robadas y drenó los depósitos de Volo en Navi.
Contexto
Volo es la bóveda orientada al usuario (0xcd86...27fefa); Navi es el protocolo de préstamos subyacente. La bóveda mantiene un AccountCap de Navi (un objeto de capacidad de Sui que autoriza retiros de la cuenta Navi de Volo) y delega los movimientos de estrategia a un rol de operador. Para depositar o retirar en Navi, el operador llama a start_op_with_bag_v2() para extraer el AccountCap de la bóveda a una bolsa temporal, luego deposit_with_account_cap() / withdraw_with_account_cap_v2() usan esa capacidad para mover fondos.
Análisis de Vulnerabilidad
La causa raíz es un fallo operacional/de custodia de claves en lugar de una vulnerabilidad a nivel de contrato. La ruta de estrategia de Volo delega la autoridad de retiro a quien posea la clave privada del operador: start_op_with_bag_v2() realiza solo dos verificaciones (assert_operator_not_freezed(operation, cap) y assert_single_vault_operator_paired(operation, vault.vault_id(), cap)), ambas de las cuales solo verifican que la capacidad suministrada sea el operador registrado. withdraw_with_account_cap_v2() luego acepta a cualquier llamante que pueda presentar el AccountCap extraído. Cualquier persona en posesión de la clave privada del operador puede por tanto ejecutar la misma ruta que usan las operaciones legítimas, de forma indistinguible.

Análisis del Ataque
El siguiente análisis se basa en la transacción AQw9wM...3RUS.
- Paso 1: El atacante llamó a
start_op_with_bag_v2en@volosui/volo-vault::operationcon la clave del operador filtrada, extrayendo elAccountCapde Navi a una bolsa temporal.

-
Paso 2: El atacante usó
bag::removepara extraer elAccountCapde la bolsa temporal. -
Paso 3: El atacante llamó a
withdraw_with_account_cap_v2en@navi-protocol/lending::incentive_v3con elAccountCapextraído, retirando los depósitos de Volo de Navi.

- Paso 4: El atacante usó
bag::addpara devolver elAccountCap, cerró la operación y transfirió los fondos.
Conclusión
El defecto es estructural: una sola clave de operador, autoridad de retiro completa, sin segunda verificación. Tres cambios reducen el daño de un compromiso de clave. Dividir el rol de operador en un esquema multisig o de umbral significa que una clave filtrada no puede autorizar un retiro por sí sola. Agregar un bloqueo de tiempo en los retiros salientes da a las llamadas anómalas una ventana impugnable antes de la liquidación. Limitar los poderes del operador solo a depositar y rebalancear, con los retiros orientados al usuario enrutados a través de una ruta separada, evita que el rol de operador alcance los fondos de los usuarios en absoluto.
Kipseli Router
El 22 de abril de 2026, el Kipseli Router en Base fue explotado por aproximadamente $72.35K. El router usa una cotización devuelta por un cotizador externo exclusivo de USDC como monto bruto de transferencia del token de salida, sin verificar que el token de salida sea igual al token de cotización. Un atacante intercambió 0.04 WETH por cbBTC en una ruta que el cotizador no admite realmente, recibiendo el valor de retorno escalado en USDC del cotizador (92,610,395) como unidades brutas de cbBTC (≈0.926 cbBTC).
Contexto
Kipseli Router (0x579f...9a07) es un contrato de ejecución de intercambios respaldado por un sistema de cotización externo. El contrato no tiene código fuente abierto; el siguiente análisis se basa en su bytecode descompilado, razón por la cual los nombres de funciones aparecen como selectores de 4 bytes (0xcce096f3(), 0x592(), 0xd88()). En lugar de calcular precios de intercambio directamente desde los pools AMM en cadena, consulta al cotizador por un monto de salida (amountOut) y luego ejecuta la transferencia de token basándose en ese valor. En operación normal, el usuario envía tokenIn a la billetera del protocolo, y el router extrae tokenOut de la misma billetera y lo reenvía al destinatario. El protocolo está configurado con un único QUOTE_TOKEN, y la lógica de cotización está denominada en USDC usando contabilidad de 6 decimales; el sistema solo está diseñado para admitir cotizaciones denominadas en USDC.
Análisis de Vulnerabilidad
El defecto abarca dos capas que se componen. En el lado del router, la función 0xcce096f3() recupera una cotización v0 a través de la función de cotizador 0x592() y la pasa sin cambios a 0xd88() como tokenOut.transferFrom(_wallet, receiver, v0). El router nunca verifica que tokenOut sea igual al QUOTE_TOKEN del protocolo, por lo que un valor escalado en USDC (precisión de 6 decimales) se transfiere como si fuera una cantidad de cbBTC (precisión de 8 decimales). En el lado del cotizador, el AMM PropAMM subyacente está diseñado exclusivamente para pares token-USDC pero acepta rutas de enrutamiento no admitidas (WETH → cbBTC) sin revertir, ignorando silenciosamente tokenIn y devolviendo un valor escalado en USDC como si el intercambio fuera válido.

Análisis del Ataque
El siguiente análisis se basa en la transacción 0x96edee...3db3bb.
- Paso 1: El atacante llamó al router con
tokenIn=WETHytokenOut=cbBTC. El AMM subyacente no admitía esta ruta pero no revirtió, y el cotizador0x592()devolvió un valor escalado enUSDCde 92,610,395 (≈92.61USDC).

- Paso 2: El router usó ese valor directamente como el monto de transferencia de
cbBTC. 0.04WETH(≈$95) entró mediantetransferFrom; 92,610,395 unidades brutas decbBTC(≈0.926cbBTC, ≈$72.35K) salieron de la billetera del protocolo al atacante.

Conclusión
El exploit ocurre porque dos suposiciones no son verificadas en ninguno de los lados de la llamada al cotizador. El cotizador asume que su salida se consume en su propio marco de 6 decimales de USDC; el router asume que lo que devuelve el cotizador está denominado en el tokenOut solicitado. Cualquiera de las correcciones elimina el error:
-
En el router: afirmar
tokenOut == QUOTE_TOKEN, o convertir la cotización escalada enUSDCa unidades detokenOutmediante un oráculo antes de la transferencia. -
En el cotizador: revertir en rutas cuyos tokens no estén registrados para el conjunto de pares admitidos, en lugar de devolver silenciosamente un valor de respaldo escalado en
USDC.
Purrlend
El 25 de abril de 2026, Purrlend, un protocolo de préstamos en HyperLiquid y MegaETH, perdió aproximadamente $1.5M tras un compromiso de clave privada. El atacante tomó control del rol de puente y acuñó pTokens sin respaldo (los tokens de recibo similares a Aave de Purrlend), luego usó esos pTokens como colateral para pedir prestado activos reales del pool.
Contexto
Purrlend (0x81d5...a702) es un protocolo de préstamos con un modelo de contabilidad similar a Aave. Cuando los usuarios suministran activos al protocolo, reciben pTokens correspondientes, similares a los aTokens de Aave, que representan su posición suministrada y pueden usarse como colateral para pedir prestado otros activos.
El protocolo también incluye roles privilegiados, incluidos pool admin, risk admin y bridge. El rol de puente está destinado a la contabilidad entre cadenas: puede acuñar pTokens para reflejar depósitos que ocurrieron en una cadena homóloga. Los otros roles de administrador modifican parámetros de riesgo y configuran activos prestables.
Análisis de Vulnerabilidad
El desencadenante inmediato fue un compromiso de clave privilegiada: el atacante obtuvo las claves que controlan los roles de administrador y puente de Purrlend. Un defecto de diseño a nivel de contrato amplificó la filtración: la ruta de acuñación de pTokens del rol bridge no está anclada a ninguna prueba verificable de custodia entre cadenas. La función permite a un llamante con el rol de puente emitir pTokens a cualquier dirección, en cualquier cantidad, sin verificar que haya ocurrido un depósito correspondiente en la cadena de origen. En cualquier otro lugar del protocolo, los pTokens se tratan como colateral válido, y la ruta de préstamo no reverifica el respaldo en el momento del préstamo. Por lo tanto, una acuñación no autorizada del rol de puente se traduce directamente en poder de préstamo, sin segunda barrera entre la acuñación y el retiro de activos.
Análisis del Ataque
El siguiente análisis se basa en la transacción 0xb96cff...dbbf24 en MegaETH.
- Paso 1: El atacante, en posesión de claves privilegiadas comprometidas, usó un lote
MultiSendCallOnlymedianteGnosisSafeProxypara establecerse comopool admin,risk admin,bridgeyemergency admina través deACLManager, luego habilitóWETHcomo activo prestable y estableció suBorrowCapen 200.

-
Paso 2: Actuando como el puente, el atacante acuñó una gran cantidad de
pTokensa su propia dirección. La ruta de acuñación del puente no realizó ninguna verificación de custodia entre cadenas, por lo que los nuevospTokensno tenían activos subyacentes que los respaldaran. -
Paso 3: El atacante usó los
pTokenssin respaldo como colateral. Debido a que la ruta de préstamo trata cualquier saldo depTokencomo una posición de suministro válida sin reverificar el respaldo, la verificación de colateral pasó y se pidió prestadoWETHdel pool.
Conclusión
Este fue un compromiso de clave privada amplificado por un defecto de diseño a nivel de contrato. Las claves filtradas dieron al atacante solo la autoridad prevista del rol de puente, pero esa autoridad incluía la acuñación sin restricciones de pTokens, que se traduce directamente en colateral prestable. Cada capa puede ser reforzada independientemente. En la capa operacional, dividir el rol de puente en un esquema multisig o de umbral para que una sola filtración de clave no pueda ejercerlo. En la capa de contrato, requerir que la acuñación del puente lleve una prueba verificable de custodia (por ejemplo, un compromiso de mensaje de un verificador entre cadenas de confianza) y revertir cuando no se proporcione ninguna prueba. Verificar la prueba en el momento de la acuñación es la corrección más duradera porque elimina la dependencia de la custodia de claves por completo.
SingularityFinance
El 26 de abril de 2026, la bóveda dynBaseUSDCv3 de SingularityFinance en Base perdió aproximadamente $413K. La bóveda estaba configurada con un nivel de comisión de Uniswap V3 inválido (42, que no existe en V3), por lo que el oráculo de precios de cada activo que no es USDC se resolvía a un pool inexistente. La función de precios devolvía 0 silenciosamente en lugar de revertir, la bóveda valoraba sus reservas que no son USDC en cero, y un atacante acuñó casi todo el suministro de participaciones depositando una cantidad mínima de USDC, luego canjeó los activos subyacentes reales.
Contexto
La bóveda dynBaseUSDCv3 (0x67b9...4dcd) mantiene múltiples tokens con rendimiento y valora las reservas que no son USDC a través de Uniswap V3. El auxiliar de precios getPrice(base, fee, quote, amount) resuelve la tupla (base, quote, fee) a un pool de Uniswap V3 a través del factory, luego lee el TWAP de ese pool. totalAssets() de la bóveda agrega las reservas con precio; las proporciones de acuñación y canje de participaciones se derivan de este total.
Análisis de Vulnerabilidad
El defecto está en la rama de retorno temprano de getPrice(). Cuando IUniswapV3Factory.getPool(base, quote, fee) devuelve address(0) (no existe ningún pool para el nivel de comisión suministrado), la función cae a través y devuelve su variable price inicializada a cero en lugar de revertir. La bóveda fue desplegada con fee=42, que no es uno de los niveles admitidos por Uniswap V3 (500/3000/10000), por lo que la búsqueda de cada token que no es USDC llega a esta rama. totalAssets() por tanto suma aproximadamente solo el saldo de USDC de la bóveda, mientras que los tokens con rendimiento reales contribuyen cero. Las proporciones de acuñación y canje que dependen de totalAssets() se calculan contra este denominador casi nulo.

Análisis del Ataque
El siguiente análisis se basa en la transacción 0x00b949...8d3732.
-
Paso 1: El atacante obtuvo un préstamo flash de aproximadamente 100K
USDC. -
Paso 2: El atacante depositó el
USDCen la bóveda. Debido a quetotalAssets()solo contaba el saldo deUSDC, la bóveda se valoró a sí misma aproximadamente en el monto del depósito y el atacante recibió casi el 100% del suministro de participaciones. -
Paso 3: El atacante canjeó las participaciones, que distribuyen las reservas subyacentes proporcionalmente a la propiedad de participaciones. El atacante recibió una gran fracción de cada token con rendimiento que la bóveda poseía.
-
Paso 4: El atacante reembolsó el préstamo flash y conservó los tokens con rendimiento drenados como ganancia.
Conclusión
Faltaban dos verificaciones. El despliegue no validó fee=42 contra los niveles admitidos por Uniswap V3 (500/3000/10000); getPrice() devolvía 0 en un pool faltante en lugar de revertir. Cualquiera de las correcciones es suficiente: validar los parámetros del oráculo en el momento de la configuración, o revertir en getPool() == address(0). Como defensa en profundidad, la lógica de acuñación de participaciones debería verificar totalAssets() contra una referencia externa antes de aceptar depósitos.
Scallop
El 26 de abril de 2026, el programa de recompensas por staking de Scallop en Sui perdió aproximadamente $142.7K. La función que actualiza las recompensas acumuladas de un usuario no verificaba que el objeto de seguimiento de recompensas pasado coincidiera con la cuenta del usuario, permitiendo a un atacante extraer un saldo ficticio de puntos de un objeto de seguimiento de recompensas abandonado y largamente inactivo, y canjearlo contra el pool de recompensas legítimo hasta que el saldo fue drenado.
Contexto
Scallop es un protocolo de préstamos en Sui. Sobre su producto de préstamos, Scallop ejecuta un programa spool: los usuarios depositan un único activo en el mercado de Scallop para recibir MarketCoin<T> (el recibo de préstamo; para depósitos de SUI esto es MarketCoin<SUI>, la representación en cadena de "sSUI"), luego hacen staking de ese MarketCoin en un Spool para ganar puntos de protocolo con el tiempo, que luego canjean contra un RewardsPool asociado por tokens de recompensa reales. Cada Spool es un objeto compartido de Sui que rastrea un index global por participación; cada usuario tiene un SpoolAccount personal que registra su saldo en staking y los points acumulados.
Análisis de Vulnerabilidad
El defecto está en spool::user::update_points: la función no afirma account.spool_id == object::id(spool) (ni account.stake_type == spool.stake_type). Las entradas hermanas stake, unstake y redeem_rewards realizan todas esa verificación de enlace en la entrada; solo update_points la omite. Sin la verificación, spool_account::accrue_points calcula account.points += stake * (spool.index − account.index) / 1e9 contra cualquier Spool pasado, tratando su index como si fuera el flujo de recompensas propio de esta cuenta.

La ruta se vuelve explotable porque Sui nunca elimina los objetos compartidos: un Spool de Scallop abandonado cuyo stakes ha decaído a polvo sigue acumulando participación de recompensa (incremento por período 1e9 * reward / stakes), por lo que su index aumenta acumulativamente con el tiempo y puede alcanzar valores arbitrariamente grandes. Con la verificación de enlace ausente, update_points puede usar este index inflado para escribir un enorme delta de puntos en cualquier cuenta. Los points contaminados luego se canjean 1:1 contra el RewardsPool del spool objetivo, porque la cuenta está legítimamente vinculada a ese spool objetivo y la propia verificación de enlace de redeem_rewards pasa.
Análisis del Ataque
El siguiente análisis se basa en la transacción 6WNDjC...NfVL.
-
Paso 1: Con 0.2
SUIcomo cebo, el atacante acuñóMarketCoin<SUI>, luego llamó anew_spool_account+stakecontra el spool objetivo para crear unSpoolAccountlegítimamente vinculado conaccount.spool_id = target_spool. -
Paso 2: El atacante llamó a
update_points<MarketCoin<SUI>>(donor_spool, account, clock)condonor_spoolconfigurado como unSpoolabandonado. Elindexdel donante (≈8.91e14) fue escrito en la cuenta comopoints:points = stake * (8.91e14 − 1.19e9) / 1e9 ≈ 1.62e14. -
Paso 3: El atacante llamó a
redeem_rewards<MarketCoin<SUI>, SUI>(target_spool, target_rp, account). Las afirmaciones de enlace aceptaron la cuenta vinculada al objetivo, la re-acumulación interna retornó anticipadamente, y lospointscontaminados fueron convertidos a la tasa 1:1 del pool de recompensas hasta su saldo:rewards = 150,098,061,595,978unidades brutas deSUI. -
Paso 4: El atacante llamó a
unstakeyredeempara recuperar el cebo de 0.2SUI, luegoTransferObjectspara mover todo.
Conclusión
La corrección es agregar la misma verificación assert!(account.spool_id == object::id(spool)) en la entrada de update_points que stake, unstake y redeem_rewards ya realizan. Como defensa en profundidad, el protocolo también podría limitar el delta de index aceptado por una única llamada a accrue_points (rechazar deltas mayores que un límite configurado), de modo que incluso si la verificación de enlace fuera eludida de nuevo en el futuro, ninguna llamada individual pudiera acreditar una cuenta con una cantidad de points desproporcionada a su duración real de staking.
Acerca de BlockSec
BlockSec es un proveedor integral de seguridad blockchain y cumplimiento cripto. Desarrollamos 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 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 prestigiosas, 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.
-
Sitio web oficial: https://blocksec.com/
-
Cuenta oficial de Twitter: https://twitter.com/BlockSecTeam



