Back to Blog

~$23M perdidos: exploits en Cosmos EVM y Moonwell | BlockSec Weekly

Code Auditing
3 de septiembre de 2026
30 min read
Key Insights
  • Cinco incidentes esta semana causaron aproximadamente $22.7M en pérdidas en Ethereum, Solana, Base, Cronos, y otras seis cadenas Cosmos EVM como TAC.

  • El código compartido convirtió dos incidentes en eventos multiprotocolo. La falla en el manejo de saldos de Cosmos EVM explotó seis cadenas Cosmos EVM la misma semana, incluyendo TAC Chain; el contrato de tarjeta desactualizado de Rain expuso a Avici, Tria y otros programas de tarjetas. Un defecto en una infraestructura común puede propagarse a cada componente vulnerable que la ejecute en el flujo posterior.

  • La manipulación de precios fue el tipo de ataque más común, detrás de dos de los cinco incidentes y una gran parte de las pérdidas de la semana. Los exploits de Moonwell y Tectonic tomaron la misma forma en mercados estilo Compound: cada uno infló tanto un precio de oráculo como la tasa de cambio del token de recibo del mercado, y luego pidió préstamos contra el colateral sobrevalorado.

Durante el período del informe (22/08/2026 - 30/08/2026), observamos 5 incidentes de seguridad con una pérdida total estimada de aproximadamente $22.7M.

Fecha Incidente Tipo Pérdida estimada
20/08/2026* La serie de exploits de Cosmos EVM (MANTRA, TAC, KiiChain, +3) Subdesbordamiento/Desbordamiento aritmético ~$5.7M*
27/08/2026 Moonwell Manipulación de precios ~$9.1M
28/08/2026 Ajna Lógica de negocio incorrecta ~$775K
28/08/2026 La serie de exploits del contrato Rain Card (Avici, Tria, y otros) Elusión de verificación de firmas ~$1.1M†
30/08/2026 Tectonic Manipulación de precios ~$6M‡

*La serie de Cosmos EVM comenzó el 20/08 (MANTRA), antes del período de este informe semanal y no cubierta en el informe de la semana pasada; se incluye aquí para completar el panorama. Los ~$5.7M es lo que los atacantes obtuvieron a través de las seis cadenas afectadas (~$2.87M vía DEXes y ~$2.85M vía exchanges centralizados, desde entonces congelados), a precios del 19 de agosto, según el post-mortem oficial de Cosmos. Los drenajes nominales fueron mayores donde se conoce la cantidad de tokens (TAC ~3B TAC, ~$7.5M; MANTRA 720.9M tokens, ~$3.6M; KiiChain 148.3M KII), pero la mayoría de los tokens no se vendieron, fueron congelados o son recuperables en la cadena.

†El ~$1.1M es el agregado estimado a través de los programas soportados por Rain expuestos por el contrato compartido; Avici y Tria son los dos más grandes, revelando ~$500,859 (1,685 usuarios) y ~$431,945 (636 usuarios) respectivamente.

‡El ~$6M es la pérdida realizada, transferida mediante puente a Ethereum antes de la reversión. Las estimaciones del total drenado van desde ~$74M (rastreado en las billeteras del atacante) hasta ~$119.5M (salida bruta del mercado); la mayor parte permaneció en Cronos y fue eliminada cuando los validadores revirtieron la cadena a su estado previo al exploit. Ni Tectonic ni Cronos han confirmado una cifra final de pérdidas.

El mejor auditor de seguridad para Web3

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

Destacado semanal: La serie de exploits de Cosmos EVM (rastreada en la cadena TAC)

Esta serie fue seleccionada por lo que revela sobre la divulgación de infraestructura compartida: debido a que el error fue mal evaluado como no constitutivo de una amenaza de seguridad grave, su corrección se lanzó como un parche público silencioso en lugar de mediante una distribución privada coordinada, y luego un fork de terceros describió la ruta del exploit abiertamente, exponiendo a todas las cadenas sin parchear que aún ejecutaban el módulo.

Entre el 20/08/2026 y el 25/08, los atacantes ejecutaron una única cadena de exploits que combinaba dos vulnerabilidades en el módulo compartido cosmos/evm a través de seis cadenas Cosmos EVM. Ambos errores se encontraban en el código que concilia el estado de la EVM con el libro contable x/bank de Cosmos: un subdesbordamiento de saldo y un desbordamiento coincidente, encadenados dentro de una única transacción neutral en cuanto a la oferta.

Según el post-mortem oficial de Cosmos, el error fue reportado a través del programa de recompensas en abril y inicialmente fue mal evaluado como no amenazante para los fondos de producción, por lo que pasó por el proceso de parche público silencioso y se lanzó en las versiones v0.6.2 y v0.7.2 el 19/08. El 20/08, un fork de terceros describió públicamente la ruta del exploit, y los primeros drenajes comenzaron horas después, afectando primero a MANTRA (20/08), luego a TAC y KiiChain (22/08) [1][2]. A través de las seis cadenas, los atacantes obtuvieron aproximadamente $5.7M a precios del 19 de agosto (alrededor de $2.87M vendidos en DEXes y ~$2.85M en exchanges centralizados, desde entonces congelados) [1].

