¿Qué es exactamente el “precio” en Uniswap?
Supongamos que tenemos 1 Ether y 2,000 USDC en un pool. Esto implica que el precio del Ether es de 2,000 USDC. Específicamente, el precio del Ether es 2,000 USDC / 1 Ether (ignorando los decimales).
De manera más general, el precio de un activo, en términos del precio del otro activo en el par, es una proporción (ratio) donde el “activo que te interesa” está en el denominador.
En el ejemplo anterior, significa “cuántos bars necesitas pagar para obtener un foo” (ignorando las comisiones).
El precio es una proporción
Debido a que el precio es una proporción, necesita ser almacenado con un tipo de dato que tenga puntos decimales (lo cual los tipos de Solidity no tienen por defecto).
Es decir, decimos que Ethereum es 2000 y USDC (en precio de Ethereum) es 0.0005 (esto ignorando los decimales de ambos activos).
Uniswap utiliza un número de coma fija con 112 bits de precisión a cada lado del decimal, lo que ocupa un total de 224 bits, y cuando se empaqueta con un número de 32 bits, utiliza un solo slot.
Definición de oráculo
Un oráculo en términos informáticos es una “fuente de verdad”. Un oráculo de precios es una fuente de precios. Uniswap tiene un precio implícito al mantener dos activos, y otros smart contracts pueden usar esto como un oráculo de precios.
Los usuarios previstos del oráculo son otros smart contracts, ya que estos pueden comunicarse fácilmente con Uniswap para determinar el precio, pero obtener datos de precios de un exchange off-chain sería mucho más difícil.
Sin embargo, simplemente tomar la proporción de los balances para obtener el precio actual no es seguro.
Motivación detrás de TWAP
Medir una captura instantánea (snapshot) de los activos en el pool deja la puerta abierta a ataques de flash loans. Es decir, alguien puede realizar una operación gigante utilizando un flash loan para causar un cambio temporal drástico en el precio, y luego aprovecharse de otro smart contract que utilice este precio para tomar decisiones.
El oráculo de Uniswap V2 se defiende contra esto de dos maneras:
- Proporciona un mecanismo para que los consumidores del precio (generalmente smart contracts) tomen el promedio de un período de tiempo anterior (decidido por el usuario). Esto significa que un atacante tendría que manipular el precio constantemente durante varios bloques, lo cual es mucho más costoso que utilizar un flash loan.
- No incorpora el balance actual en el cálculo del oráculo.
Esto no debería dar la impresión de que los oráculos que usan una media móvil son inmunes a los ataques de manipulación de precios. Si el activo no tiene mucha liquidez, o si la ventana de tiempo para tomar el promedio no es lo suficientemente grande, entonces un atacante con buenos recursos todavía puede sostener el precio (o suprimir el precio) el tiempo suficiente para manipular el precio promedio en el momento de la medición.
Cómo funciona TWAP
Un TWAP (Time Weighted Average Price) es como una media móvil simple, excepto que los periodos donde el precio “se mantuvo igual” durante más tiempo obtienen más peso; un TWAP pondera el precio en función de cuánto tiempo permanece el precio en un cierto nivel.
- Durante el último día, el precio de un activo fue de $10 durante las primeras 12 horas y de $11 durante las segundas 12 horas. El precio promedio es el mismo que el precio promedio ponderado en el tiempo: $10.5.
- Durante el último día, el precio de un activo fue de $10 durante las primeras 23 horas y de $11 durante la hora más reciente. El precio promedio esperado debería estar más cerca de $10 que de $11, pero seguirá estando entre esos valores. Específicamente, será ($10 * 23 + $11 * 1) / 24 = $10.0417
- Durante el último día, el precio de un activo fue de $10 durante la primera hora y de $11 durante las 23 horas más recientes. Esperamos que el TWAP esté más cerca de $11 que de $10. Específicamente, será ($10 * 1 + $11 * 23) / 24 = $10.9583
En general, la fórmula de TWAP es
Aquí T es una duración, no un timestamp. Es decir, cuánto tiempo se mantuvo el precio en ese nivel.
Uniswap V2 no almacena el lookback ni el denominador
En nuestro ejemplo anterior, solo observamos los precios de las últimas 24 horas, pero ¿qué pasa si te interesan los precios de la última hora, semana o algún otro intervalo? Uniswap, por supuesto, no puede almacenar cada periodo retrospectivo (lookback) en el que alguien pueda estar interesado, y tampoco existe una buena manera de hacer un snapshot constante del precio, ya que alguien tendría que pagar por el gas.
La solución es que Uniswap solo almacena el numerador de los valores: cada vez que ocurre un cambio en la proporción de liquidez (cuando se llama a mint, burn, swap o sync), registra el nuevo precio y cuánto tiempo duró el precio anterior.

