Ethereum: de historia a labs

Artículo 24 de 48

50%

completado al llegar aquí

Sistemas distribuidosNota de investigación16 min de lectura

Oráculos: datos externos con dientes

Cómo llega un precio a un contrato: feeds agregados de Chainlink, heartbeat y desviación, chequeos de frescura, TWAP frente a precio spot y manipulación en una transacción.

Ruta Ethereum: 0/48

Un protocolo de préstamos acepta ETH como colateral y presta dólares digitales. Para decidir cuánto prestar necesita saber cuánto vale ese ETH ahora. Pero un contrato Ethereum no puede abrir una conexión HTTP a un exchange: solo ve lo que está en la cadena. Alguien tiene que escribir el precio en la cadena, y el contrato tiene que decidir cuánto confiar en ese número.

Ese "alguien" es un oráculo: un servicio que lleva datos externos a la cadena para que los contratos los usen. Este capítulo trata el precio como una entrada no confiable que hay que validar: de dónde viene, qué tan viejo es, qué tan fácil es moverlo y qué hace el contrato cuando el dato falla.

Brújula de lectura

Actor, evidencia y estado

Antes de avanzar, ubica quién actúa, qué prueba queda y qué parte del sistema cambia.

Actor

Red de oráculos o pool DEX

Quien escribe el precio que tu contrato va a leer.

Evidencia

answer y updatedAt

El valor y el momento de su última actualización, ambos leídos en la misma llamada.

Estado

Decisiones que dependen del precio

Colateral, préstamos y liquidaciones que cambian según el número leído.

Preguntas guía

  • Si el contrato no puede consultar internet, ¿quién escribe el precio en la cadena y cada cuánto?
  • ¿Qué debe hacer tu contrato si el último precio tiene seis horas?
  • ¿Por qué leer el precio actual de un pool de un DEX es peligroso dentro de la misma transacción?
  • ¿Qué cambia cuando tu contrato vive en un rollup cuyo secuenciador se detuvo?
  • ¿Cuál es el comportamiento seguro cuando el oráculo falla: revertir, pausar o usar un respaldo?

El problema del oráculo: determinismo y confianza

Ethereum es determinista: dado el mismo estado y la misma entrada, todos los nodos calculan el mismo resultado. Por eso los contratos solo pueden razonar sobre datos que ya están en la cadena. ethereum.org llama "problema del oráculo" a la tensión que aparece cuando un contrato necesita un dato externo: el dato debe ser correcto para que el contrato funcione bien, y confiar en quien lo trae debilita la propiedad de no depender de terceros.

En términos de backend, un oráculo es una dependencia externa cuyo fallo se convierte en un fallo de negocio, sin reintentos ni timeouts por defecto. Un precio incorrecto no produce un error 500: produce un préstamo demasiado grande o una liquidación injusta, ejecutados y finales. La única defensa es que el contrato valide el dato antes de usarlo.

Traza de un feed agregado: proxy, aggregator y rondas

Chainlink Data Feeds es un ejemplo concreto de oráculo descentralizado, y su documentación ilustra bien las preguntas correctas. Cada feed lo actualizan varios operadores independientes; un contrato aggregator recibe sus reportes y guarda en la cadena una respuesta agregada. Si el aggregator no recibe respuestas de un número mínimo de oráculos, la última respuesta no se actualiza.

El consumidor no llama al aggregator directamente, sino a un proxy que apunta a él, de modo que el aggregator se puede reemplazar sin cambiar la dirección que usa tu contrato. El mismo patrón de proxy del capítulo de upgrades aparece aquí, con la misma consecuencia: alguien controla a qué aggregator apunta.

¿Cuándo se actualiza el precio? No en cada bloque. Una nueva ronda empieza cuando el valor fuera de cadena se desvía del valor en cadena más que un umbral de desviación, o cuando pasa un tiempo fijo desde la última actualización, el heartbeat. La primera condición que se cumpla dispara la ronda. Consecuencia práctica: en un mercado tranquilo, el precio en cadena puede tener casi un heartbeat de antigüedad y seguir siendo correcto. Tu chequeo de frescura debe derivarse del heartbeat del feed concreto que integras, no de un número genérico.

La lectura se hace con latestRoundData(), que devuelve roundId, answer, startedAt, updatedAt y answeredInRound. El precio es answer, un entero con la cantidad de decimales que informa decimals(). La referencia marca answeredInRound como obsoleto, así que los chequeos basados en ese campo ya no aportan.

Invariante de lectura: precio positivo, fresco y dentro de límites

