Durante la semana pasada (12/10/2026–18/10/2026), se destacan los siguientes 3 incidentes de seguridad notables, con una pérdida total estimada de aproximadamente 3,87 millones USD.
| Fecha | Incidente | Tipo | Pérdida estimada |
|---|---|---|---|
| 12/10/2026 | Autheo Network | Compromiso de clave privada | ~1,76 millones USD |
| 14/10/2026 | UNCX Network | Error contable | ~807 mil USD |
| 18/10/2026 | Qubic Wallet | Generación débil de semillas | ~1,30 millones USD |
Las cifras de pérdidas son estimaciones basadas en el valor realizado.
- UNCX Network: Este incidente se analiza en detalle porque su cálculo defectuoso de las participaciones agregadas ofrece la lección más clara de la semana sobre contabilidad de contratos.
Análisis de seguridad gratuito
Una revisión rápida de seguridad con nuestro motor interno de análisis automatizado.
Analizar gratisIncidente destacado de la semana: UNCX Network
El 14/10/2026, los atacantes explotaron los contratos Smart Token Vesting V2 de UNCX Network en Ethereum, BNB Chain, Base y Robinhood Chain. El incidente afectó a 10 activos en 11 transacciones de extracción de fondos confirmadas y dio lugar a aproximadamente 807,609 mil USD de valor realizado o canalizado [1]. La causa raíz fue un cálculo defectuoso de las participaciones agregadas que mezclaba tramos de adquisición gradual creados con distintas relaciones entre tokens y participaciones. Esto provocó una divergencia entre los registros de cada posición y la oferta global de participaciones, lo que permitió que las posiciones controladas por los atacantes retiraran más tokens de los que estos habían aportado.
Contexto
UNCX Network es un proveedor de infraestructura DeFi multicadena cuyo producto Smart Token Vesting V2 permite a los proyectos bloquear tokens ERC-20 mediante calendarios de adquisición gradual basados en el tiempo. Cada calendario se representa mediante un ID de adquisición gradual y registra el token, el creador, el beneficiario, el importe total, el importe ya liberado y uno o más tramos de desbloqueo. Una posición flexible que admite aportes adicionales puede recibir nuevos tramos después de su creación, mientras que su creador puede cancelar la parte aún no adquirida.
Las posiciones de un mismo token comparten un saldo en el VestingManager, y el protocolo utiliza participaciones para determinar qué parte de ese saldo corresponde a cada posición. Por tanto, su contabilidad tiene dos niveles relacionados:
Global pool, per token
create / topUp: receive tokens and mint shares
release: convert releasable shares into tokens
_totalTokenShares[token]: active shares across all positions
Position record, per vesting ID
_depositedShares[id]: share basis assigned to the position
totalAmount: token amount represented by its tranches
released: shares already paid to the beneficiary
Para efectuar una liberación, el gestor calcula primero qué parte del calendario se ha adquirido. Convierte esa fracción de totalAmount en participaciones adquiridas mediante _depositedShares[vestingId], resta released y denomina al resultado releasableShares. En la fórmula siguiente, tokenBalance es el saldo actual de tokens ERC-20 que mantiene el gestor para el token del calendario, compartido entre todas las posiciones de ese token, mientras que _totalTokenShares[token] es la oferta global correspondiente de participaciones activas. La cantidad de tokens pagada depende de la relación entre ambos:
amount = releasableShares * tokenBalance / _totalTokenShares[token]
Depositar mediante las funciones de adquisición gradual aumenta tanto el saldo de tokens como la oferta de participaciones. Una transferencia directa de tokens ERC-20 al gestor cambia únicamente el saldo de tokens y, por tanto, modifica la cantidad de tokens que representa cada participación. Este modelo de participaciones solo puede admitir posiciones agrupadas si todas las operaciones del ciclo de vida mantienen conciliados los registros de las posiciones y la oferta global.
Análisis de la vulnerabilidad
El VestingManager afectado en Ethereum, ubicado en 0xA98F...99F5, contiene un cálculo defectuoso de las participaciones agregadas que puede explotarse mediante release(), topUpVesting() y cancel(). Existen despliegues equivalentes afectados en las otras tres redes.
Para facilitar la comparación de los cambios de estado, el análisis siguiente introduce D, R y V como abreviaturas. Estos símbolos no aparecen en el código del contrato; la tabla relaciona cada uno con la variable o expresión correspondiente del contrato. Los valores indicados pertenecen a dos tipos de cantidades:
| Símbolo | Variable / expresión del contrato | Tipo de cantidad | Significado |
|---|---|---|---|
D |
_depositedShares[vestingId] |
Cantidad de participaciones | Base acumulada de participaciones asignada a todos los depósitos registrados para la posición |
R |
schedule.released |
Cantidad de participaciones | Participaciones acumuladas ya pagadas al beneficiario |
| - | schedule.totalAmount |
Cantidad de tokens | Cantidad total de tokens registrada en los tramos actuales de la posición |
| - | vestedAmount |
Cantidad de tokens | Parte de totalAmount que se ha adquirido conforme a esos tramos en el momento actual |
V |
vestedShare |
Cantidad de participaciones | Participaciones adquiridas acumuladas calculadas aplicando una única proporción combinada de tokens adquiridos a D |
En release(), el contrato calcula releasableShares, incrementa schedule.released y resta esa misma cantidad de _totalTokenShares[schedule.token]. No reduce _depositedShares[vestingId]. Por tanto, después de una liberación, la posición aporta solo D - R participaciones activas a la oferta global, aunque su base almacenada sigue siendo D.
function release(uint256 vestingId) external nonReentrant {
...
uint256 releasableShares = _getReleasableShares(vestingId);
uint256 balance = IERC20(schedule.token).balanceOf(address(this));
uint256 amount =
(releasableShares * balance) / _totalTokenShares[schedule.token];
schedule.released += releasableShares;
_totalTokenShares[schedule.token] -= releasableShares;
IERC20(schedule.token).safeTransfer(msg.sender, amount);
}
cancel() aplica una única proporción combinada de adquisición gradual a la base agregada de participaciones de la posición. Primero calcula vestedAmount mediante getVestedAmount(vestingId) y, después, convierte la fracción adquirida, expresada en tokens, en participaciones como V = (vestedAmount * D) / totalAmount. Por último, la función considera D - V como las participaciones no adquiridas que pueden retirarse de la posición. V es un valor acumulado, no las participaciones adquiridas que quedan después de los pagos anteriores.
function cancel(uint256 vestingId) external nonReentrant {
...
uint256 vestedAmount = getVestedAmount(vestingId);
uint256 remainingAmount = schedule.totalAmount - vestedAmount;
uint256 vestedShare =
vestedAmount * _depositedShares[vestingId] / schedule.totalAmount;
uint256 sharesToRemove =
_depositedShares[vestingId] - vestedShare;
_totalTokenShares[schedule.token] -= sharesToRemove;
_depositedShares[vestingId] = vestedShare;
schedule.totalAmount = vestedAmount;
schedule.cancelled = true;
IERC20(schedule.token).safeTransfer(schedule.creator, remainingAmount);
}
El defecto reside en el propio cálculo combinado. Cada tramo tiene su propia cantidad de tokens, su avance de adquisición gradual y un número de participaciones acuñadas según la relación vigente en el fondo común cuando se añadió ese tramo. Por tanto, sus participaciones adquiridas deberían calcularse por separado y luego sumarse. En cambio, el contrato agrega primero las cantidades de tokens y la base de participaciones, y después aplica una única proporción de adquisición gradual a la posición combinada porque solo almacena un D y un totalAmount. Los dos métodos son equivalentes únicamente cuando los tramos tienen la misma relación de participaciones por token o el mismo avance de adquisición gradual. Una transferencia directa de tokens cambia la relación del fondo común antes de un aporte adicional, por lo que el nuevo tramo recibe muchas menos participaciones por token que el tramo original. Aplicar una única proporción combinada de adquisición gradual a ambos tramos puede producir entonces un V que ya no representa sus participaciones adquiridas combinadas.
Como R ya se ha pagado con cargo a las participaciones adquiridas acumuladas de la posición, un calendario coherente debe satisfacer R <= V. La infracción de esta desigualdad es el síntoma directo del cálculo combinado. El código no mantiene un saldo independiente de participaciones activas para la posición; resta D - V directamente del _totalTokenShares global. Supongamos que otras posiciones aportan 1000 participaciones, mientras que esta posición tiene D = 100 y R = 80. Antes de la cancelación, la oferta global contiene 1,000 + (D - R) = 1,020 participaciones. Si el cálculo combinado produce V = 60, cancel() resta D - V = 40 y deja solo 980 participaciones globales, 20 menos que las 1000 participaciones aportadas por las otras posiciones. Si la oferta global contuviera únicamente las 20 participaciones activas de esta posición, restar 40 provocaría un desbordamiento por defecto y revertiría la operación; la deducción excesiva solo tiene éxito porque el fondo común compartido contiene participaciones aportadas por otras posiciones. La función almacena entonces _depositedShares[vestingId] = 60, mientras que schedule.released sigue siendo 80, lo que deja el registro de la posición en el estado imposible D < R.
El código no exige R <= V ni limita sharesToRemove a las participaciones activas de la posición. Por tanto, la cancelación puede restar participaciones históricas que ya no forman parte de la oferta global. Al mismo tiempo, transfiere totalAmount - vestedAmount directamente en unidades de tokens, sin convertir el reembolso mediante la relación de participaciones del fondo común. La cancelación en sí no genera un pago excesivo; únicamente reduce la oferta global de participaciones por debajo de las participaciones activas registradas en las posiciones, creando las condiciones para un canje excesivo posterior.
Análisis del ataque
El siguiente análisis utiliza una transacción representativa en Ethereum, 0x352065...c23823. El contrato de ataque ejecutó la misma secuencia básica contra cinco tokens dentro de la transacción, utilizando liquidez flash para cada token.
-
Paso 1: El atacante creó una pequeña posición de adquisición inmediata,
#5058, y dos posiciones de adquisición inmediata mucho mayores, flexibles y que admitían aportes adicionales,#5059y#5060. La posición pequeña se conservaría para el retiro final, mientras que las posiciones grandes aportaban los registros históricos de participaciones necesarios para corromper la contabilidad global. -
Paso 2: El atacante liberó las dos posiciones grandes. Con la relación inicial del fondo común cercana a 1:1, cada liberación devolvió aproximadamente el depósito neto correspondiente. Las participaciones correspondientes salieron de
_totalTokenShares, pero los valores originales permanecieron en los registros_depositedSharesde las posiciones#5059y#5060, y solo se registraron como liberados en el campo independientereleasedde cada calendario. -
Paso 3: El atacante transfirió tokens directamente al
VestingManager. Esto restableció el saldo de tokens sin restablecer las participaciones liberadas, por lo que la cantidad de tokens representada por cada participación restante aumentó considerablemente. -
Paso 4: El atacante hizo aportes adicionales a las posiciones grandes ya liberadas. Dado que los aportes se valoraron según la nueva relación, más elevada, de tokens por participación, los depósitos sustanciales de tokens acuñaron solo un pequeño número de participaciones nuevas. Las posiciones combinaban entonces una gran base histórica desactualizada con una pequeña aportación de participaciones activas.
-
Paso 5: El atacante canceló las dos posiciones que habían recibido aportes adicionales. Cada cancelación aplicó una única proporción combinada de adquisición gradual al historial agregado de participaciones depositadas. El
vestedSharerecalculado quedó por debajo de las participaciones ya liberadas, por lo quesharesToRemovesuperó la aportación activa de la posición. La oferta global se redujo en aproximadamente5.0e25participaciones fantasma por posición, aunque cada posición solo había añadido una pequeña cantidad de participaciones activas durante el aporte adicional. -
Paso 6: Con el denominador considerablemente subestimado, el atacante liberó la pequeña posición que había conservado. En el ejemplo desarrollado para un token, un depósito neto de aproximadamente
5.73e22unidades sin ajustar permitió retirar aproximadamente1.07e26unidades sin ajustar, alrededor de 1,873 veces el depósito. El atacante repitió ciclos de creación y liberación aprovechando la relación distorsionada y devolvió la liquidez flash.