Las variables price0Cumulativelast y price1CumulativeLast son públicas, por lo que una parte interesada necesita hacer un snapshot de ellas.
Pero este es un punto importante que siempre debes recordar: price0CumulativeLast y price1CumulativeLast solo se actualizan en las líneas 79 y 80 del código anterior (círculo naranja), y solo pueden aumentar hasta sufrir un overflow. No hay ningún mecanismo que haga que “bajen”. Siempre aumentan con cada llamada a _update. Esto significa que acumulan precios desde el momento en que se lanza el pool, lo cual podría ser muchísimo tiempo.
Limitando la ventana de lookback
Claramente, por lo general no estamos interesados en el precio promedio desde que se creó el pool. Solo queremos retroceder una cierta cantidad de tiempo (1 hora, 1 día, etc.).
Aquí está nuevamente la fórmula de TWAP.
Si solo nos interesan los precios desde T4, entonces queremos hacer lo siguiente:
¿Cómo logramos esto con código? Ya que price0Cumulativelast sigue registrando:
Necesitamos una forma de aislar las partes que nos interesan. Consideremos lo siguiente:
Si hacemos un snapshot del precio al final de , obtenemos el valor de UpToTime3. Si esperamos hasta que termine, entonces hacemos price0Cumulativelast - UpToTime3then y obtendremos los precios acumulados de solo la ventana reciente. Si dividimos eso por la duración de la RecentWindow , obtenemos el precio TWAP de la ventana reciente.
Gráficamente, esto es lo que estamos haciendo con el acumulador de precios.

Calculando solo el TWAP de la última hora en Solidity
Si queremos un TWAP de 1 hora, necesitamos anticipar que necesitaremos un snapshot del acumulador dentro de una hora. Por lo tanto, necesitamos acceder a la variable pública price0CumulativeLast y a la función pública getReserves() para obtener el tiempo de la última actualización, y hacer un snapshot de esos valores. (Ver la función snapshot() a continuación).
Después de que haya pasado al menos 1 hora, podemos llamar a getOneHourPrice() y accederemos al valor más reciente de price0CumulativeLast de Uniswap V2.
Desde el momento en que hicimos el snapshot del precio anterior, Uniswap ha estado actualizando el acumulador:
El siguiente código se ha simplificado al máximo con fines ilustrativos; no se recomienda su uso en producción.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/lib/contracts/libraries/UQ112x112.sol";
contract OneHourOracle {
using UQ112x112 for uint224;
IUniswapV2Pair public uniswapV2Pair;
uint256 public snapshotPrice0Cumulative;
uint32 public lastSnapshotTime;
constructor(address _uniswapV2Pair) {
uniswapV2Pair = IUniswapV2Pair(_uniswapV2Pair);
}
function getTimeElapsed() internal view returns (uint32 t) {
unchecked {
t = uint32(block.timestamp % 2**32) - lastSnapshotTime;
}
}
function snapshot() public {
require(getTimeElapsed() >= 1 hours, "snapshot is not stale");
// we don't use the reserves, just need the last timestamp update
(, , lastSnapshotTime) = uniswapV2Pair.getReserves();
snapshotPrice0Cumulative = uniswapV2Pair.price0CumulativeLast();
}
function getOneHourPrice() public view returns (uint224 price) {
require(getTimeElapsed() >= 1 hours, "snapshot not old enough");
require(getTimeElapsed() < 3 hours, "price is too stale");
uint256 recentPriceCumulative = uniswapV2Pair.price0CumulativeLast();
uint32 timeElapsed = getTimeElapsed();
unchecked {
price = uint224((recentPriceCumulative - snapshotPrice0Cumulative) / timeElapsed);
}
}
}
¿Qué pasa si el último snapshot fue hace más de tres horas?
Los lectores astutos pueden notar que el contrato anterior no podrá hacer un snapshot si el par con el que está interactuando no ha tenido ninguna interacción en las últimas tres horas. La función _update de Uniswap V2 se llama durante mint, burn y swap, pero si ninguna de esas interacciones ocurre, entonces lastSnapshotTime registrará un tiempo de hace un buen rato. La solución es que el oráculo llame a la función sync en el momento en que hace un snapshot, ya que eso llamará internamente a _update.
A continuación se muestra una captura de pantalla de la función sync.