Este informe analiza TAC Chain en detalle como el ejemplo desarrollado; fue la cadena más afectada de la serie, con una pérdida en el pool de staking de ~$7.5M nominal [3].

Contexto

TAC Chain ejecuta tanto el Cosmos SDK como la EVM. Una dirección tac1... y una dirección 0x... comparten los mismos 20 bytes subyacentes, por lo que una sola dirección puede crearse primero como una cuenta de vesting de Cosmos y luego tener un contrato EVM desplegado en ella. Estas no son dos cuentas separadas: la misma dirección lleva simultáneamente el estado de vesting y el código del contrato. Ambas se ejecutan en paralelo, y TAC expone acciones nativas de Cosmos como el staking a los llamadores EVM a través de contratos precompilados en direcciones fijas, de manera que un contrato EVM puede invocarlas como cualquier otra llamada.

El saldo total en el Banco de una cuenta de vesting incluye tokens bloqueados. La porción bloqueada no puede transferirse antes de que se libere, mientras que el saldo disponible es lo que queda después de restar el monto bloqueado del total. Cuando la EVM carga una cuenta, inicializa el saldo del StateDB a partir del saldo disponible, y las transferencias del contrato durante la ejecución operan sobre ese saldo.

Al final de una transacción, los cambios de saldo en el StateDB se liquidan de vuelta en x/bank, el libro contable autorizado, y solo lo que se liquida allí es TAC real y transferible. TAC ejecuta la línea v0.7.x, que escribe un saldo EVM directamente en x/bank, pero solo si el valor sobrevive una conversión de uint256 a int256, por lo que un saldo cercano a 2^256 no puede liquidarse en absoluto.

Los tokens bloqueados no pueden transferirse, pero aún pueden delegarse para staking. La delegación verifica el saldo total en el Banco, que incluye los tokens bloqueados, y luego mantiene la restricción de vesting a través de DelegatedVesting. Para una cuenta totalmente bloqueada con total=1, delegar esa unidad reduce el saldo total del Banco a 0 y actualiza DelegatedVesting, mientras que el saldo disponible permanece correctamente en 0 tanto antes como después.

Análisis de vulnerabilidad

El componente defectuoso es la sincronización de saldos en el manejador compartido cosmos/evm que se ejecuta en cada llamada con estado al precompilado de staking, expuesto a través del precompilado de staking en 0x0000...0800 e implementado en [4]. Concilia los saldos EVM con el libro contable de Cosmos mediante aritmética uint256 sin verificación, y esto expone dos defectos complementarios: un subdesbordamiento de saldo en la ruta de reescritura del staking, y un desbordamiento correspondiente en la ruta de suma ordinaria. Ninguno es peligroso por sí solo; el riesgo proviene de su combinación en una ruta aritmética compartida.

El primer defecto es un subdesbordamiento en la reescritura del saldo. Cuando se ejecuta una llamada con estado al precompilado de staking, la acción nativa de Cosmos se ejecuta entre un hook BeforeBalanceChange y uno AfterBalanceChange, y AfterBalanceChange es responsable de reflejar el cambio de saldo nativo de vuelta en el StateDB de la EVM.

La delegación se valida contra el saldo total del Banco, por lo que una cuenta totalmente bloqueada puede pasar la verificación de delegación mientras su saldo disponible permanece en 0. La delegación nativa deduce el monto delegado del saldo total del Banco y emite un evento coin_spent. El defecto está en cómo AfterBalanceChange consume ese evento: en lugar de recargar el saldo disponible actual de la cuenta y asignarlo, reproduce el monto de coin_spent como stateDB.SubBalance(spender, amount), restándolo del saldo EVM existente.

Debido a que SubBalance realiza la resta con aritmética uint256 sin verificación, cualquier cuenta cuyo saldo EVM sea menor que el monto del evento produce un subdesbordamiento. Para una cuenta con saldo EVM 0 y un monto de coin_spent de 1, 0 - 1 da la vuelta hasta MAX_UINT256.

El segundo defecto es un desbordamiento correspondiente en la ruta de suma. Cada transferencia de saldo ejecuta SubBalance en el remitente y AddBalance en el destinatario a través del mismo StateDB, por lo que ambas direcciones comparten una única ruta aritmética.

Ambos se resuelven en las mismas primitivas de stateObject, donde AddBalance y SubBalance calculan new(uint256.Int).Add(s.Balance(), amount) y .Sub(s.Balance(), amount) sin verificación de rango. Al igual que la resta produce un subdesbordamiento por debajo de 0, una suma suficientemente grande produce un desbordamiento por encima de MAX_UINT256 y vuelve a dar la vuelta hacia abajo.

Los dos defectos son complementarios. Por sí solo, el subdesbordamiento produce un saldo MAX_UINT256 que es inerte, porque un valor cercano a 2^256 no puede sobrevivir la conversión de uint256 a int256 que requiere la liquidación en x/bank. La suma sin verificación es la contraparte: es la única ruta que puede devolver dicho saldo desproporcionado por debajo de ese límite de liquidación.

Análisis del ataque

