Recuerdo una frase en el círculo de amigos, si Defi es la corona de Ethereum, entonces Uniswap es la joya de esta corona. Uniswap es actualmente la versión V2. En comparación con la V1, sus funciones están más optimizadas, pero su código fuente de contrato no es complicado. Este artículo es una serie de artículos de registro para el aprendizaje personal del código fuente UniswapV2.
1. Introducción al contrato ExampleSlidingWindowOracle
Este contrato es el mismo que el ExampleOracleSimple
contrato aprendido en el artículo anterior , utilizando UniswapV2 como la máquina de oráculo de precios. Pero los dos escenarios de aplicación son diferentes:
ExampleOracleSimple
El contrato se utiliza en un modo de ventana fija, en el que los datos históricos no son importantes y el precio actual tiene el mismo peso que el precio histórico. Por lo tanto, es suficiente registrar (actualizar) el precio promedio una vez por ciclo.ExampleSlidingWindowOracle
Utilizado en el modo de ventana deslizante, puede registrar información relacionada con el precio varias veces en un ciclo. El modo de ventana deslizante también se divide en dos categorías, una es la media móvil simple, lo que significa que cada cálculo de precio tiene el mismo peso. El otro es el promedio móvil exponencial, el cálculo de precio más reciente tiene un valor de peso mayor.
Este contrato es un ejemplo de implementación de media móvil simple. Para obtener más información sobre el uso de UniswapV2 como una máquina de oráculo de precios, lea su documento Comprando un Oracle . Este documento también explica los problemas de desbordamiento en los cálculos de precios.
2. Código fuente del contrato
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import '@uniswap/lib/contracts/libraries/FixedPoint.sol';
import '../libraries/SafeMath.sol';
import '../libraries/UniswapV2Library.sol';
import '../libraries/UniswapV2OracleLibrary.sol';
// sliding window oracle that uses observations collected over a window to provide moving price averages in the past
// `windowSize` with a precision of `windowSize / granularity`
// note this is a singleton oracle and only needs to be deployed once per desired parameters, which
// differs from the simple oracle which must be deployed once per pair.
contract ExampleSlidingWindowOracle {
using FixedPoint for *;
using SafeMath for uint;
struct Observation {
uint timestamp;
uint price0Cumulative;
uint price1Cumulative;
}
address public immutable factory;
// the desired amount of time over which the moving average should be computed, e.g. 24 hours
uint public immutable windowSize;
// the number of observations stored for each pair, i.e. how many price observations are stored for the window.
// as granularity increases from 1, more frequent updates are needed, but moving averages become more precise.
// averages are computed over intervals with sizes in the range:
// [windowSize - (windowSize / granularity) * 2, windowSize]
// e.g. if the window size is 24 hours, and the granularity is 24, the oracle will return the average price for
// the period:
// [now - [22 hours, 24 hours], now]
uint8 public immutable granularity;
// this is redundant with granularity and windowSize, but stored for gas savings & informational purposes.
uint public immutable periodSize;
// mapping from pair address to a list of price observations of that pair
mapping(address => Observation[]) public pairObservations;
constructor(address factory_, uint windowSize_, uint8 granularity_) public {
require(granularity_ > 1, 'SlidingWindowOracle: GRANULARITY');
require(
(periodSize = windowSize_ / granularity_) * granularity_ == windowSize_,
'SlidingWindowOracle: WINDOW_NOT_EVENLY_DIVISIBLE'
);
factory = factory_;
windowSize = windowSize_;
granularity = granularity_;
}
// returns the index of the observation corresponding to the given timestamp
function observationIndexOf(uint timestamp) public view returns (uint8 index) {
uint epochPeriod = timestamp / periodSize;
return uint8(epochPeriod % granularity);
}
// returns the observation from the oldest epoch (at the beginning of the window) relative to the current time
function getFirstObservationInWindow(address pair) private view returns (Observation storage firstObservation) {
uint8 observationIndex = observationIndexOf(block.timestamp);
// no overflow issue. if observationIndex + 1 overflows, result is still zero.
uint8 firstObservationIndex = (observationIndex + 1) % granularity;
firstObservation = pairObservations[pair][firstObservationIndex];
}
// update the cumulative price for the observation at the current timestamp. each observation is updated at most
// once per epoch period.
function update(address tokenA, address tokenB) external {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// populate the array with empty observations (first call only)
for (uint i = pairObservations[pair].length; i < granularity; i++) {
pairObservations[pair].push();
}
// get the observation for the current period
uint8 observationIndex = observationIndexOf(block.timestamp);
Observation storage observation = pairObservations[pair][observationIndex];
// we only want to commit updates once per period (i.e. windowSize / granularity)
uint timeElapsed = block.timestamp - observation.timestamp;
if (timeElapsed > periodSize) {
(uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair);
observation.timestamp = block.timestamp;
observation.price0Cumulative = price0Cumulative;
observation.price1Cumulative = price1Cumulative;
}
}
// given the cumulative prices of the start and end of a period, and the length of the period, compute the average
// price in terms of how much amount out is received for the amount in
function computeAmountOut(
uint priceCumulativeStart, uint priceCumulativeEnd,
uint timeElapsed, uint amountIn
) private pure returns (uint amountOut) {
// overflow is desired.
FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(
uint224((priceCumulativeEnd - priceCumulativeStart) / timeElapsed)
);
amountOut = priceAverage.mul(amountIn).decode144();
}
// returns the amount out corresponding to the amount in for a given token using the moving average over the time
// range [now - [windowSize, windowSize - periodSize * 2], now]
// update must have been called for the bucket corresponding to timestamp `now - windowSize`
function consult(address tokenIn, uint amountIn, address tokenOut) external view returns (uint amountOut) {
address pair = UniswapV2Library.pairFor(factory, tokenIn, tokenOut);
Observation storage firstObservation = getFirstObservationInWindow(pair);
uint timeElapsed = block.timestamp - firstObservation.timestamp;
require(timeElapsed <= windowSize, 'SlidingWindowOracle: MISSING_HISTORICAL_OBSERVATION');
// should never happen.
require(timeElapsed >= windowSize - periodSize * 2, 'SlidingWindowOracle: UNEXPECTED_TIME_ELAPSED');
(uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair);
(address token0,) = UniswapV2Library.sortTokens(tokenIn, tokenOut);
if (token0 == tokenIn) {
return computeAmountOut(firstObservation.price0Cumulative, price0Cumulative, timeElapsed, amountIn);
} else {
return computeAmountOut(firstObservation.price1Cumulative, price1Cumulative, timeElapsed, amountIn);
}
}
}
Tres, un breve estudio del código fuente
Estudiemos brevemente el código fuente de este contrato. Es posible que la comprensión personal no sea correcta, deje un mensaje para corregirme.
-
Versión de solidez en la primera línea
-
Las líneas 2-7 importan 6 definiciones de interfaz o bibliotecas de herramientas, que es solo una más que el artículo anterior
SafeMath
. -
A continuación está la nota del contrato (descripción), que revela varios puntos clave:
- La ventana deslizante adopta el modo de observador. El tamaño de la ventana de observación (tiempo) es
windowSize
, y la precisión eswindowSize / granularity
. Elgranularity
valor literal aquí es la granularidad, que en realidad significa el escenario. Se asume aquí quewindowSize
son 24 horas, es decir, la ventana de observación es 24 horas. La granularidad es 8, luego la precisión es 3 horas, es decir, el precio promedio se puede registrar 8 veces en un ciclo, por lo que es más fácil ver la tendencia del precio. - Para los parámetros fijos, este contrato solo debe implementarse una vez, que es un contrato único. El modo de ventana fija del artículo anterior requiere que se implemente un contrato para cada par comercial.
- La ventana deslizante adopta el modo de observador. El tamaño de la ventana de observación (tiempo) es
-
La definición del contrato y las siguientes dos
using
gramáticas se han estudiado muchas veces, y también son muy simples, así que omítalas directamente. -
struct Observation { uint timestamp; uint price0Cumulative; uint price1Cumulative; }
Define una estructura llamada observador. Tiene tres campos: el primer campo registra el tiempo de bloque durante la observación y los dos últimos campos registran el valor acumulado del precio durante la observación.
-
Lo siguiente es la definición de cuatro variables de estado, respectivamente
factory,windowSize,granularity,periodSize
. Sus significados son lafactory
dirección del contrato de V2 , el tamaño de la ventana de observación, la granularidad y la precisión de la ventana de observación (es decir, el tamaño de la ventana dividido por la granularidad). Las notas aquí también mencionan una gran cantidad de contenido, como cuanto mayor es la granularidad, más frecuente es la actualización y más preciso es el precio promedio móvil. Allí se mencionan, podrían haber sido revisadasgranularity
ywindowSize
calculadasperiodSize
, pero con el fin de ahorrar de forma más intuitiva y Gas, también se registra como una variable de estado. -
pairObservations
Utilice un mapa para registrar los observadores de cada par comercial. El observador es una matriz y su longitud es granularidad, que representa el número de observaciones. -
El siguiente es el constructor.
- Primero, verifique que la granularidad no pueda ser 0, porque es un divisor. Aunque dividir por cero sin verificación también informará una transacción de restablecimiento de error, el
require
significado es más claro. - Luego verifique que la ventana de observación pueda ser divisible por la granularidad y
periodSize
asigne valores al mismo tiempo . Esto es obvio, de lo contrario habrá un espacio en la ventana de observación. Observe la sintaxis aquí: el(periodSize = windowSize_ / granularity_) * granularity_ == windowSize_,
extremo izquierdo de la ecuación usa una expresión como multiplicador. Esto no es compatible con algunos lenguajes de programación y las expresiones no se pueden mezclar con valores. Pero enJavaScript
una serie de idiomas admitidos, el valor del lado izquierdo de la expresión es el valor de la expresión. Aunque esta sintaxis rara vez se usa en Solidity, muestra que también es compatible. - A continuación, establezca el valor de la variable de estado previamente definida (parámetro de observación). Tenga en cuenta que
granularity
es deuint8
tipo, es decir, un contenedor puede grabar hasta 255 veces, lo cual es suficiente.
- Primero, verifique que la granularidad no pueda ser 0, porque es un divisor. Aunque dividir por cero sin verificación también informará una transacción de restablecimiento de error, el
-
Función
observationIndexOf
para obtener el índice de observador en un momento dado. Primero divide porperiodSize
para obtener el resto, asumiendo que se llama E (es decir, cuántas precisiones contiene). Debidogranularity
aluint8
tamaño, es imposible registrar todos los datos E. Así que solo toma el módulo para reciclar. Dado que una operaciónuint
ANDuint8
sigue siendo unauint
, es necesario convertirla a por finuint8
. -
getFirstObservationInWindow
La función es una función privada. El comentario mencionado es el primer observador en obtener la nueva ventana actual. Su índice suma 1 al índice del registro de bloque actual.¿Por qué agregar 1? Porque el observador es cíclico. Si el último índice se incrementa en 1, entonces su posición está vacía o tiene un valor antiguo. Tener un valor antiguo equivale a volver al comienzo de un período de ventana. Esta función se utiliza en cálculos posteriores, de modo que el tiempo de bloque actual menos el tiempo de inicio del período de ventana cuando se calcula es exactamente un período de ventana.
Para evitar el desbordamiento, se adopta el método de módulo, que por supuesto es equivalente a la conversión directa de tipos. Esto también se menciona en el aprendizaje de pares de negociación de contratos básicos. Lo último a tener en cuenta es que, debido a que es una función privada, se usa internamente. Por tanto
storage
,Observation
se devuelve una variable del tipo, de modo que la referencia se pasará al pasar, evitando la sobrecarga de copiar el objeto. -
update
función. Actualiza el precio acumulado del observador de bloque actual. Se menciona en los comentarios que cadaperiod
(precisión) se actualiza como máximo una vez. Los parámetros de la función son las dos direcciones de token del par comercial.- Primero use la biblioteca de herramientas UniswapV2 para calcular la dirección del par comercial.
- El siguiente es un
for
bucle, si la matriz de observadores del par comercial no se inicializa en este momento, se inicializa con datos vacíos. La longitud de la matriz es lagranularity
misma después de la inicialización , por lo que no se inicializará una segunda vez. - Las siguientes dos líneas de código obtienen la información del observador registrada en el bloque actual.
- A continuación, se
*uint* timeElapsed = *block*.timestamp - observation.timestamp;
utiliza para calcular la diferencia entre el tiempo de bloqueo actual y el tiempo registrado por el observador actual (el tiempo registrado actual también puede ser 0, es decir, no se ha registrado). - La siguiente es una
if
oración para determinar si la diferencia de tiempo es mayor que la precisión especificada (registre como máximo una vez dentro de una precisión). Si se cumplen las condiciones, la biblioteca de herramientas UniswapV2 se utiliza para calcular el precio acumulativo del bloque actual y actualizar el registro del observador actual. Esto actualiza el valor acumulado del precio del observador del bloque actual y el tiempo del bloque (si se cumple el requisito de intervalo de tiempo). Tenga en cuenta aquí: los observadores se reciclan y los nuevos sobrescribirán a los antiguos.
-
computeAmountOut
función. También es una función privada que utiliza el precio medio para calcular la cantidad de un determinado activo. Tenga en cuenta que el método de cálculo del precio medio es el mismo que se menciona en el artículo anterior, es decir, la diferencia de precio acumulada dividida por el intervalo de tiempo (similar a la fórmula para calcular la velocidad media). -
consult
Función de consulta. Basado en el precio promedio durante toda la ventana, dada la cantidad de un token, calcule la cantidad de otro token. Sus parámetros son la dirección y la cantidad del token de entrada y la dirección del token que se va a calcular.- En los comentarios de la función se menciona mucha información, como el intervalo de tiempo del precio medio utilizado en la consulta. Además, el período de tiempo correspondiente debe tener precios actualizados.
- La primera línea de la función se utiliza para calcular la dirección del par comercial.
- La segunda línea obtiene el primer observador de la nueva ventana.
- La tercera línea calcula la diferencia entre el tiempo de bloque actual y el tiempo registrado por el primer observador en la nueva ventana.
- La cuarta línea verifica que la diferencia de tiempo debe ser menor que un período de ventana, es decir, no se puede actualizar por mucho tiempo.
- La quinta línea se utiliza para verificar el límite inferior de la diferencia horaria.
- La sexta línea se utiliza para obtener la diferencia de precio acumulada del bloque actual.
- La séptima línea se utiliza para ordenar las dos direcciones de token de los parámetros de entrada.
- La función finalmente calcula el número de tokens según si la entrada está
token0
quietatoken1
. ¿Por qué utilizar el primer cálculo de índice de la nueva ventana aquí? Consulte lagetFirstObservationInWindow
descripción de la función.
Cuatro, resumen
Nota: Aunque la precisión (etapas) se divide de acuerdo con la granularidad en el período de la ventana, cada etapa registra el tiempo de bloque del observador y el precio acumulado en ese momento. Su función es reflejar el deslizamiento del precio y la otra no es utilizar la misma acumulación. Puntos de precio (no fijos, en relación con los puntos de precio acumulados fijos del artículo anterior) para calcular el precio medio. Sin embargo, el precio promedio todavía se calcula durante todo el período de ventana, no un precio promedio con precisión.
Durante cada consulta, el período de ventana de la consulta se period
deslizará una cuadrícula de arriba a la derecha (una granularidad), por lo que se denomina ventana deslizante vívidamente.
Cada máquina debe utilizar dichas predicciones, period
debe actualizarse el valor del precio acumulado, ad infinitum; de lo contrario, la posición inicial de la ventana durante este period
tiempo, habrá circunstancias en que el intervalo de consulta sea mayor que durante la ventana, lo que provocará que la consulta falle. Pero siempre que period
la información del observador se actualice nuevamente , la consulta se puede reanudar.
Bueno, este estudio ha terminado, la próxima vez que planeo estudiar examples
bajo el catálogo ExampleSwapToPrice.sol
.
Debido a la capacidad personal limitada, es inevitable que haya errores o entendimientos incorrectos. Deje un mensaje para corregirlo.