Durante la semana pasada (2026/06/08 - 2026/06/15), se detectaron 4 incidentes notables en Ethereum y Solana, con pérdidas totales de aproximadamente $5.98M. La tabla a continuación destaca los eventos más representativos:
| Fecha | Incidente | Tipo | Pérdida estimada |
|---|---|---|---|
| 2026/06/08 | Flooring Protocol | Desbordamiento de enteros | ~$900K |
| 2026/06/09 | Top Token | Ataque de gobernanza | ~$1.59M |
| 2026/06/10 | Raydium (en Solana) | Falta de validación de entradas | ~$1.34M |
| 2026/06/15 | Aztec | Falta de validación de entradas | ~$2.15M |
- Aztec: Una brecha de validación entre la ruta de prueba del rollup y la ruta de liquidación en L1 permitió que ambas procesaran conjuntos de transacciones distintos, alcanzando estados inconsistentes.
- Raydium: Una verificación de validación ausente permitió al atacante manipular el cálculo de redención de tokens LP, vaciando las reservas completas de cuatro pools.
El mejor auditor de seguridad para Web3
Valida el diseño, el código y la lógica de negocio antes del lanzamiento
Destacado de la semana: Aztec
En este incidente, el verificador de pruebas ZK y la lógica de liquidación en L1 procesaron conjuntos de transacciones distintos porque un único parámetro quedó sin acotar. Esta brecha de consistencia entre la prueba y la liquidación se aplica a cualquier diseño de rollup donde estas dos rutas se ejecuten como código separado.
El 15 de junio de 2026, Aztec Connect, un rollup centrado en la privacidad sobre Ethereum, fue explotado por aproximadamente $2.15M [1]. La causa raíz fue una discrepancia entre el conjunto de transacciones del rollup verificado y el límite de procesamiento de liquidación en L1, lo que permitió que la ruta de prueba ZK y la lógica de liquidación procesaran listas de transacciones distintas. El atacante aprovechó esta brecha para acreditar saldos de depósito sin respaldo en el estado del rollup y luego los retiró mediante los flujos normales de liquidación.
Contexto
Aztec Connect es un rollup centrado en la privacidad sobre Ethereum que permite transacciones privadas en L2. Dado que los fondos de los usuarios se originan en L1, primero deben depositarse en el contrato del procesador de rollup antes de poder representarse como notas en el árbol de Merkle de L2.
El proceso de depósito funciona en dos etapas:
Etapa 1: El usuario llama a depositPendingFunds(), que incrementa userPendingDeposits[assetId][owner] mediante increasePendingDepositBalance() y transfiere los tokens al RollupProcessor. Esto crea un depósito pendiente en L1.
function depositPendingFunds(uint256 _assetId, uint256 _amount, address _owner, bytes32 _proofHash) external {
increasePendingDepositBalance(_assetId, _owner, _amount);
// ... transferir tokens al contrato
}
Etapa 2: El usuario envía una prueba de depósito, que luego se incluye en un rollup y se agrega al estado de L2. Cuando se ejecuta processRollup(), decodeProof() lee numTxs del calldata codificado y lo devuelve junto con los datos de prueba decodificados. Ambos se pasan luego a processRollupProof():
function processRollup(bytes calldata, bytes calldata _signatures) external {
(bytes memory proofData, uint256 numTxs, uint256 publicInputsHash) = decodeProof();
processRollupProof(proofData, _signatures, numTxs, publicInputsHash, rollupBeneficiary);
}
Dentro de processRollupProof(), se llama a dos funciones de forma secuencial. Primero, verifyProofAndUpdateState() verifica la prueba ZK contra todas las transacciones decodificadas y actualiza el estado del rollup. Luego, processDepositsAndWithdrawals() gestiona la liquidación en L1, iterando solo los primeros _numTxs slots y llamando a decreasePendingDepositBalance() por cada depósito (esta llamada revierte si el usuario no depositó fondos realmente en la Etapa 1, vinculando el crédito del rollup a una transferencia real en L1):
function processRollupProof(bytes memory _proofData, bytes memory _signatures,
uint256 _numTxs, uint256 _publicInputsHash, address _rollupBeneficiary) internal {
verifyProofAndUpdateState(_proofData, _publicInputsHash); // ruta de prueba: todas las transacciones decodificadas
processDepositsAndWithdrawals(_proofData, _numTxs, _signatures); // ruta de liquidación: solo los primeros _numTxs
}
// dentro de processDepositsAndWithdrawals:
end := add(proofDataPtr, mul(_numTxs, TX_PUBLIC_INPUT_LENGTH))
while (proofDataPtr < end) {
// ... para cada depósito:
decreasePendingDepositBalance(assetId, publicOwner, publicValue);
}
Este diseño en dos etapas requiere que la lógica de liquidación en L1 procese exactamente el mismo conjunto de transacciones que verificó la prueba ZK. Si las dos rutas no coinciden en qué transacciones procesar, los depósitos pueden acreditarse en el estado del rollup sin consumir sus saldos pendientes en L1.
Análisis de la vulnerabilidad
En el contrato del procesador de rollup (0x7d65...2728), numTxs no estaba efectivamente acotado al conjunto de transacciones que impone la prueba ZK. Por tanto, la ruta de prueba y la ruta de liquidación podían procesar listas de transacciones distintas.
En el rollup_circuit fuera de cadena, num_txs se carga como testigo y solo se acota por rango. El circuito lo usa para determinar qué slots se tratan como transacciones reales, pero no verifica que num_txs sea igual al recuento real de pruebas que no son de relleno:
const auto num_txs = uint32_ct(witness_ct(&composer, rollup.num_txs));
field_ct(num_txs).create_range_constraint(MAX_TXS_BIT_LENGTH);
// ...
auto is_real = num_txs > uint32_ct(&composer, i); // controla la lógica de transacción real por slot
El probador puede establecer num_txs en cualquier valor dentro del rango permitido. Los slots más allá de num_txs siguen siendo verificados recursivamente, pero sus entradas públicas se ponen a cero, por lo que no contribuyen al estado del rollup:

En el lado de Solidity, decodeProof() lee numTxs de los metadatos del calldata que no se copian en el proofData reconstruido verificado por verifyProofAndUpdateState(). Por tanto, el límite del bucle de liquidación tampoco está cubierto por la prueba ZK:

Sin que ninguno de los dos lados acote este valor, un atacante podría establecer numTxs por debajo del número real de transacciones decodificadas. El bucle de liquidación entonces omitiría transacciones que la prueba ya había acreditado en el estado del rollup. Una transacción no ejecutable podría ocupar el primer slot decodificado (dentro del rango de escaneo de liquidación), mientras que un depósito real podría situarse en un slot posterior (probado por el circuito pero fuera del rango de escaneo de liquidación). La prueba acreditaría el depósito en el estado del rollup, pero la lógica de liquidación lo omitiría por completo, incluida la llamada a decreasePendingDepositBalance(). Esto dejaba el saldo de depósito pendiente sin consumir en L1 mientras el estado del rollup ya reflejaba el depósito.
Análisis del ataque
El siguiente análisis se basa en la transacción 0x074ec9...9aeeb1.
El atacante aprovechó la brecha entre la ruta de prueba y la ruta de liquidación en dos fases.
Fase 1: Creación de saldos sin respaldo
-
Paso 1: El atacante envió múltiples lotes de rollup, cada uno con dos transacciones decodificadas: una transacción no ejecutable (basura) en el slot 1 y un depósito real en el slot 2, con
numTxsestablecido en 1. La lógica de liquidación en L1 procesó únicamente la transacción basura del slot 1, omitiendo por completo el depósito real del slot 2. -
Paso 2: Sin embargo, la prueba ZK verificó y acreditó todas las transacciones decodificadas, incluido el depósito del slot 2. Como la lógica de liquidación nunca llegó a ese depósito,
decreasePendingDepositBalance()no fue llamado y el saldo de depósito pendiente en L1 permaneció sin consumir. El atacante repitió este patrón para siete activos distintos, acumulando saldos sin respaldo en el estado del rollup.
Fase 2: Extracción de fondos
- Paso 3: Una vez establecidos los siete saldos sin respaldo, el atacante inició retiros estándar para cada activo. Estos retiros parecían legítimos para la lógica de liquidación porque los saldos existían en el estado del rollup, por lo que el contrato en L1 liberó los fondos correspondientes — aproximadamente $2.15M en total.

