Volver al blog

Pérdidas de ~5,57 millones USD: exploits de X100 Freezer y NEM | Resumen semanal de BlockSec

Code Auditing
29 de octubre de 2026
10 min de lectura
Ideas clave
  • Dos incidentes: ~5,57 millones USD de pérdidas dentro del alcance analizado, sin incluir las extracciones posteriores de X100.

  • NEM: liquidación defectuosa de impuestos.

  • X100: las nuevas posiciones en staking adquirían poder de voto de inmediato; las propuestas no tenían un plazo de espera para su ejecución.

Durante la última semana (19/10/2026–25/10/2026), este informe hizo seguimiento de dos incidentes de seguridad con pérdidas estimadas de aproximadamente 5,57 millones USD. El análisis detallado se centra en la toma de control de la gobernanza de X100 Freezer en Ethereum.

Fecha Incidente Tipo Pérdida estimada*
21/10/2026 NEM Token Lógica de negocio defectuosa ~820 mil USD
24/10/2026 X100 Freezer Diseño de gobernanza defectuoso ~4,75 millones USD

*Las pérdidas son aproximadas. La cifra de X100 Freezer corresponde a la transacción representativa analizada a continuación y coincide con la estimación inicial de Phalcon; el total no incluye las extracciones posteriores notificadas por separado. La fecha de NEM corresponde a la transacción en cadena; la alerta pública se publicó el 22 de octubre [1].

  • X100 Freezer: El incidente muestra el riesgo combinado de que la elegibilidad para participar en la gobernanza surta efecto de inmediato y de que no exista un plazo de espera para la ejecución: el atacante utilizó fondos obtenidos mediante un préstamo flash para comprar e inmovilizar tokens, obtuvo de inmediato la mayoría de los escaños del Senado y aprobó y ejecutó una propuesta maliciosa relativa a la tesorería dentro de la misma transacción.

El mejor auditor de seguridad para Web3

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

Incidente destacado de la semana: X100 Freezer

El 24/10/2026, X100 Freezer, un sistema de staking y gestión de liquidez en cadena en Ethereum, fue objeto de un ataque que ocasionó pérdidas de aproximadamente 4,75 millones USD en la transacción examinada aquí. Un diseño de gobernanza defectuoso permitió que los saldos recién inmovilizados otorgaran escaños en el Senado de inmediato, sin un plazo de espera entre la aprobación y la ejecución de las propuestas. El atacante consiguió dos de los tres escaños, dirigió un intercambio de la tesorería hacia un pool bajo su control y vendió parte de los tokens recuperados para devolver el préstamo flash y obtener ganancias [2].

Contexto

Los usuarios de X100 inmovilizan tokens X en posiciones de staking por niveles para obtener recompensas. El contrato Freezer en 0x79f3...2a46 también gestiona la liquidez del proyecto en Uniswap V3/V4 y PancakeSwap V3. Hereda el contrato de gobernanza Senate a través de DexTreasury; el Senado decide las operaciones de la tesorería mediante propuestas y votaciones.

El punto de entrada público freeze() transfiere los tokens del usuario al contrato y luego llama a la función privada _freeze() para crear una posición de staking y actualizar su contabilidad. _tierTotal[owner][tier] registra el importe inmovilizado de cada cuenta en cada nivel. _freeze() actualiza estos saldos y notifica al sistema de gobernanza mediante _onStakeChanged(owner):

function _freeze(address owner, uint8 tier, uint256 amount) private returns (uint256 id) {
    _settle(owner);

    Account storage account = _accounts[owner];
    if (account.firstFreezeAt == 0) {
        account.firstFreezeAt = block.timestamp;
    }

    id = _create(owner, tier, amount, block.timestamp, block.timestamp);

    _tierTotal[owner][tier] += amount;
    totalFrozen += amount;
    totalFrozenIn[tier] += amount;
    _onStakeChanged(owner);

    emit Frozen(id, owner, tier, amount);
}