El siguiente análisis se basa en la transacción 0xae4e9b...da46fc.

  • Paso 1: En la transacción 0x4da591...df1af7, el atacante desplegó una fábrica CREATE2 para que la dirección del contrato de ataque 0x5711...c978 pudiera calcularse antes de que el contrato fuera desplegado.

  • Paso 2: En la transacción 95F43742...6A885BA, el atacante usó MsgCreateVestingAccount para transferir y bloquear una unidad base (1utac) en esa futura dirección de contrato, dándole un saldo total en el Banco que podía pasar la verificación de delegación mientras mantenía su saldo disponible en 0.

  • Paso 3: En la transacción 0x2400f8...c57c81, el atacante usó CREATE2 para desplegar el contrato de ataque en la misma dirección 0x5711...c978, convirtiéndola tanto en una cuenta de vesting de Cosmos como en un contrato EVM, de manera que una cuenta externa pudiera pagar el gas mientras la dirección del contrato permanecía como el delegador con spendable=0.
  • Paso 4: El contrato de ataque delegó la unidad base bloqueada a través del precompilado de staking. El flujo de staking aceptó la delegación contra el saldo total del Banco, y la reescritura del saldo hizo entonces que el saldo EVM del contrato diera la vuelta hasta MAX_UINT256.

  • Paso 5: El saldo envuelto de MAX_UINT256 no podía liquidarse de vuelta en el libro contable de Cosmos tal cual, por lo que el contrato de ataque primero lo redujo a un valor liquidable. Envió casi todo el saldo a bonded_tokens_pool, la cuenta más grande de la cadena y la que contiene todo el TAC en staking, eligiendo el monto de manera que el saldo del pool sufriera un desbordamiento al sumarse y diera la vuelta hasta 0. Debido a que ese monto se dedujo del propio MAX_UINT256 del contrato de ataque, el contrato quedó en posesión exactamente del saldo anterior del pool, sin crear nueva oferta. Reducir a cero el pool toma todo su saldo, lo máximo que este desbordamiento podía producir, razón por la cual el atacante apuntó en primer lugar a la cuenta más grande de la cadena.

  • Paso 6: El contrato de ataque transfirió luego ese saldo, 2,985,651,403.40 TAC, a la dirección del atacante.

Nuestro análisis a nivel de transacción coincide con el post-mortem oficial de Cosmos, que describe dos vulnerabilidades encadenadas: el subdesbordamiento anterior produce el saldo anómalo, y el desbordamiento del Paso 5 del Análisis del ataque lo convierte en los fondos reales del pool [1]. Un post-mortem anterior de KiiChain, publicado antes de ese informe oficial, sostuvo que estaban involucrados al menos tres defectos ascendentes y que solo el subdesbordamiento ha sido parcheado [2]. La cobertura de prensa independiente resumió el mismo desacuerdo sobre cuántos defectos ascendentes permanecen [5].

Conclusión

La causa raíz de la serie de exploits de Cosmos EVM fue una inconsistencia en la forma en que las capas EVM y Cosmos contabilizaban el mismo saldo, combinada con aritmética que nunca fue verificada en sus límites. Cuando dos entornos de ejecución comparten un mismo libro contable, deben coincidir en la semántica de saldos hasta el nivel de cada cuenta individual, y cada cambio de saldo debe verificarse tanto para desbordamiento como para subdesbordamiento. Debido a que la falla residía en un módulo compartido en lugar del código de una sola cadena, un defecto expuso a todas las cadenas que lo ejecutaban, lo que convirtió un solo error en un evento multicadena. Más allá del código, el incidente es una lección en evaluación de severidad. La vulnerabilidad fue evaluada inicialmente como no amenazante para los fondos de producción, por lo que su corrección se lanzó como un parche público silencioso; para cuando esa evaluación fue corregida, el parche ya era público, y la divulgación de la ruta del exploit por parte de un tercero convirtió el error de evaluación en seis cadenas explotadas. Un error de infraestructura compartida que puede mover fondos reales necesita una distribución privada y coordinada desde el principio, no un parche público silencioso que cualquiera pueda descifrar.

Comience con Phalcon Explorer

Sumérjase en las transacciones para actuar con inteligencia

Pruébelo gratis ahora

Más incidentes esta semana


Moonwell

El 27/08/2026, Moonwell en Base fue explotado por aproximadamente $9.1M mediante la combinación de una inflación en la contabilidad de garantías con la manipulación del precio del oráculo de MAMO, un activo de baja liquidez listado en su Core Market. Más allá de elevar el precio de MAMO, el atacante transfirió MAMO directamente al contrato del mercado mMAMO sin acuñar acciones (shares), lo que elevó el respaldo de cada acción e infló el valor de la garantía además del movimiento de precio. Contra la garantía doblemente inflada, el atacante tomó aproximadamente $11.03M en préstamos brutos entre cbBTC, WETH, USDC y wstETH, dejando alrededor de $9.13M en obligaciones residuales después de las liquidaciones [6][7].

Análisis de vulnerabilidad

El Core Market de Moonwell funciona con código de Compound v2, donde una acción del mercado (mMAMO) vale (cash + totalBorrows - totalReserves) / totalSupply. Aquí se combinan dos debilidades. Primero, el límite de oferta de MAMO solo verifica la ruta formal de acuñación, de manera que una transferencia directa de MAMO al contrato mMAMO se suma al efectivo del mercado sin acuñar acciones; esto eleva la tasa de cambio calculada (exchangeRateStored()) y aumenta el valor de garantía de cada acción existente mientras elude por completo el límite. Segundo, MAMO fue listado como garantía con un factor de garantía del 50% a pesar de su baja liquidez, por lo que su precio de oráculo puede moverse con un capital moderado. Debido a que el valor de la garantía se calcula como acciones multiplicadas por la tasa de cambio multiplicada por el precio del oráculo, tanto la tasa de cambio como el precio son superficies manipulables, y al inflarlas juntas se multiplica el poder de endeudamiento contra activos mucho más líquidos.