El informe oficial del incidente identificó el mismo patrón en 11 transacciones de extracción de fondos confirmadas. Algunas variantes se completaron de forma atómica, mientras que otras efectuaron la liberación final en una transacción vinculada. UNCX calculó la cifra de aproximadamente 807,609 mil USD a partir de los ingresos realizados o canalizados, tras excluir el capital aportado por el atacante, el volumen temporal de las operaciones flash y los activos devueltos [1].
Comience a usar Phalcon Explorer
Examine las transacciones en profundidad para actuar con criterio
Pruébelo gratis ahoraConclusión
La causa raíz fue una contabilidad defectuosa de las participaciones agregadas. El protocolo combinó tramos creados con distintas relaciones entre tokens y participaciones y luego aplicó una única proporción de adquisición gradual a su base agregada de participaciones. Esto calculó incorrectamente las participaciones adquiridas acumuladas, permitió que una posición retirara más participaciones de las que aún aportaba y provocó una divergencia entre los registros de cada posición y la oferta global de participaciones. Los atacantes aprovecharon la oferta global subestimada para retirar del saldo compartido tokens que seguían siendo necesarios para satisfacer otras posiciones de adquisición gradual.
Un modelo contable que permita depósitos posteriores con relaciones de participaciones cambiantes debería conservar la cantidad de tokens, la base de participaciones y el avance de adquisición gradual de cada tramo, en lugar de combinarlos bajo una única proporción. También debería garantizar que los pagos acumulados nunca superen las participaciones adquiridas acumuladas y que una posición no pueda retirar más que su aportación activa.
Referencias
[1] Informe del incidente de Smart Token Vesting V2 de UNCX Network
El mejor auditor de seguridad para Web3
Valide el diseño, el código y la lógica de negocio antes del lanzamiento