Para incorporarse a la lista de candidatos al Senado, una cuenta llama a applyForSenate(). Esto invoca _register(), que registra la inscripción, cobra la tasa de candidatura y llama a la misma función de actualización del stake:

function _register(address account) internal {
    require(!_registered[account], AlreadyRegistered(account));

    _registered[account] = true;
    _collectCandidacyFee(account);

    emit Registered(account);

    _onStakeChanged(account);
}

La función omite las cuentas no inscritas. Para las inscritas, mantiene una lista ordenada según los saldos inmovilizados e inserta o cambia de posición las cuentas mediante _link(). isSenator() identifica como senadores a las primeras senateSize cuentas de esa lista. El sistema admite entre 3 y 21 escaños, cada uno con un voto. Los tokens inmovilizados están sujetos a un plazo de retiro, calculado por _unfreezeAt() a partir de la marca de tiempo frozenAt de la posición y su nivel de staking.

Un senador presenta una operación de tesorería permitida mediante propose(), que registra la propuesta y emite automáticamente un voto a favor del proponente. Los demás senadores emiten sus votos mediante vote(), y _tally() cuenta los votos a favor entre los escaños actuales. La propuesta registra su momento de creación y una fecha límite determinada por votingPeriod, cuyo valor predeterminado es de tres días.

Las funciones de tesorería como v3Swap() son objetivos permitidos para las propuestas en DexTreasury._isProposable(). Su modificador onlySenate exige que la llamada provenga del propio contrato. La ruta de ejecución realiza esta llamada a sí mismo utilizando los datos de la propuesta aprobada. Después, Freezer._afterExecute() comprueba que el saldo restante de X cubra totalFrozen, lo que preserva el principal inmovilizado total.

Análisis de la vulnerabilidad

El diseño de gobernanza defectuoso de Freezer carecía de un período de espera antes de que el stake recién adquirido pudiera conferir autoridad en el Senado y de un plazo de espera antes de que pudiera ejecutarse una propuesta aprobada.

En primer lugar, el stake utilizado para la clasificación procede directamente de los saldos inmovilizados actuales. _stakeOf() llama a frozenOf(), que suma _tierTotal sin comprobar la antigüedad del stake ni consultar un saldo histórico:

function _stakeOf(address account) internal view override returns (uint256) {
    return frozenOf(account);
}

function frozenOf(address owner) public view returns (uint256 total) {
    total = 0;
    for (uint8 tier = 0; tier < TIERS; ++tier) {
        total += _tierTotal[owner][tier];
    }
}

Las rutas de depósito e inscripción mostradas anteriormente actualizan la elegibilidad dentro de la misma secuencia de llamadas. Aunque _freeze() registra marcas de tiempo para la posición, las comprobaciones de clasificación y de condición de senador no las utilizan para imponer un período mínimo de tenencia. Por lo tanto, un nuevo candidato con fondos suficientes puede obtener un escaño y ejercer sus derechos de propuesta y voto dentro de la misma transacción. El plazo de retiro de los tokens no retrasa estos derechos de gobernanza.

En segundo lugar, el período de votación de tres días solo determina cuándo vence una propuesta. _state() devuelve Open en cuanto existe la propuesta, y esta permanece abierta hasta su ejecución, cancelación o vencimiento. La comprobación de la mayoría es independiente del tiempo transcurrido:

function _state(Proposal storage proposal) private view returns (ProposalState) {
    if (proposal.executed) {
        return ProposalState.Executed;
    }
    if (proposal.cancelled) {
        return ProposalState.Cancelled;
    }
    if (block.timestamp > proposal.deadline) {
        return ProposalState.Expired;
    }
    return ProposalState.Open;
}

function _isPassed(uint256 support) private view returns (bool) {
    return support * 2 > senateSize;
}

execute() acepta una propuesta abierta en cuanto sus votos a favor superan la mitad de senateSize. No comprueba ni que haya transcurrido un intervalo mínimo desde createdAt ni que exista un plazo de espera tras la aprobación antes de llamar a la operación de tesorería:

function execute(uint256 id) external {
    Proposal storage proposal = _requireOpen(id);

    uint256 size = senateSize;
    (uint256 support, address[] memory seats) = _tally(id);
    require(_isPassed(support), NotPassed(id, support, size));

    proposal.executed = true;

    (bool ok, bytes memory result) = address(this).call(proposal.data);
    if (!ok) {
        assembly ("memory-safe") {
            revert(add(result, 32), mload(result))
        }
    }

    _afterExecute(id);

    emit ProposalExecuted(id, _msgSender(), support, size, seats);
}

La llamada del contrato a sí mismo satisface onlySenate, por lo que la comprobación de acceso a la tesorería no añade ningún retraso a la ejecución. La comprobación final del principal inmovilizado no limita el momento en que se ejecuta la propuesta ni impone un rendimiento mínimo para el intercambio de la tesorería. Estas condiciones permiten que una mayoría recién establecida autorice y ejecute una operación de tesorería dentro de una sola transacción.

Análisis del ataque

El siguiente análisis se basa en la transacción 0x38c608...4d81d0.

  • Paso 1: El atacante utilizó fondos obtenidos mediante un préstamo flash para comprar tokens X y distribuirlos entre dos contratos bajo su control. Los fondos prestados financiaron la compra de tokens con la que se establecerían las dos posiciones de staking.

  • Paso 2: Cada contrato bajo su control inmovilizó sus tokens X en Freezer y llamó a applyForSenate(). La traza muestra que ambos contratos recibieron un evento Ranked tras inscribirse. Sus saldos inmovilizados les aseguraron dos escaños en el Senado dentro de la transacción. En el momento del ataque, el Senado tenía tres escaños, por lo que controlar dos proporcionaba la mayoría necesaria para aprobar una propuesta relativa a la tesorería.

  • Paso 3: El atacante creó un pool X/USDT en PancakeSwap V3 y aportó su liquidez. Uno de los contratos bajo su control propuso un intercambio de la tesorería y emitió automáticamente un voto a favor; el otro emitió el segundo voto a favor. Una vez alcanzado el umbral de mayoría, Freezer ejecutó la propuesta 7. Su llamada a v3Swap() especificaba 400.000 X como entrada y amountOutMinimum = 0. La tesorería recibió solo unos 35,68 USDT. La traza registra votesFor = 2 y senateSize = 3, lo que muestra que la mayoría obtenida autorizó la operación.
  • Paso 4: El atacante retiró liquidez del pool de PancakeSwap V3 que acababa de crear para recuperar los tokens X de la tesorería. Vendió parte de esos tokens a través del pool X/USDT ya existente en Uniswap V3 y devolvió el préstamo flash. Los fondos para devolverlo provinieron de la venta posterior, no de un retiro inmediato de las posiciones de staking inmovilizadas.

Comience a utilizar Phalcon Explorer

Explore las transacciones para actuar con criterio

Pruébelo gratis ahora

Conclusión

El incidente se debió a un diseño de gobernanza defectuoso: los saldos inmovilizados actuales conferían autoridad de inmediato y las propuestas aprobadas podían ejecutarse sin un plazo de espera. El atacante combinó estas condiciones para obtener una mayoría y utilizarla antes de tener que devolver el préstamo flash. La tesorería vendió entonces 400.000 X por unos 35,68 USDT a través del pool de PancakeSwap V3 bajo el control del atacante. El atacante retiró liquidez de ese pool para recuperar los tokens de la tesorería, vendió parte de ellos a través del pool X/USDT de Uniswap V3, independiente y ya existente, y utilizó los ingresos para devolver el préstamo flash y conservar una ganancia.