Análisis del ataque

El siguiente análisis se basa en la transacción 0x09687d...395593e. La operación se inició con aproximadamente $1.947M (799 ETH convertidos a USDC y transferidos mediante puente a Base); el volumen bruto de compra de MAMO alcanzó aproximadamente $7.50M una vez que los activos prestados fueron reciclados.

  • Paso 1: El atacante suministró MAMO formalmente para acuñar mMAMO, luego transfirió 53,393,290 MAMO directamente al contrato mMAMO en dos transacciones que no emitieron ningún evento Mint, elevando la tasa de cambio calculada del mercado en aproximadamente 3.68x y revaluando las acciones mMAMO que el atacante acababa de acuñar junto con las de todos los demás poseedores.

  • Paso 2: El atacante compró MAMO a través de pools de DEX mientras la liquidez era baja, llevando el feed MAMO/USD desde aproximadamente $0.0106 hasta aproximadamente $0.43.

  • Paso 3: Con tanto la tasa de cambio como el precio inflados, el atacante completó 18 préstamos entre cbBTC, WETH, USDC y wstETH (aproximadamente $11.03M brutos), luego convirtió y consolidó las ganancias, transfiriendo mediante puente aproximadamente 8.729M USDC a Ethereum a través de CCTP y convirtiéndolos en aproximadamente 8.728M DAI.

Conclusión

La causa raíz fue que Moonwell valoraba un activo de garantía de baja liquidez en dos superficies manipulables de forma independiente al mismo tiempo: su precio de oráculo, que la baja liquidez permitió al atacante mover con capital moderado, y la tasa de cambio de su token de recibo, que una transferencia directa del activo subyacente infló porque el límite de oferta solo protegía la ruta formal de acuñación. Dado que el valor de la garantía multiplica ambos factores, inflarlos juntos multiplicó el poder de endeudamiento contra activos mucho más líquidos. Un mercado de préstamos debería tratar tanto el precio como la contabilidad de acciones de la garantía como manipulables: excluir las transferencias no solicitadas del cálculo de la tasa de cambio, y combinar factores de garantía conservadores con precios sensibles a la liquidez y límites estrictos de oferta y préstamo para activos de baja liquidez.

Ajna

El 28/08/2026, Ajna fue explotado por aproximadamente $775K a través de siete pools de Ethereum mediante una falla de lógica de negocio en su ruta de liquidación. Ajna es un protocolo de préstamos que no usa oráculo externo; valora las posiciones a través del LUP (Lowest Utilized Price, precio más bajo utilizado) derivado de la liquidez de los buckets. El atacante primero manipuló el LUP para crear una posición severamente insolvente, luego hizo que el protocolo liquidara una posición controlada a un precio de subasta holandesa que el protocolo aún mantenía muy por encima del mercado, de manera que las reclamaciones de depósitos del bucket se consumieron a su valor nominal en el token de cotización para pagar la deuda mientras el atacante recibía garantía real a cambio [8].

Contexto

Ajna es un protocolo sin permisos donde cualquiera puede crear un pool de tokens emparejados para suministrar y pedir prestado, similar a un pool de Uniswap. Cada pool se divide en buckets, similares a los ticks de precio en Uniswap V3, donde cada tick representa una cantidad fija del token de cotización por unidad de garantía. Los prestamistas eligen un bucket según su ratio préstamo-valor preferido y suministran liquidez allí, recibiendo LP (una reclamación sobre los depósitos de ese bucket) a cambio. Debido a que no hay un oráculo externo, el LUP se deriva directamente de la distribución de depósitos del bucket y de la deuda total.

Una posición se vuelve liquidable una vez que su threshold price (su deuda dividida por su garantía) supera el LUP. Cualquiera puede entonces hacer un kick para llevarla a liquidación depositando un bono, lo que abre una subasta holandesa. El precio de la subasta no está vinculado al mercado spot: comienza en 32 veces un precio de referencia y se reduce a la mitad cada hora, y se mantiene fijo durante la primera hora (un período de cura) antes de que comience a decaer.

La garantía subastada puede tomarse de dos maneras. Un llamador puede hacer take() de la garantía pagando el token de cotización al precio actual de la subasta, o llamar a bucketTake(), que usa los depósitos de cotización de un bucket elegido para comprar la garantía subastada y pagar al prestatario. En un bucketTake(), la garantía se acredita en ese bucket, por lo que sus prestamistas son quienes la adquieren; para un bucket take de arbitraje, además se le paga al llamador recompensas en LP (una reclamación sobre ese bucket) sobre el diferencial entre el precio del bucket y el precio de la subasta, como incentivo para activar la liquidación. El diseño asume que un tomador solo interviene una vez que el precio de la subasta ha caído hasta igualar o por debajo de lo que vale la garantía: sin oráculo, esta subasta descendente es la forma en que Ajna descubre un precio justo, y los prestamistas de un bucket obtienen ganancias adquiriendo garantía por debajo del precio de su propio bucket. La subasta en sí solo termina cuando la deuda del prestatario se salda o el préstamo vuelve a estar garantizado; una ruta de liquidación separada resuelve una subasta después de un período de gracia de 72 horas, o antes si no queda garantía, absorbiendo cualquier deuda incobrable residual y devolviendo la garantía sobrante al prestatario.