Conclusión
Esta vulnerabilidad no fue una debilidad criptográfica, sino un error de consistencia de estado entre dos rutas de código críticas en la arquitectura del rollup. La causa raíz: numTxs no estaba vinculado al conjunto de transacciones probado en ninguno de los dos lados. El circuito solo lo acotaba por rango, y el decodificador de Solidity lo leía de metadatos del calldata no verificados. Sin esta vinculación, la ruta de prueba y la ruta de liquidación podían procesar listas de transacciones distintas. El atacante estableció numTxs por debajo del recuento real de transacciones para que la lógica de liquidación omitiera depósitos que la prueba ya había acreditado en el estado del rollup. Los saldos sin respaldo resultantes fueron retirados posteriormente mediante los flujos normales de liquidación.
El rollup de Aztec Connect anunció su cierre, con el procesamiento de transacciones y los retiros programados para finalizar el 31 de marzo de 2024 [2]. Sin embargo, el contrato del procesador de rollup fue actualizado el 10 de abril de 2024 mediante una pull request [3], y la lógica vulnerable está presente en esa actualización posterior al cierre.
La corrección requiere vincular numTxs al conjunto completo de transacciones verificadas por la prueba ZK, de modo que ambas rutas procesen siempre el mismo conjunto. Cualquier diseño de rollup que separe la verificación de pruebas de la liquidación en L1 debe garantizar que ambas rutas operen sobre un conjunto de transacciones idéntico y acotado de forma verificable. Una discrepancia en un solo parámetro puede convertir un sistema de pruebas sólido en un vector para la creación de saldos sin respaldo.
Referencias
- [1] Alerta de BlockSec Phalcon: Análisis del incidente de Aztec
- [2] Aviso de cierre de Aztec Connect
- [3] PR #67 de actualización de RollupProcessorV3
Más incidentes de esta semana
Raydium
El 10 de junio de 2026, cuatro pools del programa AMM v3 heredado de Raydium en Solana fueron explotados por aproximadamente $1.34M [1]. El manejador de retiros no verificó que una cuenta proporcionada por el llamador coincidiera con la contraparte almacenada en el pool, por lo que el atacante sustituyó una cuenta controlada para manipular el cálculo de pago. La misma técnica vació todas las reservas de cuatro pools en cuestión de segundos.
Contexto
El AMM de Raydium es un creador de mercado de producto constante en Solana. Cada pool mantiene dos bóvedas de tokens y acuña un token LP que representa una participación proporcional de las reservas. Cuando un proveedor de liquidez retira fondos, el manejador calcula el pago de forma proporcional y transfiere la parte correspondiente de ambas bóvedas:
coin_out = total_coin * withdraw_amount / lp_supply
pc_out = total_pc * withdraw_amount / lp_supply
En Solana, cada tipo de token está definido por una cuenta Mint que almacena el suministro total, los decimales y la autoridad de acuñación. El saldo de cada titular se almacena en una cuenta Token independiente vinculada a ese Mint — un Mint puede tener muchas cuentas Token en distintos titulares. Esto difiere del EVM, donde un único contrato ERC-20 gestiona internamente tanto la definición del token como todos los saldos.
En la fórmula de retiro anterior, lp_supply se lee de la cuenta Mint de LP del pool — la que rastrea el suministro total de LP. La corrección del cálculo depende de que este valor sea el Mint de LP real. Sin embargo, en Solana, el llamador pasa cada cuenta a cada instrucción de forma posicional, por lo que el manejador debe validar que cada cuenta proporcionada por el llamador coincida con la cuenta canónica almacenada en el estado del pool.
Análisis de la vulnerabilidad
El programa explotado (27haf8...8vQv) no era de código abierto, y sus datos ejecutables (ProgramData) fueron cerrados tras el ataque, lo que imposibilita la inspección directa del bytecode. El análisis a continuación se basa en el bytecode reconstruido a partir del último búfer de actualización del programa y contrastado con el comportamiento de las transacciones en cadena.
En el manejador de retiros, la cuenta Mint de LP proporcionada por el llamador no estaba vinculada al amm.lp_mint registrado en el pool. El siguiente pseudocódigo de ingeniería inversa, reconstruido a partir del bytecode en cadena, muestra el diseño de cuentas. El manejador verificó las vinculaciones del estado del pool, la autoridad PDA, ambas bóvedas y las cuentas de usuario — pero no la del Mint de LP en el slot 5:
let amm_info = next_account_info(it)?; // accounts[1] — estado del pool (contiene amm.lp_mint)
// ...
let amm_lp_mint_info = next_account_info(it)?; // accounts[5] — mint proporcionado por el llamador
let amm = AmmInfo::load(amm_info)?;
// aquí se verifican las vinculaciones de autoridad, bóvedas y open_orders...
// >>> FALTA: verificar que accounts[5].key == amm.lp_mint <<<
let lp_mint = Mint::unpack(&amm_lp_mint_info.data.borrow())?;
let lp_mint_supply = lp_mint.supply; // lee del mint no verificado
let coin_amount = total_coin * withdraw_amount / lp_mint_supply;
let pc_amount = total_pc * withdraw_amount / lp_mint_supply;
Dado que la cuenta Mint de LP no estaba vinculada, un atacante podía sustituirla por una cuenta Mint que controlara completamente. Estableciendo su supply total en 1 y quemando 1 token se obtenía una ratio de pago de 1 / 1 = 100% de cada reserva.
El código vulnerable había estado activo y sin cambios desde la última actualización del programa el 3 de enero de 2023, aproximadamente 1.254 días antes del exploit.
Análisis del ataque
El siguiente análisis se basa en la transacción 1csN6v...3s7s.
- Paso 1: El atacante creó una cuenta Mint de LP falsa con
decimals = 0ysupplytotal = 0.

