Bugs de contabilidad en AMM de DeFi & Explotación de la caché de balances virtuales
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Overview
El pool yETH de Yearn Finance (Nov 2025) mostró cómo las cachés diseñadas para ahorrar gas dentro de AMMs complejos pueden ser usadas como arma cuando no se reconcilian durante transiciones de estado límite. El weighted stableswap pool rastrea hasta 32 liquid staking derivatives (LSDs), los convierte a equivalentes en ETH saldos virtuales (vb_i = balance_i × rate_i / PRECISION), y almacena esos valores en un array de almacenamiento empaquetado packed_vbs[]. Cuando todos los LP tokens son quemados, totalSupply cae correctamente a cero pero los slots cacheados packed_vbs[i] retuvieron enormes valores históricos. El depositante posterior fue tratado como el “primer” proveedor de liquidez aunque la caché aún contenía liquidez fantasma, permitiendo a un atacante acuñar ~235 septillones de yETH por solo 16 wei antes de drenar ≈USD 9M en colateral LSD.
Ingredientes clave:
- Derived-state caching: se evitan costosas consultas al oráculo persistiendo saldos virtuales y actualizándolos de forma incremental.
- Falta de reinicio cuando
supply == 0: los decrementos proporcionales enremove_liquidity()dejaron residuos no nulos enpacked_vbs[]tras cada ciclo de retiro. - La rama de inicialización confía en la caché:
add_liquidity()llama a_calc_vb_prod_sum()y simplemente leepacked_vbs[]cuandoprev_supply == 0, asumiendo que la caché también está a cero. - Flash-loan financed state poisoning: bucles de deposit/withdraw amplificaron residuos por redondeo sin bloqueo de capital, permitiendo una sobreemisión catastrófica en la ruta de “primer depósito”.
Cache design & missing boundary handling
El flujo vulnerable se simplifica a continuación:
function remove_liquidity(uint256 burnAmount) external {
uint256 supplyBefore = totalSupply();
_burn(msg.sender, burnAmount);
for (uint256 i; i < tokens.length; ++i) {
packed_vbs[i] -= packed_vbs[i] * burnAmount / supplyBefore; // truncates to floor
}
// BUG: packed_vbs not cleared when supply hits zero
}
function add_liquidity(Amounts calldata amountsIn) external {
uint256 prevSupply = totalSupply();
uint256 sumVb = prevSupply == 0 ? _calc_vb_prod_sum() : _calc_adjusted_vb(amountsIn);
uint256 lpToMint = pricingInvariant(sumVb, prevSupply, amountsIn);
_mint(msg.sender, lpToMint);
}
function _calc_vb_prod_sum() internal view returns (uint256 sum) {
for (uint256 i; i < tokens.length; ++i) {
sum += packed_vbs[i]; // assumes cache == 0 for a pristine pool
}
}
Porque remove_liquidity() solo aplicaba decrementos proporcionales, cada ciclo dejaba polvo de redondeo en punto fijo. Después de ≳10 ciclos de depósito/retiro esos residuos se acumularon en saldos virtuales fantasma extremadamente grandes mientras los saldos on-chain de tokens estaban casi vacíos. Quemar las últimas LP shares dejó totalSupply en cero, pero las cachés permanecieron pobladas, preparando el protocolo para una inicialización malformada.
Exploit playbook (estudio de caso yETH)
- Flash-loan working capital – Pedir prestado wstETH, rETH, cbETH, ETHx, WETH, etc. desde Balancer/Aave para evitar inmovilizar capital mientras se manipula el pool.
- Poison
packed_vbs[]– Repetir depósitos y retiros a través de ocho assets LSD. Cada retiro parcial truncapacked_vbs[i] − vb_share, dejando residuos >0 por token. Repetir el bucle infla saldos fantasma equivalentes a ETH sin levantar sospechas porque los saldos reales más o menos se compensan. - Force
supply == 0– Quemar todos los LP token restantes para que el pool crea que está vacío. Una omisión en la implementación deja elpacked_vbs[]envenenado sin tocar. - Dust-size “first deposit” – Enviar un total de 16 wei dividido entre las ranuras LSD soportadas.
add_liquidity()veprev_supply == 0, ejecuta_calc_vb_prod_sum()y lee la caché obsoleta en vez de recomputar a partir de los saldos reales. El cálculo del mint actúa entonces como si entraran billones de USD, emitiendo ~2.35×10^26 yETH. - Drain & repay – Canjear la posición LP inflada por todos los LSD almacenados, swap yETH→WETH en Balancer, convertir a ETH vía Uniswap v3, repagar los flash loans/fees y blanquear el beneficio (p. ej., a través de Tornado Cash). Beneficio neto ≈ USD 9M mientras que solo 16 wei de fondos propios tocaron el pool.
Condiciones generalizadas de explotación
Se puede abusar de AMMs similares cuando se cumplen todas las siguientes condiciones:
- Derivados en caché de saldos (virtual balances, TWAP snapshots, invariant helpers) persisten entre transacciones para ahorrar gas.
- Actualizaciones parciales que truncan resultados (floor division, fixed-point rounding), permitiendo a un atacante acumular residuos con estado mediante ciclos simétricos de depósito/retiro.
- Condiciones límite que reutilizan cachés en vez de recomputar desde la verdad básica, especialmente cuando
totalSupply == 0,totalLiquidity == 0, o la composición del pool se reinicia. - La lógica de minting carece de cheques de cordura de ratio (p. ej., ausencia de límites
expected_value/actual_value) de modo que un depósito de polvo puede mintear esencialmente todo el supply histórico. - Capital barato disponible (flash loans o crédito interno) para ejecutar docenas de operaciones que ajusten estado dentro de una sola transacción o de un bundle coreografiado.
Defensive engineering checklist
- Resets explícitos cuando supply/lpShares llegan a cero:
if (totalSupply == 0) {
for (uint256 i; i < tokens.length; ++i) packed_vbs[i] = 0;
}
Aplicar el mismo tratamiento a todo acumulador en caché derivado de saldos o datos de oráculo.
- Recomputar en ramas de inicialización – Cuando
prev_supply == 0, ignorar las cachés por completo y reconstruir los virtual balances a partir de los saldos reales de tokens + tasas de oráculo en vivo. - Límites de cordura en minting – Revertir si
lpToMint > depositValue × MAX_INIT_RATIOo si una sola transacción mina >X% del supply histórico mientras los depósitos totales están por debajo de un umbral mínimo. - Drenajes de residuos por redondeo – Agregar el polvo por token a un sumidero (treasury/burn) para que los ajustes proporcionales repetidos no desvíen las cachés de los saldos reales.
- Tests diferenciales – Para cada transición de estado (add/remove/swap), recomputar el mismo invariante off-chain con math de alta precisión y asegurar igualdad dentro de un epsilon estricto incluso después de drenajes completos de liquidez.
Monitoring & response
- Detección multi-transacción – Rastrear secuencias de eventos de depósito/retiro casi simétricos que dejan el pool con saldos bajos pero alto estado en caché, seguidos de
supply == 0. Detectores de anomalías por transacción única fallan en estas campañas de envenenamiento. - Simulaciones en runtime – Antes de ejecutar
add_liquidity(), recomputar los virtual balances desde cero y comparar con las sumas en caché; revertir o pausar si las deltas exceden un umbral en basis points. - Alertas conscientes de flash-loans – Marcar transacciones que combinen grandes flash loans, retiros exhaustivos del pool y un depósito final de tamaño polvo; bloquear o requerir aprobación manual.
References
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.