Tres detalles de implementación de esta maquinaria determinan las cifras que produce un take. Primero, el precio de la subasta no se deriva del mercado; decae como 32 * max(kickMomp, neutralPrice) * 2^(-max(elapsedHours - 1, 0)), por lo que se mantiene fijo durante el período de cura de la primera hora y solo entonces comienza a caer, permaneciendo cerca de 32 veces su precio de referencia poco después de ese período.

Segundo, un kick solo se admite cuando el prestatario ya está infragarantizado en su deuda previa a la penalización: _kick() ejecuta la verificación _isCollateralized() y revierte con BorrowerOk() antes de agregar la penalización de tres meses de interés, por lo que esa penalización aumenta la deuda registrada después del kick sin haber ayudado a que la posición calificara.

Tercero, el primer take en una subasta agrega una penalización del 7% a la deuda del prestatario antes de que se calculen los montos de pago y garantía para el take, por lo que un solo bucketTake() no necesita saldar la deuda por sí solo.

Análisis de vulnerabilidad

El contrato defectuoso es el pool de Ajna (0xad24...178e); la lógica de liquidación descrita aquí proviene de ese código fuente verificado. Dentro de bucketTake(), el protocolo usa las reclamaciones de depósito de un bucket a su valor nominal en el token de cotización para pagar la deuda de un prestatario en subasta, y, en un bucket take de arbitraje, paga al tomador recompensas en LP basadas en el diferencial entre el precio del bucket y el precio de la subasta.

Nunca verifica el valor real recuperable de esas reclamaciones de depósito ni si el precio de la subasta es económicamente razonable; simplemente asume que las reclamaciones pueden redimirse más tarde a su valor nominal. Los dos precios de los que depende se calculan de forma independiente, y ninguno se valida contra el otro: el LUP sigue la distribución de depósitos del bucket y la deuda total del pool, mientras que el precio de la subasta decae puramente en función del tiempo transcurrido desde un precio de referencia fijado en el kick. El valor nominal en la cadena de una reclamación puede, por tanto, divergir de lo que realmente puede recuperar, y bucketTake() sigue saldando la deuda contra ese valor nominal.

Internamente, bucketTake() se enruta a través de _takeBucket() hacia _rewardBucketTake(), donde, en un take de arbitraje, la recompensa en LP del tomador se calcula como la garantía tomada multiplicada por el diferencial entre el precio del bucket y el precio de la subasta.

Análisis del ataque

Lo siguiente se basa en el análisis en cadena del pool cbETH, uno de los siete afectados, usando las transacciones 0x8a8793...016e64 y 0x12dfde...14e4f5.

  • Paso 1: En la transacción de preparación, el atacante agregó aproximadamente 49.343 WETH de liquidez de cotización al índice de bucket 2000, un precio muy por encima del mercado. Respaldado por ese bucket inflado, el atacante abrió una posición casi sin garantía, depositando aproximadamente 0.001 cbETH para pedir prestados aproximadamente 49.319 WETH. Esta posición no mantiene casi ninguna garantía, por lo que no es el premio; cumple dos propósitos de preparación. Primero, como una deuda insolvente arrastra el LUP hacia abajo una vez que el mercado vuelve a la normalidad. Segundo, dado que casi todos los 49.343 WETH depositados en el bucket 2000 ahora se han vuelto a pedir prestados, dejando solo una cantidad residual, el bucket 2000 queda con una reclamación de depósito cuyo valor nominal, aproximadamente 49.343 WETH, excede con creces lo que realmente puede recuperar; esa reclamación deteriorada es la munición que el atacante gasta más tarde a su valor nominal en el Paso 4.
  • Paso 2: En la misma transacción de preparación, el atacante usó una segunda dirección controlada, 0x02d329...6f5f, para abrir una posición de prestatario en el nivel de precio normal, depositando aproximadamente 48.128 cbETH de garantía y pidiendo prestados aproximadamente 49.319 WETH contra ella. Esto devolvió el precio de mercado a un rango normal, dejando la primera posición severamente insolvente mientras la segunda posición se ubicaba justo alrededor de su umbral de salud. Esta segunda posición, que contiene la garantía real, es la que el atacante realmente pretende drenar; la primera posición insolvente es solo la palanca que movió el LUP.

  • Paso 3: El atacante luego hizo kick a la segunda posición (la abierta por 0x02d329...6f5f) para llevarla a subasta en lugar de la primera insolvente. Un kick solo se acepta si la posición ya está infragarantizada en su deuda previa a la penalización contra el LUP actual, y la primera posición insolvente ya había arrastrado el LUP lo suficientemente bajo para que la deuda acumulada de la segunda posición alcanzara ese umbral. Solo después de la verificación de elegibilidad el protocolo agrega una penalización de kick de tres meses de interés, razón por la cual la deuda registrada después del kick asciende a aproximadamente 49.362 WETH. El kick abrió la subasta holandesa de la posición.

  • Paso 4: El atacante esperó hasta justo después del período de cura de una hora, cuando el precio de la subasta apenas había comenzado a decaer y aún estaba cerca de 32 veces el precio de referencia. Al llamar a bucketTake() sobre la segunda posición en el bucket 2000, el mismo bucket que ahora contenía la reclamación deteriorada, la liquidó a ese precio aún alto, muy por encima del mercado. Debido a que este fue el primer take en la subasta, el protocolo agregó una penalización del 7% a la deuda del prestatario antes de calcular los montos de pago y garantía del take. La garantía se valora al precio de la subasta, por lo que consumir aproximadamente 49.34 WETH del depósito del bucket 2000 solo pagó una parte de esa deuda y removió solo aproximadamente 1.51 cbETH de garantía, dejando la mayor parte de la garantía sin tocar mientras el llamador recolectaba una gran cantidad de recompensas en LP sobre el diferencial.
  • Paso 5: El atacante redimió esas recompensas en LP a través de removeCollateral(), retirando parte de la ganancia en forma de garantía.

  • Paso 6: Para cerrar la operación, el atacante tomó un préstamo flash de Balancer y llamó al take() de Ajna para pagar la deuda residual que el bucketTake() había dejado, esta vez con token de cotización real, hasta que la deuda del prestatario llegó a cero y la subasta finalizó. Una llamada final a repayDebt() retiró entonces la garantía ahora sin gravamen, aproximadamente 46.51 cbETH, con quoteRepaid=0: no se devolvió ningún token de cotización por ella. La ganancia se produjo a expensas del pool. El déficit no desapareció, se movió: con la segunda posición cerrada, el préstamo aún impago y casi sin respaldo de la primera posición quedó en el pool como deuda incobrable soportada por los demás prestamistas.