- Paso 2: El atacante inicializó una cuenta Token vinculada al Mint de LP falso, luego acuñó exactamente 1 token en ella (como autoridad de Mint), fijando el
supplytotal del Mint en 1.

- Paso 3: El atacante llamó a la función de retiro, pasando el Mint de LP falso en el slot de cuenta esperado y la cuenta Token del Paso 2 (que contenía 1 token LP falso) como origen de LP. Con
withdraw_amount = 1ylp_supply = 1, el manejador calculótotal_coin * 1 / 1ytotal_pc * 1 / 1, lo que equivalía al 100% de ambas reservas (893.700USDCy 66.837RAYpara el pool RAY/USDC).

- Paso 4: El manejador quemó el 1 token del atacante y transfirió la totalidad de las reservas fuera de ambas bóvedas del pool, vaciando por completo el pool RAY/
USDC.

El atacante repitió el mismo patrón contra otros tres pools en aproximadamente 15 segundos. En los cuatro pools, los montos drenados fueron:
| Pool | Drenado (aprox.) |
|---|---|
| RAY/USDC | ~66.837 RAY + ~893.700 USDC |
| RAY/wSOL | ~74.720 RAY + ~5.603 wSOL |
| RAY/SRM | ~8.622 RAY + ~10.692 SRM |
| RAY/Sollet ETH | ~5.038 RAY + ~16 Sollet ETH |
Conclusión
La causa raíz es una única verificación de validación de cuenta ausente: el manejador de retiros usó el supply de una cuenta Mint proporcionada por el llamador como divisor del suministro de LP sin vincularlo al amm.lp_mint registrado en el pool. En Solana, cada cuenta proporcionada por el llamador debe vincularse a su contraparte canónica almacenada en el estado del pool. Una implementación correcta debe rechazar cualquier Mint de LP cuya clave no coincida con el registro almacenado del pool, y calcular la redención a partir de un contador de LP interno al pool en lugar del supply del Mint suministrado externamente. El contrato explotado era un despliegue más antiguo (última actualización en enero de 2023) que fue cerrado el mismo día del ataque. Según el equipo de Raydium, la compensación total será gestionada por el tesoro de Raydium [1].
Referencias
Acerca de BlockSec
BlockSec es un proveedor integral de seguridad blockchain y cumplimiento de criptomonedas. 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 de AML/CFT, a lo largo de todo el ciclo de vida de protocolos y plataformas.
BlockSec ha publicado múltiples artículos de seguridad blockchain en conferencias de prestigio, ha reportado varios ataques de día cero en aplicaciones DeFi, ha bloqueado múltiples hackeos para rescatar más de 20 millones de dólares y ha protegido miles de millones en criptomonedas.
-
Sitio web oficial: https://blocksec.com/
-
Cuenta oficial de Twitter: https://twitter.com/BlockSecTeam