La gobernanza debería exigir una antigüedad mínima del stake o determinar la elegibilidad para ocupar escaños y los derechos de voto a partir de una instantánea histórica de los saldos tomada en un bloque anterior a la transacción actual. Esto impide que las posiciones financiadas dentro de la transacción autoricen movimientos de la tesorería de inmediato. Un plazo de espera entre la aprobación y la ejecución debería impedir que toda la secuencia de toma de control y extracción se complete en una sola transacción y dar tiempo para revisar o cancelar una propuesta maliciosa. Estos controles abordan etapas distintas del proceso de gobernanza y deberían evaluarse conjuntamente durante una auditoría de contratos inteligentes.

Referencias

[1] BlockSec Phalcon: análisis del incidente de NEM Token

[2] BlockSec Phalcon: análisis del incidente de gobernanza de X100 Freezer

Análisis de seguridad gratuito

Una revisión rápida de seguridad con nuestro motor interno de análisis automatizado.

Analice gratis

Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento normativo en el ámbito de las criptomonedas. Desarrollamos productos y servicios que ayudan a los clientes a realizar auditorías de código (incluidos contratos inteligentes, blockchain y billeteras), interceptar ataques en tiempo real, analizar incidentes, rastrear fondos ilícitos y cumplir las obligaciones de prevención del lavado de dinero y de la financiación del terrorismo (AML/CFT), a lo largo de todo el ciclo de vida de los protocolos y las plataformas.

BlockSec ha publicado varios artículos sobre seguridad blockchain en conferencias prestigiosas, ha informado de varios ataques de día cero contra aplicaciones DeFi, ha bloqueado múltiples ataques informáticos para rescatar más de 20 millones USD y ha protegido miles de millones en criptomonedas.

Apúntate para recibir las novedades
Pérdidas de aproximadamente 3,87 millones USD: hackeos de Autheo Network y UNCX | Resumen semanal de BlockSec
Security Insights

Pérdidas de aproximadamente 3,87 millones USD: hackeos de Autheo Network y UNCX | Resumen semanal de BlockSec

Este informe semanal cubre 3 incidentes de seguridad correspondientes al período 12/10/2026–18/10/2026, con pérdidas totales estimadas en aproximadamente 3,87 millones USD. Autheo Network sufrió una vulneración de clave privada con pérdidas de aproximadamente 1,76 millones USD; Qubic Wallet perdió aproximadamente 1,30 millones USD debido a la generación de una semilla débil; y los contratos Smart Token Vesting V2 de UNCX Network sufrieron un error contable con pérdidas de aproximadamente 807 mil USD en Ethereum, BNB Chain, Base y Robinhood Chain. El análisis destacado de UNCX explica cómo un cálculo agregado de participaciones defectuoso mezcló tramos de adquisición de derechos creados con distintas proporciones entre tokens y participaciones, lo que permitió retiros excesivos de pools compartidos.

Pérdidas de ~542.000 dólares: explotación de MakerDaoBot | Resumen semanal de BlockSec
Security Insights

Pérdidas de ~542.000 dólares: explotación de MakerDaoBot | Resumen semanal de BlockSec

Ethereum (05–11/10/2026): 1 incidente; pérdida estimada de $542 mil. MakerDaoBot, sin control de acceso, dio a un adaptador malicioso acceso a sus saldos MakerDAO. Este cerró 4 subastas ETH-A y convirtió 200 ETH-A en WETH; el atacante los cambió por 200 ETH y se los envió.

~$418M perdidos: Exploits de Bitget y NEAR Intents | BlockSec Weekly
Security Insights

~$418M perdidos: Exploits de Bitget y NEAR Intents | BlockSec Weekly

Este informe quincenal registra ocho incidentes del 21 sept. al 4 oct. de 2026, con ~$418.4M en pérdidas en Ethereum, BNB Chain, Solana, Bitcoin, TRON, XRP Ledger, Zcash, Avalanche, NEAR y otras redes. Incluye análisis de la vulnerabilidad de $387.5M en Bitget y la falla de $3.87M en NEAR Intents.

El mejor auditor de seguridad para Web3

Valida el diseño, el código y la lógica de negocio antes del lanzamiento, siguiendo los estándares de seguridad más exigentes del sector.

Auditoría de BlockSec