Conclusión

La causa raíz es que la ruta de liquidación de Ajna salda la deuda de un prestatario en subasta contra las reclamaciones de depósito de un bucket a su valor nominal, sin verificar su valor real recuperable ni si el precio de la subasta es económicamente razonable. Esa verificación faltante es lo que permitió que una reclamación deteriorada fabricada se gastara a su valor nominal para drenar garantía real, dejando el déficit en el pool como deuda incobrable. La ruta de liquidación debería valorar las reclamaciones de depósito por su monto real recuperable en lugar de su valor nominal, confirmar que el precio de la subasta es económicamente razonable antes de completar un take, y protegerse contra el consumo de reclamaciones que la deuda incobrable existente de un pool ya haya deteriorado. Los contratos del pool afectado son inmutables y no exponen ninguna pausa administrativa, por lo que una vez que comenzó el drenaje no hubo forma de detenerlo; los usuarios solo pudieron salir por su cuenta.

La serie de exploits del contrato Rain Card (rastreada en Avici)

El 28/08/2026 (UTC), una versión desactualizada del programa compartido de Rain para garantías de tarjetas en Solana fue explotada mediante una elusión de la verificación de firmas Ed25519. Al hacer que el programa aceptara una aprobación de administrador falsificada, el atacante tomó control de las cuentas de garantía de usuarios y drenó los saldos de tokens que contenían. Debido a que la falla residía en código de programa compartido entre los programas de tarjetas impulsados por Rain, un solo error expuso a todos ellos a la vez: el exploit drenó un estimado de ~$1.1M en total, siendo Avici y Tria los dos más grandes, revelando aproximadamente $500,859 (1,685 usuarios) y $431,945 (636 usuarios) respectivamente [9].

El análisis a continuación usa Avici como el ejemplo desarrollado.

Contexto

En Solana, las verificaciones de firma las realiza un precompilado nativo Ed25519 como parte del procesamiento de la transacción: si una firma referenciada no es válida, toda la transacción falla. Un programa de negocio no recibe ese resultado directamente; inspecciona las otras instrucciones en la misma transacción (a través del sysvar de Instructions) y confía en que la validación haya pasado. Cuando un usuario recarga una tarjeta impulsada por Rain (como con Avici), el saldo se mantiene en una cuenta de garantía por usuario gestionada por el programa compartido de Rain, y la autoridad de token de esa cuenta se deriva del programa, por lo que el administrador de la cuenta puede mover los activos de esa cuenta a través del flujo de transferencia del programa.

Cambiar el administrador de una cuenta de garantía requiere dos firmas. Una debe provenir del administrador designado por el protocolo; el otro firmante no tiene ningún requisito especial de identidad. Este diseño se basa en la autorización fuera de la cadena: una vez que el administrador del protocolo ha firmado el mensaje de cambio de administrador, el programa lo trata como aprobado.

Una instrucción de verificación Ed25519 comienza con un byte que cuenta las firmas a verificar y un byte de relleno, seguido de una estructura Ed25519SignatureOffsets por cada firma. Cada estructura especifica no solo los desplazamientos de byte de la firma, la clave pública y el mensaje, sino también el índice de instrucción del que debe leerse cada uno de ellos [10]. Estos índices son una característica intencionada: permiten que una verificación lea sus entradas de los datos de cualquier instrucción indexada en la transacción, por ejemplo para verificar una firma sobre los datos de otra instrucción sin copiarlos. El verificador nativo simplemente lee lo que sea que los desplazamientos y los índices de instrucción apunten.