Este contrato encapsula la lectura y revierte si el dato no es utilizable. Escala el precio a 18 decimales para que el resto del sistema no dependa de decimals(). Lo ejecutamos con Foundry 1.5.1 contra un feed simulado; es ilustrativo, no un contrato auditado:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
interface AggregatorV3Interface {
    function decimals() external view returns (uint8);
    function latestRoundData()
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
 
contract PriceReader {
    AggregatorV3Interface public immutable feed;
    uint256 public immutable maxStaleness; // segundos; derivado del heartbeat del feed
    uint256 public immutable minPrice;     // límites de cordura, en 18 decimales
    uint256 public immutable maxPrice;
 
    error InvalidPrice(int256 answer);
    error StalePrice(uint256 updatedAt, uint256 nowTs);
    error PriceOutOfBounds(uint256 price);
 
    constructor(AggregatorV3Interface _feed, uint256 _maxStaleness, uint256 _minPrice, uint256 _maxPrice) {
        feed = _feed;
        maxStaleness = _maxStaleness;
        minPrice = _minPrice;
        maxPrice = _maxPrice;
    }
 
    function price() public view returns (uint256) {
        (, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();
        if (answer <= 0) revert InvalidPrice(answer);
        if (updatedAt == 0 || block.timestamp - updatedAt > maxStaleness) {
            revert StalePrice(updatedAt, block.timestamp);
        }
        uint8 dec = feed.decimals();
        uint256 scaled = dec <= 18
            ? uint256(answer) * 10 ** (18 - dec)
            : uint256(answer) / 10 ** (dec - 18);
        if (scaled < minPrice || scaled > maxPrice) revert PriceOutOfBounds(scaled);
        return scaled;
    }
}

Los tests cubren cuatro casos: un precio fresco de 8 decimales se escala bien; uno con más antigüedad que maxStaleness revierte; uno fuera de límites revierte; y uno igual a cero revierte. Los límites de cordura no reemplazan al oráculo: atrapan errores groseros, como un feed mal configurado o un activo que dejó de cotizar, antes de que el contrato actúe sobre ellos.

Chainlink pone la responsabilidad de estos controles en quien integra: la documentación dice que los desarrolladores son los únicos responsables de chequeos de calidad de datos, circuit breakers y lógica de contingencia. También recomienda circuit breakers que pausen el contrato ante precios viejos, saltos bruscos o valores cerca de mínimos y máximos predefinidos, y advierte que algunos feeds solo se actualizan en horario de mercado.

Superficie de manipulación: el precio spot dentro de una transacción

Ahora la alternativa tentadora: leer el precio directamente de un pool de un DEX, dividiendo sus reservas. Es gratis, está en la cadena y se actualiza con cada swap. Ese último punto es el problema. Un atacante puede mover el precio de un pool con un swap grande, hacer que tu contrato lo lea y deshacer el swap, todo en la misma transacción.

¿De dónde saca el capital? De un flash loan, un préstamo sin colateral que debe devolverse dentro de la misma transacción. En Aave v3, flashLoanSimple permite a un contrato usar la liquidez del pool dentro de una transacción, siempre que devuelva el monto más una comisión. Si el préstamo no se devuelve, toda la transacción revierte; si se devuelve, el atacante solo pagó la comisión y el gas.

Figura 1

Manipulación de precio spot dentro de una transacción

Fuentes:

[6][8]
Manipulación de precio spot dentro de una transacción
Cómo leer esta figura

Diagrama en dos partes. Arriba, un marco titulado una sola transacción del atacante contiene cinco cajas en secuencia: 1, flash loan de un activo; 2, swap grande en pool chico; 3, la víctima lee el spot; 4, préstamo con colateral inflado; 5, swap inverso y devolución. Abajo, un gráfico cualitativo con el eje horizontal marcado paso, de 1 a 5, y el eje vertical marcado precio. Una línea roja, precio spot del pool, parte baja en el paso 1, sube en el paso 2, se mantiene alta en los pasos 3 y 4 y vuelve a bajar en el paso 5. Una línea verde punteada, TWAP o feed agregado, se mantiene plana en todos los pasos, con la nota igual en todo el bloque.

Visual original de TrautsLab. Esquema cualitativo, sin valores reales. Arriba, los cinco pasos de una transacción de ataque. Abajo, el precio spot de un pool chico salta en el paso 2 y vuelve en el paso 5; un TWAP o un feed agregado no cambia dentro del bloque, así que la víctima que lo usa no ve el salto.

La documentación de Uniswap v2 reconoce que muchos hackeos de alto perfil tuvieron al oráculo como vector principal. Su respuesta fue medir el precio al comienzo de cada bloque, antes de cualquier operación, y acumularlo ponderado por el tiempo en una variable. Con dos lecturas de ese acumulador se calcula un TWAP, un precio promedio ponderado por tiempo en un intervalo.

Mover ese precio exige operar al final de un bloque anterior y sostener el desvío en el tiempo. Según la misma documentación, el costo de manipular un TWAP simple crece de forma aproximadamente lineal con la liquidez del pool y con la longitud del intervalo, y se estima como lo perdido ante arbitrajistas y comisiones en cada bloque del período. Uniswap v3 guarda un historial de observaciones consultable con observe; promediar el tick acumulado da una media geométrica del precio.

Un TWAP no es gratis: un intervalo largo resiste mejor la manipulación pero reacciona tarde a una caída real del mercado, justo cuando un protocolo de préstamos necesita liquidar. Y la documentación de v3 aclara que Uniswap v4 no incluye oráculo integrado.

Comparación

Tres fuentes de precio y su riesgo dominante

Precaución

Precio spot de un pool

Reservas actuales del pool, leídas en la misma transacción.

Ideal para
Mostrar un precio orientativo en una interfaz.
Ojo con
Manipulable dentro de una transacción con capital prestado.

TWAP de un DEX

Promedio ponderado por tiempo calculado desde acumuladores en cadena.

Ideal para
Activos con pools líquidos y decisiones que toleran retraso.
Ojo con
Barato de mover en pools con poca liquidez; reacciona tarde a caídas reales.
Recomendado

Feed agregado (por ejemplo Chainlink)

Varios operadores reportan; se actualiza por desviación o heartbeat.

Ideal para
Colateral y liquidaciones, con chequeos de frescura y límites.
Ojo con
Dependencia de un tercero; datos viejos entre actualizaciones; horarios de mercado.
Ninguna es segura sin chequeos; cambia qué falla primero.

Riesgo operacional: secuenciadores, liquidez y respaldo

Si tu contrato vive en un rollup, hay un modo de falla adicional. Los rollups dependen de un secuenciador que ordena las transacciones; si se detiene, los usuarios comunes no pueden operar, aunque la seguridad siga anclada en L1. Chainlink ofrece feeds de disponibilidad del secuenciador que informan 0 si está activo y 1 si está caído, y recomienda aplicar un período de gracia al volver, para evitar liquidaciones masivas cuando los usuarios no pudieron reaccionar.

La liquidez del activo también es parte del oráculo. Chainlink clasifica sus feeds por riesgo de precio de mercado y advierte que activos con poca liquidez o volumen son más vulnerables a manipulación; para feeds de fuente única exige salvaguardas adicionales como límites, chequeos de frescura, comportamiento de respaldo y controles de pausa manual.

Revisión de integración

Antes de que un precio mueva dinero

0 de 6 revisados

Cada punto tiene una prueba que lo verifica en un test o en monitoreo.

El último punto es una decisión de diseño, no un detalle. Revertir es lo más simple, pero puede bloquear liquidaciones necesarias. Un respaldo agrega una segunda dependencia con sus propios riesgos. Lo importante es que la elección quede escrita y probada antes del despliegue, no improvisada durante un incidente.

Conexiones dentro de la serie

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    ethereum.org, "Oracles," Ethereum Developer Documentation.

    Documentation siteID: ETHORG-ORACLESAbrir fuente (abre en otra pestaña)

    Determinismo, problema del oráculo, oráculos centralizados y descentralizados, patrones publish-subscribe y request-response.

  2. [6]

    Uniswap Labs, "Oracles," Uniswap v2 Protocol Documentation, Uniswap/docs repository.

    Documentation siteID: UNISWAP-V2-ORACLESAbrir fuente (abre en otra pestaña)

    Precio medido al inicio de cada bloque, acumulador de precio ponderado por tiempo, construcción de TWAP y costo aproximado de manipulación.

  3. [7]

    Uniswap Labs, "Price Oracles," Uniswap v3 Protocol Documentation, Uniswap/docs repository.

    Documentation siteID: UNISWAP-V3-ORACLESAbrir fuente (abre en otra pestaña)

    Observaciones, observe(secondsAgos), acumulador de tick, media geométrica, actualización como máximo una vez por bloque y ausencia de oráculo integrado en v4.

  4. [8]

    Aave, "IPool.sol: flashLoanSimple," aave-v3-core source code, GitHub repository aave/aave-v3-core.

    Source codeID: AAVE-V3-IPOOLAbrir fuente (abre en otra pestaña)

    flashLoanSimple: acceso a la liquidez del pool dentro de una transacción si se devuelve el monto más una comisión.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

Rutas vecinas

Si este tema te abrió hambre, sigue por aquí.

Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.

Los comentarios se cargan al final de la lectura para no meter peso antes de tiempo.