Por qué TWAP debe rastrear dos proporciones
El precio de A con respecto a B es simplemente A/B y viceversa. Por ejemplo, si tenemos 2000 USDC en el pool (ignorando los decimales) y 1 Ether, entonces el precio de 1 Ether es simplemente 2000 USDC / 1 ETH.
El precio de USDC, denominado en ETH, es simplemente ese número con el numerador y el denominador invertidos.
Sin embargo, no podemos simplemente “invertir” uno de los precios para obtener el otro cuando estamos acumulando precios. Considera lo siguiente. Si nuestro acumulador de precios comienza en 2 y añade 3, no podemos simplemente hacer uno sobre el acumulador:
Sin embargo, los precios siguen siendo “en cierto modo simétricos”, por lo tanto, la elección de la representación aritmética de coma fija debe tener la misma capacidad para los enteros y para los decimales. Si Eth es 1,000 veces más “valioso” que un USDC, entonces USDC es 1,000 veces “menos valioso” que un USDC. Para almacenar esto con precisión, el número de coma fija debería tener el mismo tamaño en ambos lados del decimal, de ahí la elección de Uniswap de usar u112x112.
PriceCumulativeLast siempre aumenta hasta que sufre un overflow, y luego continúa
Uniswap V2 se construyó antes de Solidity 0.8.0, por lo tanto, la aritmética sufría de overflow y underflow por defecto. Las implementaciones modernas y correctas del oráculo de precios necesitan usar el bloque unchecked para asegurar que todo sufra overflow como se espera.
Eventualmente, los priceAccumulators y el block timestamp sufrirán overflow. En ese caso, la reserva anterior será más alta que la nueva reserva. Cuando el oráculo calcula el cambio en el precio, obtendrá un valor negativo. Sin embargo, esto no importará debido a las reglas de la aritmética modular.
Para simplificar las cosas, usemos enteros sin signo (unsigned integers) imaginarios que sufren un overflow al llegar a 100.
Hacemos un snapshot de priceAccumulator en 80 y unas pocas transacciones/bloques después el priceAccumulator llega a 110, pero sufre un overflow hacia 10. Restamos 80 de 10, lo que da -70. Pero el valor se almacena como un entero sin signo, por lo que da -70 mod(100) que es 30. Ese es el mismo resultado que esperaríamos si no sufriera el overflow (110-80=30).
Esto se cumple para todos los límites de overflow, no solo para 100 como en nuestro ejemplo. El overflow del timestamp o del priceAccumulator no causa problemas debido a cómo funciona la aritmética modular.
Overflow en el timestamp
Lo mismo ocurre cuando el timestamp sufre un overflow. Dado que estamos usando un uint32 para representarlo, no habrá ningún número negativo. Nuevamente, asumamos que ocurre un overflow en 100 por el bien de la simplicidad. Si hacemos un snapshot en el tiempo 98 y consultamos el oráculo de precios en el tiempo 4, entonces han pasado 6 segundos. 4 - 98 % 100 = 6, como se esperaba.
Aprende más con RareSkills
Este material es parte de nuestro Solidity Bootcamp avanzado. Por favor, consulta el programa para aprender más.
Publicado originalmente el 3 de noviembre de 2023