Análisis de vulnerabilidad

La falla está en el programa de garantía (3zVB...yBzDuc): confía en una clave pública sin confirmar que esa clave sea la que el verificador realmente comprobó. Para registrar el firmante que aprueba como administrador, lee una clave pública de una posición fija dentro de la instrucción de verificación Ed25519 que inspecciona, y luego toma la mera presencia de una verificación aprobada como prueba de que el poseedor de esa clave firmó el mensaje de cambio de administrador.

Lo que nunca confirma es dónde miró realmente esa verificación. Debido a que esos campos de índice de instrucción son controlados por el llamador y el entorno de ejecución entrega los datos de cada instrucción al verificador nativo, la instrucción de verificación puede contener una clave pública en su propio cuerpo mientras sus índices de instrucción dirigen al verificador hacia una instrucción diferente, para verificar una firma bajo una clave pública distinta sobre un mensaje obtenido por separado.

Así que existen dos lecturas de "la clave del administrador" sin nada que las vincule: el programa confía en la clave que se encuentra en la instrucción que inspecciona, mientras que el verificador solo comprobó lo que sea que los índices apuntaban. La clave del administrador puede estar presente en la transacción como datos aunque nunca se haya verificado ninguna firma válida bajo esa clave. Ese desacople es la vulnerabilidad, y la verificación faltante es la vinculación que obligaría a que la clave pública y el mensaje que el verificador realmente verificó sean la misma clave y el mismo mensaje en los que confía el programa.

Análisis del ataque

El siguiente análisis se basa en la transacción ZmpBgn...mqWL.

  • Paso 1: El atacante construyó una transacción con dos instrucciones de verificación Ed25519 seguidas de SubmitSignatures. La instrucción 0 llevaba la propia clave pública del atacante (cafa…53db) y una firma real sobre el mensaje de cambio de administrador, dando a la transacción una verificación Ed25519 genuinamente válida.
  • Paso 2: La instrucción 1 colocó la clave pública del administrador del protocolo (a2fc…959a) en el slot de clave pública, pero rellenó el slot de firma con bytes falsos 0x09, por lo que esta instrucción fallaría si sus propios datos fueran realmente verificados.

  • Paso 3: El atacante configuró el encabezado Ed25519 de la instrucción 1 como 01003000000010000000700020000000. Solo los tres campos de índice de instrucción realizan la redirección, y decodificados son todos cero, de manera que la firma, la clave pública y el mensaje se leen todos de la instrucción 0 (los desplazamientos de byte siguen apuntando dentro de los datos de esa instrucción):

    num_signatures      = 1,    padding = 0
    signature_offset    = 48,   signature_instruction_index   = 0
    public_key_offset   = 16,   public_key_instruction_index  = 0
    message_data_offset = 112,  message_data_size = 32,  message_instruction_index = 0

    Por lo tanto, la segunda verificación volvió a leer la instrucción 0 y volvió a verificar la propia firma válida del atacante en lugar de la carga útil 0x09 inválida en la instrucción 1.

  • Paso 4: El atacante llamó a SubmitSignatures. El programa vio dos verificaciones Ed25519 exitosas, pero al registrar el segundo firmante leyó la clave pública del administrador del protocolo incrustada en la instrucción 1, por lo que aceptó esa clave de administrador como un segundo firmante aprobado.

  • Paso 5: Con esa aprobación falsa registrada, el atacante usó el flujo de cambio de administrador para establecer al atacante como administrador de la cuenta de garantía de la víctima, convirtiendo la elusión de verificación en control directo de esa cuenta.

  • Paso 6: Habiendo validado al llamador como el administrador de la garantía, el programa movió activos fuera de la cuenta de token de garantía de la tarjeta del usuario a través de su flujo de transferencia, invocando la transferencia con su propia dirección derivada del programa (PDA) como la autoridad de esa cuenta de token.

Conclusión

La causa raíz de la serie de exploits del contrato Rain Card fue que el programa de garantía confirmó que se había ejecutado una verificación Ed25519 nativa, pero nunca confirmó que la clave pública y el mensaje en los que confiaba fueran los que el verificador realmente había comprobado. Un programa que se apoya en la verificación de firma nativa debe vincular los datos de autorización que consume a esa verificación: ya sea exigiendo un formato Ed25519 autocontenido que lea sus entradas de la instrucción actual, o resolviendo cada instrucción referenciada y comparando la clave pública y el mensaje verificados, byte por byte, contra los datos sobre los que actúa. El hecho de que la misma falla se ejecutara sin parchear en muchos programas de tarjetas impulsados por Rain es lo que convirtió un error en un evento multiprograma.

Tectonic

El 30/08/2026, Tectonic, un protocolo de préstamos al estilo Compound en Cronos, fue explotado porque su token de gobernanza de baja liquidez TONIC fue permitido como garantía con un factor de garantía del 20%. El atacante infló la garantía denominada en TONIC en dos superficies a la vez: una manipulación por transferencia directa de la tasa de cambio de tTONIC, combinada con compras en DEX que empujaron hacia arriba el precio del oráculo. Contra la garantía doblemente inflada, el atacante pidió prestado en múltiples mercados de préstamos. Aproximadamente $6.29M (2,592 ETH) fueron transferidos mediante puente a Ethereum y constituyen la pérdida realizada; la mayor parte del drenaje permaneció en Cronos y fue eliminada cuando los validadores revirtieron la cadena a un estado previo al exploit. Ni Tectonic ni Cronos han confirmado una cifra final de pérdidas [11].

Análisis de vulnerabilidad

La causa raíz fue listar TONIC, un token de gobernanza de baja liquidez, como garantía con un factor de garantía del 20%. A pesar de su rol de gobernanza, TONIC tenía una liquidez en cadena extremadamente baja, por lo que su valoración podía moverse drásticamente con un capital limitado. Al igual que en el exploit de Moonwell, dos superficies eran manipulables a la vez: el precio del oráculo TONIC/USD, y la tasa de cambio del token de recibo tTONIC, que una simple transferencia de TONIC al mercado eleva sin cancelar la deuda correspondiente [11]. Cualquier factor de garantía distinto de cero convirtió entonces esa valoración inflada en poder de endeudamiento contra activos mucho más líquidos.

Análisis del ataque

La reconstrucción del ataque a continuación se basa en inteligencia en cadena y en una reconstrucción detallada de un nodo de archivo [11][12].

  • Paso 1: El atacante suministró aproximadamente 5M USDC a Tectonic como garantía. Mientras el oráculo TONIC/USD aún valoraba el token cerca de $1.06e-8, una cuenta controlada pidió prestados aproximadamente 376.54T TONIC y los movió a una segunda cuenta.

  • Paso 2: La segunda cuenta suministró aproximadamente 41.87T TONIC normalmente, recibiendo tTONIC (el token de recibo del mercado) a cambio.

  • Paso 3: El atacante transfirió la mayor parte del TONIC restante prestado directamente al contrato del mercado tTONIC sin pagar la deuda original de TONIC, elevando la tasa de cambio de tTONIC y aumentando el valor de garantía del tTONIC en poder de la segunda cuenta.

  • Paso 4: Usando la garantía inflada de tTONIC, el atacante pidió prestados 200,000 USDC y aproximadamente 6.96M CRO, luego compró aproximadamente 16.23T más TONIC a través de los pools TONIC/USDC, TONIC/WCRO y TONIC/VVS y los volvió a enrutar hacia el mercado. Esto elevó aún más la tasa de cambio e impulsó al alza el precio spot de DEX; el feed offchain TONIC/USD de Tectonic aceptó entonces una serie de cotizaciones en rápido aumento (de aproximadamente $1.06e-8 a las 12:19 UTC a aproximadamente $2.08e-6 a las 12:49 UTC). Esta es la superficie externa.

  • Paso 5: Nuevos préstamos de aproximadamente 3.31M USDC y 21.21M CRO compraron otros 7.68T TONIC, nuevamente enrutados hacia el mercado, reforzando tanto la tasa de cambio inflada como el precio manipulado inmediatamente antes de la extracción.

  • Paso 6: En la transacción final, el atacante extrajo fondos contra la garantía doblemente inflada en múltiples mercados de préstamos, obteniendo aproximadamente 55.24M USDC, 45.65M USDT, 98 WBTC, 1,895 WETH, 16.75M CRO y otros activos. Aproximadamente $6.29M fueron transferidos mediante puente a Ethereum (~2,592 ETH) antes de que los validadores detuvieran la cadena. La producción de bloques se reanudó el 30/08/2026 a las 23:49 UTC (anunciado el 31/08) después de que los validadores revirtieran el estado a un bloque previo al exploit, eliminando el saldo en Cronos; solo los ~$6.29M transferidos mediante puente constituyen la pérdida realizada.

Conclusión

Al igual que el exploit de Moonwell anterior esta semana, este fue un ataque de manipulación de precios sobre un mercado al estilo Compound que aceptó un token de baja liquidez como garantía, inflando el valor de la garantía en dos superficies a la vez: el precio del oráculo y la tasa de cambio del token de recibo. Los protocolos de préstamos deberían evitar listar activos de baja liquidez como garantía, aplicar límites estrictos de oferta y préstamo donde deban ser soportados, y usar precios ponderados en el tiempo o sensibles a la liquidez para que un mercado spot de poca profundidad no pueda mover el oráculo. La superficie de la tasa de cambio necesita su propia protección: la contabilidad de garantía de un mercado debería excluir las transferencias no solicitadas del activo subyacente, de manera que una transferencia directa no pueda inflar la tasa de cambio del token de recibo mientras la deuda correspondiente permanece pendiente. El monitoreo en cadena de actividad anormal de precios y préstamos puede reducir aún más la ventana de respuesta antes de que el valor sea transferido mediante puente hacia afuera.

Comience con Phalcon Security

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

Pruébelo gratis ahora

Referencias

Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento normativo cripto. Desarrollamos productos y servicios que ayudan a los clientes a realizar auditorías de código (incluyendo contratos inteligentes, blockchain y wallets), 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 las plataformas.

BlockSec ha publicado múltiples artículos de seguridad blockchain en conferencias prestigiosas, 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 asegurado miles de millones de criptomonedas.

Best Security Auditor for Web3

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

BlockSec Audit