Análisis detallado del contrato Uniswap-v2
Introducción
Uniswap v2 (opens in a new tab) puede crear un mercado de intercambio entre dos tokens ERC-20 cualesquiera. En este artículo repasaremos el código fuente de los contratos que implementan este protocolo y veremos por qué están escritos de esta manera.
¿Qué hace Uniswap?
Básicamente, hay dos tipos de usuarios: proveedores de liquidez y comerciantes.
Los proveedores de liquidez proporcionan al fondo de liquidez los dos tokens que se pueden intercambiar (los llamaremos Token0 y Token1). A cambio, reciben un tercer token que representa la propiedad parcial del fondo, llamado token de liquidez.
Los comerciantes envían un tipo de token al fondo de liquidez y reciben el otro (por ejemplo, envían Token0 y reciben Token1) del fondo proporcionado por los proveedores de liquidez. El tipo de cambio está determinado por el número relativo de Token0 y Token1 que tiene el fondo. Además, el fondo toma un pequeño porcentaje como recompensa para el fondo de liquidez.
Cuando los proveedores de liquidez quieren recuperar sus activos, pueden quemar los tokens del fondo y recibir de vuelta sus tokens, incluida su parte de las recompensas.
Haga clic aquí para obtener una descripción más completa (opens in a new tab).
¿Por qué v2? ¿Por qué no v3?
Uniswap v3 (opens in a new tab) es una actualización que es mucho más complicada que la v2. Es más fácil aprender primero la v2 y luego pasar a la v3.
Contratos principales frente a contratos periféricos
Uniswap v2 se divide en dos componentes, uno principal (core) y uno periférico (periphery). Esta división permite que los contratos principales, que mantienen los activos y, por lo tanto, tienen que ser seguros, sean más simples y fáciles de auditar. Toda la funcionalidad adicional requerida por los comerciantes puede ser proporcionada por los contratos periféricos.
Flujos de datos y control
Este es el flujo de datos y control que ocurre cuando realizas las tres acciones principales de Uniswap:
- Intercambio entre diferentes tokens
- Añadir liquidez al mercado y obtener como recompensa tokens de liquidez ERC-20 del intercambio de pares
- Quemar tokens de liquidez ERC-20 y recuperar los tokens ERC-20 que el intercambio de pares permite intercambiar a los comerciantes
Intercambio
Este es el flujo más común, utilizado por los comerciantes:
Invocador
- Proporcionar a la cuenta de la periferia una asignación por la cantidad a intercambiar.
- Llamar a una de las muchas funciones de intercambio del contrato de la periferia (cuál depende de si hay ETH involucrado o no, si el comerciante especifica la cantidad de tokens a depositar o la cantidad de tokens a recuperar, etc.).
Cada función de intercambio acepta un
path, una matriz de intercambios por los que pasar.
En el contrato de la periferia (UniswapV2Router02.sol)
- Identificar las cantidades que deben negociarse en cada intercambio a lo largo de la ruta.
- Itera sobre la ruta. Para cada intercambio en el camino, envía el token de entrada y luego llama a la función
swapdel intercambio. En la mayoría de los casos, la dirección de destino para los tokens es el siguiente intercambio de pares en la ruta. En el intercambio final, es la dirección proporcionada por el comerciante.
En el contrato principal (UniswapV2Pair.sol)
- Verificar que el contrato principal no esté siendo engañado y pueda mantener suficiente liquidez después del intercambio.
- Ver cuántos tokens adicionales tenemos además de las reservas conocidas. Esa cantidad es el número de tokens de entrada que recibimos para intercambiar.
- Enviar los tokens de salida al destino.
- Llamar a
_updatepara actualizar las cantidades de reserva
De vuelta en el contrato de la periferia (UniswapV2Router02.sol)
- Realizar cualquier limpieza necesaria (por ejemplo, quemar tokens WETH para recuperar ETH y enviarlo al comerciante)
Añadir liquidez
Invocador
- Proporcionar a la cuenta de la periferia una asignación por las cantidades que se añadirán al fondo de liquidez.
- Llamar a una de las funciones
addLiquiditydel contrato de la periferia.
En el contrato de la periferia (UniswapV2Router02.sol)
- Crear un nuevo intercambio de pares si es necesario
- Si existe un intercambio de pares, calcular la cantidad de tokens a añadir. Se supone que este es un valor idéntico para ambos tokens, por lo que es la misma proporción de tokens nuevos con respecto a los tokens existentes.
- Comprobar si las cantidades son aceptables (los invocadores pueden especificar una cantidad mínima por debajo de la cual prefieren no añadir liquidez)
- Llamar al contrato principal.
En el contrato principal (UniswapV2Pair.sol)
- Acuñar tokens de liquidez y enviarlos al invocador
- Llamar a
_updatepara actualizar las cantidades de reserva
Eliminar liquidez
Invocador
- Proporcionar a la cuenta de la periferia una asignación de tokens de liquidez para ser quemados a cambio de los tokens subyacentes.
- Llamar a una de las funciones
removeLiquiditydel contrato de la periferia.
En el contrato de la periferia (UniswapV2Router02.sol)
- Enviar los tokens de liquidez al intercambio de pares
En el contrato principal (UniswapV2Pair.sol)
- Enviar a la dirección de destino los tokens subyacentes en proporción a los tokens quemados. Por ejemplo, si hay 1000 tokens A en el fondo, 500 tokens B y 90 tokens de liquidez, y recibimos 9 tokens para quemar, estamos quemando el 10 % de los tokens de liquidez, por lo que devolvemos al usuario 100 tokens A y 50 tokens B.
- Quemar los tokens de liquidez
- Llamar a
_updatepara actualizar las cantidades de reserva
Los contratos principales
Estos son los contratos seguros que mantienen la liquidez.
UniswapV2Pair.sol
Este contrato (opens in a new tab) implementa el fondo real que intercambia tokens. Es la funcionalidad principal de Uniswap.
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';
Estas son todas las interfaces que el contrato necesita conocer, ya sea porque el contrato las implementa (IUniswapV2Pair y UniswapV2ERC20) o porque llama a contratos que las implementan.
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
Este contrato hereda de UniswapV2ERC20, que proporciona las funciones ERC-20 para los tokens de liquidez.
using SafeMath for uint;
La biblioteca SafeMath (opens in a new tab) se utiliza para evitar desbordamientos por exceso y por defecto (overflows y underflows). Esto es importante porque, de lo contrario, podríamos terminar en una situación en la que un valor debería ser -1, pero en su lugar es 2^256-1.
using UQ112x112 for uint224;
Muchos cálculos en el contrato del fondo requieren fracciones. Sin embargo, la EVM no admite fracciones.
La solución que encontró Uniswap es usar valores de 224 bits, con 112 bits para la parte entera y 112 bits para la fracción. Así que 1.0 se representa como 2^112, 1.5 se representa como 2^112 + 2^111, etc.
Hay más detalles sobre esta biblioteca disponibles más adelante en el documento.
Variables
uint public constant MINIMUM_LIQUIDITY = 10**3;
Para evitar casos de división por cero, hay un número mínimo de tokens de liquidez que siempre existen (pero son propiedad de la cuenta cero). Ese número es MINIMUM_LIQUIDITY, mil.
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
Este es el selector de la ABI para la función de transferencia ERC-20. Se utiliza para transferir tokens ERC-20 en las dos cuentas de tokens.
address public factory;
Este es el contrato de fábrica que creó este fondo. Cada fondo es un intercambio entre dos tokens ERC-20, la fábrica es un punto central que conecta todos estos fondos.
address public token0;
address public token1;
Estas son las direcciones de los contratos para los dos tipos de tokens ERC-20 que pueden ser intercambiados por este fondo.
uint112 private reserve0; // utiliza una única ranura de almacenamiento, accesible mediante getReserves
uint112 private reserve1; // utiliza una única ranura de almacenamiento, accesible mediante getReserves
Las reservas que tiene el fondo para cada tipo de token. Asumimos que los dos representan la misma cantidad de valor y, por lo tanto, cada token0 vale reserve1/reserve0 token1.
uint32 private blockTimestampLast; // utiliza una única ranura de almacenamiento, accesible mediante getReserves
La marca de tiempo del último bloque en el que ocurrió un intercambio, utilizada para rastrear los tipos de cambio a lo largo del tiempo.
Uno de los mayores gastos de gas de los contratos de Ethereum es el almacenamiento, que persiste de una llamada del contrato a la siguiente. Cada celda de almacenamiento tiene una longitud de 256 bits. Por lo tanto, tres variables, reserve0, reserve1 y blockTimestampLast, se asignan de tal manera que un solo valor de almacenamiento puede incluir a las tres (112+112+32=256).
uint public price0CumulativeLast;
uint public price1CumulativeLast;
Estas variables mantienen los costos acumulativos para cada token (cada uno en términos del otro). Se pueden usar para calcular el tipo de cambio promedio durante un período de tiempo.
uint public kLast; // reserve0 * reserve1, inmediatamente después del evento de liquidez más reciente
La forma en que el intercambio de pares decide el tipo de cambio entre token0 y token1 es mantener constante el múltiplo de las dos reservas durante las operaciones. kLast es este valor. Cambia cuando un proveedor de liquidez deposita o retira tokens, y aumenta ligeramente debido a la tarifa de mercado del 0,3 %.
Aquí hay un ejemplo simple. Tenga en cuenta que, en aras de la simplicidad, la tabla solo tiene tres dígitos después del punto decimal, e ignoramos la tarifa de negociación del 0,3 %, por lo que los números no son exactos.
| Evento | reserve0 | reserve1 | reserve0 * reserve1 | Tipo de cambio promedio (token1 / token0) |
|---|---|---|---|---|
| Configuración inicial | 1,000.000 | 1,000.000 | 1,000,000 | |
| El comerciante A intercambia 50 token0 por 47.619 token1 | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
| El comerciante B intercambia 10 token0 por 8.984 token1 | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
| El comerciante C intercambia 40 token0 por 34.305 token1 | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
| El comerciante D intercambia 100 token1 por 109.01 token0 | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
| El comerciante E intercambia 10 token0 por 10.079 token1 | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
A medida que los comerciantes proporcionan más token0, el valor relativo de token1 aumenta, y viceversa, según la oferta y la demanda.
Bloqueo
uint private unlocked = 1;
Hay una clase de vulnerabilidades de seguridad que se basan en el abuso de reentrada (opens in a new tab). Uniswap necesita transferir tokens ERC-20 arbitrarios, lo que significa llamar a contratos ERC-20 que pueden intentar abusar del mercado de Uniswap que los llama.
Al tener una variable unlocked como parte del contrato, podemos evitar que se llame a las funciones mientras se están ejecutando (dentro de la misma transacción).
modifier lock() {
Esta función es un modificador (opens in a new tab), una función que envuelve a una función normal para cambiar su comportamiento de alguna manera.
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
Si unlocked es igual a uno, establézcalo en cero. Si ya es cero, revertir la llamada, hacer que falle.
_;
En un modificador, _; es la llamada a la función original (con todos los parámetros). Aquí significa que la llamada a la función solo ocurre si unlocked era uno cuando se llamó, y mientras se está ejecutando el valor de unlocked es cero.
unlocked = 1;
}
Después de que la función principal regrese, libere el bloqueo.
Funciones misceláneas
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
Esta función proporciona a los llamadores el estado actual del intercambio. Tenga en cuenta que las funciones de Solidity pueden devolver múltiples valores (opens in a new tab).
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
Esta función interna transfiere una cantidad de tokens ERC-20 del intercambio a otra persona. SELECTOR especifica que la función a la que estamos llamando es transfer(address,uint) (consulte la definición anterior).
Para evitar tener que importar una interfaz para la función del token, creamos "manualmente" la llamada utilizando una de las funciones de la ABI (opens in a new tab).
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
Hay dos formas en las que una llamada de transferencia ERC-20 puede informar de un fallo:
- Revertir. Si una llamada a un contrato externo se revierte, entonces el valor de retorno booleano es
false - Terminar normalmente pero informar de un fallo. En ese caso, el búfer del valor de retorno tiene una longitud distinta de cero, y cuando se decodifica como un valor booleano es
false
Si ocurre alguna de estas condiciones, revertir.
Eventos
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
Estos dos eventos se emiten cuando un proveedor de liquidez deposita liquidez (Mint) o la retira (Burn). En cualquier caso, las cantidades de token0 y token1 que se depositan o retiran son parte del evento, así como la identidad de la cuenta que nos llamó (sender). En el caso de un retiro, el evento también incluye el destino que recibió los tokens (to), que puede no ser el mismo que el remitente.
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
Este evento se emite cuando un comerciante intercambia un token por el otro. Nuevamente, el remitente y el destino pueden no ser los mismos. Cada token puede ser enviado al intercambio o recibido de él.
event Sync(uint112 reserve0, uint112 reserve1);
Finalmente, Sync se emite cada vez que se agregan o retiran tokens, independientemente del motivo, para proporcionar la información de reserva más reciente (y, por lo tanto, el tipo de cambio).
Funciones de configuración
Se supone que estas funciones se llaman una vez cuando se configura el nuevo intercambio de pares.
constructor() public {
factory = msg.sender;
}
El constructor se asegura de que mantendremos un registro de la dirección de la fábrica que creó el par. Esta información es necesaria para initialize y para la tarifa de fábrica (si existe alguna)
// llamado una vez por la fábrica en el momento del despliegue
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // comprobación suficiente
token0 = _token0;
token1 = _token1;
}
Esta función permite a la fábrica (y solo a la fábrica) especificar los dos tokens ERC-20 que intercambiará este par.
Funciones de actualización interna
_update
// actualiza las reservas y, en la primera llamada por bloque, los acumuladores de precios
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
Esta función se llama cada vez que se depositan o retiran tokens.
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
Si balance0 o balance1 (uint256) es mayor que uint112(-1) (=2^112-1) (por lo que se desborda y vuelve a 0 cuando se convierte a uint112), se niega a continuar con _update para evitar desbordamientos. Con un token normal que se puede subdividir en 10^18 unidades, esto significa que cada intercambio está limitado a aproximadamente 5,1*10^15 de cada token. Hasta ahora eso no ha sido un problema.
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // se desea el desbordamiento
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
Si el tiempo transcurrido no es cero, significa que somos la primera transacción de intercambio en este bloque. En ese caso, necesitamos actualizar los acumuladores de costos.
// * nunca se desborda, y se desea el desbordamiento de +
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
Cada acumulador de costos se actualiza con el último costo (reserva del otro token/reserva de este token) multiplicado por el tiempo transcurrido en segundos. Para obtener un precio promedio, se lee el precio acumulativo en dos puntos en el tiempo y se divide por la diferencia de tiempo entre ellos. Por ejemplo, asuma esta secuencia de eventos:
| Evento | reserve0 | reserve1 | marca de tiempo | Tipo de cambio marginal (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Configuración inicial | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
| El comerciante A deposita 50 token0 y recibe 47.619 token1 a cambio | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
| El comerciante B deposita 10 token0 y recibe 8.984 token1 a cambio | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
| El comerciante C deposita 40 token0 y recibe 34.305 token1 a cambio | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
| El comerciante D deposita 100 token1 y recibe 109.01 token0 a cambio | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
| El comerciante E deposita 10 token0 y recibe 10.079 token1 a cambio | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.63+40*1.1018 = 143.702 |
Digamos que queremos calcular el precio promedio de Token0 entre las marcas de tiempo 5.030 y 5.150. La diferencia en el valor de price0Cumulative es 143,702-29,07=114,632. Este es el promedio a lo largo de dos minutos (120 segundos). Así que el precio promedio es 114,632/120 = 0,955.
Este cálculo de precio es la razón por la que necesitamos conocer los tamaños de las reservas antiguas.
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
Finalmente, actualice las variables globales y emita un evento Sync.
_mintFee
// si la tarifa está activada, acuñar liquidez equivalente a 1/6 del crecimiento en sqrt(k)
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
En Uniswap 2.0, los comerciantes pagan una tarifa del 0,30 % para usar el mercado. La mayor parte de esa tarifa (0,25 % de la operación) siempre va a los proveedores de liquidez. El 0,05 % restante puede ir a los proveedores de liquidez o a una dirección especificada por la fábrica como una tarifa de protocolo, que paga a Uniswap por su esfuerzo de desarrollo.
Para reducir los cálculos (y, por lo tanto, los costos de gas), esta tarifa solo se calcula cuando se agrega o retira liquidez del fondo, en lugar de en cada transacción.
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
Lea el destino de la tarifa de la fábrica. Si es cero, entonces no hay tarifa de protocolo y no hay necesidad de calcular esa tarifa.
uint _kLast = kLast; // ahorro de gas
La variable de estado kLast se encuentra en el almacenamiento, por lo que tendrá un valor entre diferentes llamadas al contrato.
El acceso al almacenamiento es mucho más costoso que el acceso a la memoria volátil que se libera cuando finaliza la llamada a la función del contrato, por lo que usamos una variable interna para ahorrar gas.
if (feeOn) {
if (_kLast != 0) {
Los proveedores de liquidez obtienen su parte simplemente por la apreciación de sus tokens de liquidez. Pero la tarifa del protocolo requiere que se acuñen nuevos tokens de liquidez y se proporcionen a la dirección feeTo.
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
Si hay nueva liquidez sobre la cual cobrar una tarifa de protocolo. Puede ver la función de raíz cuadrada más adelante en este artículo
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
Este complicado cálculo de tarifas se explica en el documento técnico (opens in a new tab) en la página 5. Sabemos que entre el momento en que se calculó kLast y el presente no se agregó ni retiró liquidez (porque ejecutamos este cálculo cada vez que se agrega o retira liquidez, antes de que realmente cambie), por lo que cualquier cambio en reserve0 * reserve1 tiene que provenir de las tarifas de transacción (sin ellas mantendríamos reserve0 * reserve1 constante).
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
Use la función UniswapV2ERC20._mint para crear realmente los tokens de liquidez adicionales y asignarlos a feeTo.
} else if (_kLast != 0) {
kLast = 0;
}
}
Si no hay tarifa, establezca kLast en cero (si no lo está ya). Cuando se escribió este contrato, había una función de reembolso de gas (opens in a new tab) que animaba a los contratos a reducir el tamaño general del estado de Ethereum poniendo a cero el almacenamiento que no necesitaban.
Este código obtiene ese reembolso cuando es posible.
Funciones accesibles externamente
Tenga en cuenta que, si bien cualquier transacción o contrato puede llamar a estas funciones, están diseñadas para ser llamadas desde el contrato periférico. Si las llama directamente, no podrá engañar al intercambio de pares, pero podría perder valor por un error.
mint
// esta función de bajo nivel debe ser llamada desde un contrato que realice comprobaciones de seguridad importantes
function mint(address to) external lock returns (uint liquidity) {
Esta función se llama cuando un proveedor de liquidez agrega liquidez al fondo. Acuña tokens de liquidez adicionales como recompensa. Debería ser llamada desde un contrato periférico que la llama después de agregar la liquidez en la misma transacción (para que nadie más pueda enviar una transacción que reclame la nueva liquidez antes que el propietario legítimo).
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // ahorro de gas
Esta es la forma de leer los resultados de una función de Solidity que devuelve múltiples valores. Descartamos los últimos valores devueltos, la marca de tiempo del bloque, porque no la necesitamos.
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
Obtenga los saldos actuales y vea cuánto se agregó de cada tipo de token.
bool feeOn = _mintFee(_reserve0, _reserve1);
Calcule las tarifas del protocolo a cobrar, si las hay, y acuñe tokens de liquidez en consecuencia. Debido a que los parámetros para _mintFee son los valores de reserva antiguos, la tarifa se calcula con precisión basándose solo en los cambios del fondo debido a las tarifas.
uint _totalSupply = totalSupply; // ahorro de gas, debe definirse aquí ya que totalSupply puede actualizarse en _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // bloquear permanentemente los primeros MINIMUM_LIQUIDITY tokens
Si este es el primer depósito, cree MINIMUM_LIQUIDITY tokens y envíelos a la dirección cero para bloquearlos. Nunca se pueden canjear, lo que significa que el fondo nunca se vaciará por completo (esto nos salva de la división por cero en algunos lugares). El valor de MINIMUM_LIQUIDITY es mil, lo que considerando que la mayoría de los ERC-20 se subdividen en unidades de 10^-18 de un token, al igual que ETH se divide en Wei, es 10^-15 del valor de un solo token. No es un costo alto.
En el momento del primer depósito no conocemos el valor relativo de los dos tokens, así que simplemente multiplicamos las cantidades y sacamos una raíz cuadrada, asumiendo que el depósito nos proporciona el mismo valor en ambos tokens.
Podemos confiar en esto porque es de interés del depositante proporcionar el mismo valor, para evitar perder valor por el arbitraje. Digamos que el valor de los dos tokens es idéntico, pero nuestro depositante depositó cuatro veces más de Token1 que de Token0. Un comerciante puede usar el hecho de que el intercambio de pares piensa que Token0 es más valioso para extraer valor de él.
| Evento | reserve0 | reserve1 | reserve0 * reserve1 | Valor del fondo (reserve0 + reserve1) |
|---|---|---|---|---|
| Configuración inicial | 8 | 32 | 256 | 40 |
| El comerciante deposita 8 tokens Token0, recibe 16 Token1 a cambio | 16 | 16 | 256 | 32 |
Como puede ver, el comerciante ganó 8 tokens adicionales, que provienen de una reducción en el valor del fondo, perjudicando al depositante que lo posee.
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
Con cada depósito posterior ya conocemos el tipo de cambio entre los dos activos, y esperamos que los proveedores de liquidez proporcionen el mismo valor en ambos. Si no lo hacen, les damos tokens de liquidez basados en el menor valor que proporcionaron como castigo.
Ya sea el depósito inicial o uno posterior, el número de tokens de liquidez que proporcionamos es igual a la raíz cuadrada del cambio en reserve0*reserve1 y el valor del token de liquidez no cambia (a menos que recibamos un depósito que no tenga valores iguales de ambos tipos, en cuyo caso la "multa" se distribuye). Aquí hay otro ejemplo con dos tokens que tienen el mismo valor, con tres depósitos buenos y uno malo (depósito de un solo tipo de token, por lo que no produce ningún token de liquidez).
| Evento | reserve0 | reserve1 | reserve0 * reserve1 | Valor del fondo (reserve0 + reserve1) | Tokens de liquidez acuñados para este depósito | Total de tokens de liquidez | valor de cada token de liquidez |
|---|---|---|---|---|---|---|---|
| Configuración inicial | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
| Depósito de cuatro de cada tipo | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
| Depósito de dos de cada tipo | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
| Depósito de valor desigual | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
| Después del arbitraje | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
Use la función UniswapV2ERC20._mint para crear realmente los tokens de liquidez adicionales y dárselos a la cuenta correcta.
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 y reserve1 están actualizados
emit Mint(msg.sender, amount0, amount1);
}
Actualice las variables de estado (reserve0, reserve1 y, si es necesario, kLast) y emita el evento apropiado.
burn
// esta función de bajo nivel debe ser llamada desde un contrato que realice comprobaciones de seguridad importantes
function burn(address to) external lock returns (uint amount0, uint amount1) {
Esta función se llama cuando se retira liquidez y los tokens de liquidez apropiados necesitan ser quemados. También debería ser llamada desde una cuenta periférica.
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // ahorro de gas
address _token0 = token0; // ahorro de gas
address _token1 = token1; // ahorro de gas
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
El contrato periférico transfirió la liquidez a quemar a este contrato antes de la llamada. De esa manera sabemos cuánta liquidez quemar, y podemos asegurarnos de que se queme.
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // ahorro de gas, debe definirse aquí ya que totalSupply puede actualizarse en _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // el uso de saldos garantiza una distribución prorrateada
amount1 = liquidity.mul(balance1) / _totalSupply; // el uso de saldos garantiza una distribución prorrateada
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
El proveedor de liquidez recibe el mismo valor de ambos tokens. De esta manera no cambiamos el tipo de cambio.
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 y reserve1 están actualizados
emit Burn(msg.sender, amount0, amount1, to);
}
El resto de la función burn es la imagen especular de la función mint anterior.
swap
// esta función de bajo nivel debe ser llamada desde un contrato que realice comprobaciones de seguridad importantes
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
También se supone que esta función se llama desde un contrato periférico.
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // ahorro de gas
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // ámbito para _token{0,1}, evita errores de pila demasiado profunda
Las variables locales se pueden almacenar en la memoria o, si no hay demasiadas, directamente en la pila. Si podemos limitar el número para usar la pila, usamos menos gas. Para obtener más detalles, consulte el libro amarillo, las especificaciones formales de Ethereum (opens in a new tab), p. 26, ecuación 298.
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // transferir tokens de forma optimista
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // transferir tokens de forma optimista
Esta transferencia es optimista, porque transferimos antes de estar seguros de que se cumplen todas las condiciones. Esto está bien en Ethereum porque si las condiciones no se cumplen más adelante en la llamada, revertimos y deshacemos cualquier cambio que haya creado.
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
Informe al receptor sobre el intercambio si se solicita.
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
Obtenga los saldos actuales. El contrato periférico nos envía los tokens antes de llamarnos para el intercambio. Esto facilita que el contrato verifique que no está siendo engañado, una verificación que tiene que ocurrir en el contrato principal (porque podemos ser llamados por otras entidades además de nuestro contrato periférico).
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // ámbito para reserve{0,1}Adjusted, evita errores de pila demasiado profunda
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
Esta es una verificación de cordura para asegurarnos de que no perdemos con el intercambio. No hay ninguna circunstancia en la que un intercambio deba reducir reserve0*reserve1. Aquí también es donde nos aseguramos de que se envíe una tarifa del 0,3 % en el intercambio; antes de verificar la cordura del valor de K, multiplicamos ambos saldos por 1000 restados por las cantidades multiplicadas por 3, esto significa que el 0,3 % (3/1000 = 0,003 = 0,3 %) se deduce del saldo antes de comparar su valor K con el valor K de las reservas actuales.
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
Actualice reserve0 y reserve1 y, si es necesario, los acumuladores de precios y la marca de tiempo y emita un evento.
Sincronización o Skim
Es posible que los saldos reales se desincronicen con las reservas que el intercambio de pares cree que tiene.
No hay forma de retirar tokens sin el consentimiento del contrato, pero los depósitos son un asunto diferente. Una cuenta puede transferir tokens al intercambio sin llamar a mint ni a swap.
En ese caso hay dos soluciones:
sync, actualizar las reservas a los saldos actualesskim, retirar la cantidad adicional. Tenga en cuenta que cualquier cuenta puede llamar askimporque no sabemos quién depositó los tokens. Esta información se emite en un evento, pero los eventos no son accesibles desde la cadena de bloques.
// forzar que los saldos coincidan con las reservas
function skim(address to) external lock {
address _token0 = token0; // ahorro de gas
address _token1 = token1; // ahorro de gas
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
// forzar que las reservas coincidan con los saldos
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
}
UniswapV2Factory.sol
Este contrato (opens in a new tab) crea los intercambios de pares.
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
contract UniswapV2Factory is IUniswapV2Factory {
address public feeTo;
address public feeToSetter;
Estas variables de estado son necesarias para implementar la tarifa del protocolo (consulte el documento técnico (opens in a new tab), p. 5).
La dirección feeTo acumula los tokens de liquidez para la tarifa del protocolo, y feeToSetter es la dirección permitida para cambiar feeTo a una dirección diferente.
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
Estas variables mantienen un registro de los pares, los intercambios entre dos tipos de tokens.
La primera, getPair, es un mapeo que identifica un contrato de intercambio de pares basado en los dos tokens ERC-20 que intercambia. Los tokens ERC-20 se identifican por las direcciones de los contratos que los implementan, por lo que las claves y el valor son todas direcciones. Para obtener la dirección del intercambio de pares que le permite convertir de tokenA a tokenB, usa getPair[<tokenA address>][<tokenB address>] (o al revés).
La segunda variable, allPairs, es una matriz que incluye todas las direcciones de los intercambios de pares creados por esta fábrica. En Ethereum no se puede iterar sobre el contenido de un mapeo, ni obtener una lista de todas las claves, por lo que esta variable es la única forma de saber qué intercambios gestiona esta fábrica.
Nota: La razón por la que no se puede iterar sobre todas las claves de un mapeo es que el almacenamiento de datos del contrato es costoso, por lo que cuanto menos lo usemos, mejor, y cuanto menos a menudo lo cambiemos mejor. Puede crear mapeos que admitan la iteración (opens in a new tab), pero requieren almacenamiento adicional para una lista de claves. En la mayoría de las aplicaciones no necesita eso.
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
Este evento se emite cuando se crea un nuevo intercambio de pares. Incluye las direcciones de los tokens, la dirección del intercambio de pares y el número total de intercambios gestionados por la fábrica.
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
Lo único que hace el constructor es especificar el feeToSetter. Las fábricas comienzan sin una tarifa, y solo feeSetter puede cambiar eso.
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
Esta función devuelve el número de pares de intercambio.
function createPair(address tokenA, address tokenB) external returns (address pair) {
Esta es la función principal de la fábrica, para crear un intercambio de pares entre dos tokens ERC-20. Tenga en cuenta que cualquiera puede llamar a esta función. No necesita permiso de Uniswap para crear un nuevo intercambio de pares.
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
Queremos que la dirección del nuevo intercambio sea determinista, para que pueda calcularse por adelantado fuera de la cadena (esto puede ser útil para transacciones de capa 2). Para hacer esto, necesitamos tener un orden consistente de las direcciones de los tokens, independientemente del orden en que las hayamos recibido, por lo que las ordenamos aquí.
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // una única comprobación es suficiente
Los fondos de liquidez grandes son mejores que los pequeños, porque tienen precios más estables. No queremos tener más de un solo fondo de liquidez por par de tokens. Si ya hay un intercambio, no hay necesidad de crear otro para el mismo par.
bytes memory bytecode = type(UniswapV2Pair).creationCode;
Para crear un nuevo contrato necesitamos el código que lo crea (tanto la función del constructor como el código que escribe en la memoria el código de bytes de la EVM del contrato real). Normalmente en Solidity simplemente usamos addr = new <name of contract>(<constructor parameters>) y el compilador se encarga de todo por nosotros, pero para tener una dirección de contrato determinista necesitamos usar el código de operación CREATE2 (opens in a new tab).
Cuando se escribió este código, ese código de operación aún no era compatible con Solidity, por lo que fue necesario obtener el código manualmente. Esto ya no es un problema, porque Solidity ahora admite CREATE2 (opens in a new tab).
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
Cuando un código de operación aún no es compatible con Solidity, podemos llamarlo usando ensamblador en línea (opens in a new tab).
IUniswapV2Pair(pair).initialize(token0, token1);
Llame a la función initialize para decirle al nuevo intercambio qué dos tokens intercambia.
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // poblar el mapeo en la dirección inversa
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
Guarde la información del nuevo par en las variables de estado y emita un evento para informar al mundo del nuevo intercambio de pares.
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}
Estas dos funciones permiten a feeSetter controlar el destinatario de la tarifa (si lo hay), y cambiar feeSetter a una nueva dirección.
UniswapV2ERC20.sol
Este contrato (opens in a new tab) implementa el token de liquidez ERC-20. Es similar al contrato ERC-20 de OpenZeppelin, por lo que solo explicaré la parte que es diferente, la funcionalidad permit.
Las transacciones en Ethereum cuestan ether (ETH), que es equivalente a dinero real. Si tiene tokens ERC-20 pero no ETH, no puede enviar transacciones, por lo que no puede hacer nada con ellos. Una solución para evitar este problema son las metatransacciones (opens in a new tab). El propietario de los tokens firma una transacción que permite a otra persona retirar tokens fuera de la cadena y la envía a través de Internet al destinatario. El destinatario, que sí tiene ETH, luego envía el permiso en nombre del propietario.
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
Este hash es el identificador para el tipo de transacción (opens in a new tab). El único que admitimos aquí es Permit con estos parámetros.
mapping(address => uint) public nonces;
No es factible que un destinatario falsifique una firma digital. Sin embargo, es trivial enviar la misma transacción dos veces (esta es una forma de ataque de repetición (opens in a new tab)). Para evitar esto, usamos un nonce (opens in a new tab). Si el nonce de un nuevo Permit no es uno más que el último utilizado, asumimos que es inválido.
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
Este es el código para recuperar el identificador de la cadena (opens in a new tab). Utiliza un dialecto de ensamblador de la EVM llamado Yul (opens in a new tab). Tenga en cuenta que en la versión actual de Yul tiene que usar chainid(), no chainid.
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}
Calcule el separador de dominio (opens in a new tab) para EIP-712.
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
Esta es la función que implementa los permisos. Recibe como parámetros los campos relevantes y los tres valores escalares para la firma (opens in a new tab) (v, r y s).
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
No acepte transacciones después de la fecha límite.
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
abi.encodePacked(...) es el mensaje que esperamos recibir. Sabemos cuál debería ser el nonce, por lo que no hay necesidad de que lo obtengamos como parámetro.
El algoritmo de firma de Ethereum espera obtener 256 bits para firmar, por lo que usamos la función hash keccak256.
address recoveredAddress = ecrecover(digest, v, r, s);
A partir del resumen y la firma podemos obtener la dirección que lo firmó usando ecrecover (opens in a new tab).
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
Si todo está bien, trate esto como un aprobar ERC-20 (opens in a new tab).
Los contratos periféricos
Los contratos periféricos son la API (interfaz de programación de aplicaciones) de Uniswap. Están disponibles para llamadas externas, ya sea desde otros contratos o aplicaciones descentralizadas. Podría llamar a los contratos principales directamente, pero eso es más complicado y podría perder valor si comete un error. Los contratos principales solo contienen pruebas para asegurarse de que no sean engañados, no comprobaciones de cordura para nadie más. Esas están en la periferia para que puedan actualizarse según sea necesario.
UniswapV2Router01.sol
Este contrato (opens in a new tab) tiene problemas y ya no debería usarse (opens in a new tab). Afortunadamente, los contratos periféricos no tienen estado y no contienen ningún activo, por lo que es fácil desaprobarlo y sugerir a las personas que usen el reemplazo, UniswapV2Router02, en su lugar.
UniswapV2Router02.sol
En la mayoría de los casos, usaría Uniswap a través de este contrato (opens in a new tab). Puede ver cómo usarlo aquí (opens in a new tab).
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';
La mayoría de estos ya los hemos encontrado antes, o son bastante obvios. La única excepción es IWETH.sol. Uniswap v2 permite intercambios para cualquier par de tokens ERC-20, pero el ether (ETH) en sí no es un token ERC-20. Es anterior al estándar y se transfiere mediante mecanismos únicos. Para permitir el uso de ETH en contratos que se aplican a tokens ERC-20, se ideó el contrato de ether envuelto (WETH) (opens in a new tab). Usted envía ETH a este contrato y le acuña una cantidad equivalente de WETH. O puede quemar WETH y recuperar ETH.
contract UniswapV2Router02 is IUniswapV2Router02 {
using SafeMath for uint;
address public immutable override factory;
address public immutable override WETH;
El enrutador necesita saber qué fábrica usar y, para las transacciones que requieren WETH, qué contrato WETH usar. Estos valores son inmutables (opens in a new tab), lo que significa que solo se pueden establecer en el constructor. Esto da a los usuarios la confianza de que nadie podrá cambiarlos para que apunten a contratos menos honestos.
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
Este modificador se asegura de que las transacciones con límite de tiempo ("hacer X antes del tiempo Y si es posible") no ocurran después de su límite de tiempo.
constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}
El constructor simplemente establece las variables de estado inmutables.
receive() external payable {
assert(msg.sender == WETH); // solo aceptar ETH a través de fallback desde el contrato WETH
}
Esta función se llama cuando canjeamos tokens del contrato WETH de vuelta a ETH. Solo el contrato WETH que usamos está autorizado para hacer eso.
Añadir liquidez
Estas funciones añaden tokens al intercambio de pares, lo que aumenta el fondo de liquidez.
// **** AÑADIR LIQUIDEZ ****
function _addLiquidity(
Esta función se utiliza para calcular la cantidad de tokens A y B que deben depositarse en el intercambio de pares.
address tokenA,
address tokenB,
Estas son las direcciones de los contratos de tokens ERC-20.
uint amountADesired,
uint amountBDesired,
Estas son las cantidades que el proveedor de liquidez quiere depositar. También son las cantidades máximas de A y B que se depositarán.
uint amountAMin,
uint amountBMin
Estas son las cantidades mínimas aceptables para depositar. Si la transacción no puede llevarse a cabo con estas cantidades o más, se debe revertir. Si no desea esta función, simplemente especifique cero.
Los proveedores de liquidez especifican un mínimo, por lo general, porque quieren limitar la transacción a un tipo de cambio que esté cerca del actual. Si el tipo de cambio fluctúa demasiado, podría significar noticias que cambian los valores subyacentes, y quieren decidir manualmente qué hacer.
Por ejemplo, imagine un caso en el que el tipo de cambio es uno a uno y el proveedor de liquidez especifica estos valores:
| Parámetro | Valor |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
Mientras el tipo de cambio se mantenga entre 0,9 y 1,25, la transacción se lleva a cabo. Si el tipo de cambio sale de ese rango, la transacción se cancela.
El motivo de esta precaución es que las transacciones no son inmediatas, usted las envía y, finalmente, un validador las incluirá en un bloque (a menos que su precio del gas sea muy bajo, en cuyo caso deberá enviar otra transacción con el mismo nonce y un precio del gas más alto para sobrescribirla). No puede controlar lo que sucede durante el intervalo entre el envío y la inclusión.
) internal virtual returns (uint amountA, uint amountB) {
La función devuelve las cantidades que el proveedor de liquidez debe depositar para tener una proporción igual a la proporción actual entre las reservas.
// crear el par si aún no existe
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
Si aún no hay un intercambio para este par de tokens, créelo.
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
Obtenga las reservas actuales en el par.
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
Si las reservas actuales están vacías, entonces este es un nuevo intercambio de pares. Las cantidades a depositar deben ser exactamente las mismas que las que el proveedor de liquidez quiere proporcionar.
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
Si necesitamos ver cuáles serán las cantidades, obtenemos la cantidad óptima usando esta función (opens in a new tab). Queremos la misma proporción que las reservas actuales.
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
Si amountBOptimal es menor que la cantidad que el proveedor de liquidez quiere depositar, significa que el token B es más valioso actualmente de lo que piensa el depositante de liquidez, por lo que se requiere una cantidad menor.
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
Si la cantidad óptima de B es mayor que la cantidad deseada de B, significa que los tokens B son menos valiosos actualmente de lo que piensa el depositante de liquidez, por lo que se requiere una cantidad mayor. Sin embargo, la cantidad deseada es un máximo, por lo que no podemos hacer eso. En su lugar, calculamos el número óptimo de tokens A para la cantidad deseada de tokens B.
Juntando todo esto obtenemos este gráfico. Suponga que está intentando depositar mil tokens A (línea azul) y mil tokens B (línea roja). El eje x es el tipo de cambio, A/B. Si x=1, tienen el mismo valor y deposita mil de cada uno. Si x=2, A tiene el doble del valor de B (obtiene dos tokens B por cada token A), por lo que deposita mil tokens B, pero solo 500 tokens A. Si x=0,5, la situación se invierte, mil tokens A y quinientos tokens B.
Podría depositar liquidez directamente en el contrato principal (usando UniswapV2Pair::mint (opens in a new tab)), pero el contrato principal solo verifica que no esté siendo engañado, por lo que corre el riesgo de perder valor si el tipo de cambio cambia entre el momento en que envía su transacción y el momento en que se ejecuta. Si usa el contrato periférico, calcula la cantidad que debe depositar y la deposita de inmediato, por lo que el tipo de cambio no cambia y no pierde nada.
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
Esta función puede ser llamada por una transacción para depositar liquidez. La mayoría de los parámetros son los mismos que en _addLiquidity arriba, con dos excepciones:
. to es la dirección que obtiene los nuevos tokens de liquidez acuñados para mostrar la porción del fondo de liquidez del proveedor de liquidez
. deadline es un límite de tiempo en la transacción
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
Calculamos las cantidades a depositar realmente y luego encontramos la dirección del fondo de liquidez. Para ahorrar gas, no hacemos esto preguntando a la fábrica, sino usando la función de biblioteca pairFor (ver más abajo en bibliotecas)
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
Transfiera las cantidades correctas de tokens del usuario al intercambio de pares.
liquidity = IUniswapV2Pair(pair).mint(to);
}
A cambio, dé a la dirección to tokens de liquidez por la propiedad parcial del fondo. La función mint del contrato principal ve cuántos tokens adicionales tiene (en comparación con lo que tenía la última vez que cambió la liquidez) y acuña liquidez en consecuencia.
function addLiquidityETH(
address token,
uint amountTokenDesired,
Cuando un proveedor de liquidez quiere proporcionar liquidez a un intercambio de pares Token/ETH, hay algunas diferencias. El contrato se encarga de envolver el ETH para el proveedor de liquidez. No hay necesidad de especificar cuántos ETH quiere depositar el usuario, porque el usuario simplemente los envía con la transacción (la cantidad está disponible en msg.value).
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
Para depositar el ETH, el contrato primero lo envuelve en WETH y luego transfiere el WETH al par. Tenga en cuenta que la transferencia está envuelta en un assert. Esto significa que si la transferencia falla, esta llamada de contrato también falla y, por lo tanto, la envoltura no ocurre realmente.
liquidity = IUniswapV2Pair(pair).mint(to);
// reembolsar el polvo de eth, si lo hay
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
El usuario ya nos ha enviado el ETH, por lo que si sobra algo (porque el otro token es menos valioso de lo que pensaba el usuario), debemos emitir un reembolso.
Retirar liquidez
Estas funciones retirarán liquidez y pagarán al proveedor de liquidez.
// **** RETIRAR LIQUIDEZ ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
El caso más simple de retirar liquidez. Hay una cantidad mínima de cada token que el proveedor de liquidez acepta recibir, y debe ocurrir antes de la fecha límite.
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // enviar liquidez al par
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
La función burn del contrato principal se encarga de devolver los tokens al usuario.
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
Cuando una función devuelve múltiples valores, pero solo estamos interesados en algunos de ellos, así es como obtenemos solo esos valores. Es algo más barato en términos de gas que leer un valor y nunca usarlo.
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
Traduzca las cantidades de la forma en que las devuelve el contrato principal (el token de dirección más baja primero) a la forma en que el usuario las espera (correspondiente a tokenA y tokenB).
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
Está bien hacer la transferencia primero y luego verificar que sea legítima, porque si no lo es, revertiremos todos los cambios de estado.
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, amountToken);
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
Retirar liquidez para ETH es casi lo mismo, excepto que recibimos los tokens WETH y luego los canjeamos por ETH para devolverlos al proveedor de liquidez.
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
function removeLiquidityETHWithPermit(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountToken, uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
}
Estas funciones retransmiten metatransacciones para permitir a los usuarios sin ether retirar del fondo, usando el mecanismo de permiso.
// **** RETIRAR LIQUIDEZ (compatible con tokens con tarifa por transferencia) ****
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
(, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
Esta función se puede usar para tokens que tienen tarifas de transferencia o almacenamiento. Cuando un token tiene tales tarifas, no podemos confiar en la función removeLiquidity para que nos diga cuánto del token recuperamos, por lo que debemos retirar primero y luego obtener el saldo.
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
token, liquidity, amountTokenMin, amountETHMin, to, deadline
);
}
La función final combina tarifas de almacenamiento con metatransacciones.
Intercambio
// **** INTERCAMBIO ****
// requiere que la cantidad inicial ya haya sido enviada al primer par
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
Esta función realiza el procesamiento interno que se requiere para las funciones que están expuestas a los comerciantes.
for (uint i; i < path.length - 1; i++) {
Mientras escribo esto, hay 388.160 tokens ERC-20 (opens in a new tab). Si hubiera un intercambio de pares para cada par de tokens, habría más de 150 mil millones de intercambios de pares. Toda la cadena, en este momento, solo tiene el 0,1 % de ese número de cuentas (opens in a new tab). En su lugar, las funciones de intercambio admiten el concepto de una ruta. Un comerciante puede intercambiar A por B, B por C y C por D, por lo que no hay necesidad de un intercambio directo de pares A-D.
Los precios en estos mercados tienden a estar sincronizados, porque cuando no lo están, se crea una oportunidad para el arbitraje. Imagine, por ejemplo, tres tokens, A, B y C. Hay tres intercambios de pares, uno para cada par.
- La situación inicial
- Un comerciante vende 24,695 tokens A y obtiene 25,305 tokens B.
- El comerciante vende 24,695 tokens B por 25,305 tokens C, manteniendo aproximadamente 0,61 tokens B como ganancia.
- Luego, el comerciante vende 24,695 tokens C por 25,305 tokens A, manteniendo aproximadamente 0,61 tokens C como ganancia. El comerciante también tiene 0,61 tokens A adicionales (los 25,305 con los que termina el comerciante, menos la inversión original de 24,695).
| Paso | Intercambio A-B | Intercambio B-C | Intercambio A-C |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1,05 | B:1000 C:1050 B/C=1,05 | A:1050 C:1000 C/A=1,05 |
| 2 | A:1024,695 B:1024,695 A/B=1 | B:1000 C:1050 B/C=1,05 | A:1050 C:1000 C/A=1,05 |
| 3 | A:1024,695 B:1024,695 A/B=1 | B:1024,695 C:1024,695 B/C=1 | A:1050 C:1000 C/A=1,05 |
| 4 | A:1024,695 B:1024,695 A/B=1 | B:1024,695 C:1024,695 B/C=1 | A:1024,695 C:1024,695 C/A=1 |
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
Obtenga el par que estamos manejando actualmente, ordénelo (para usarlo con el par) y obtenga la cantidad de salida esperada.
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
Obtenga las cantidades de salida esperadas, ordenadas de la forma en que el intercambio de pares espera que estén.
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
¿Es este el último intercambio? Si es así, envíe los tokens recibidos por la operación al destino. Si no, envíelo al siguiente intercambio de pares.
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
Llame realmente al intercambio de pares para intercambiar los tokens. No necesitamos una devolución de llamada para que se nos informe sobre el intercambio, por lo que no enviamos ningún byte en ese campo.
function swapExactTokensForTokens(
Esta función es utilizada directamente por los comerciantes para intercambiar un token por otro.
uint amountIn,
uint amountOutMin,
address[] calldata path,
Este parámetro contiene las direcciones de los contratos ERC-20. Como se explicó anteriormente, esta es una matriz porque es posible que deba pasar por varios intercambios de pares para pasar del activo que tiene al activo que desea.
Un parámetro de función en Solidity se puede almacenar ya sea en memory o en calldata. Si la función es un punto de entrada al contrato, llamada directamente por un usuario (usando una transacción) o desde un contrato diferente, entonces el valor del parámetro se puede tomar directamente de los datos de llamada. Si la función se llama internamente, como _swap arriba, entonces los parámetros deben almacenarse en memory. Desde la perspectiva del contrato llamado, calldata es de solo lectura.
Con tipos escalares como uint o address, el compilador maneja la elección del almacenamiento por nosotros, pero con las matrices, que son más largas y costosas, especificamos el tipo de almacenamiento que se utilizará.
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
Los valores de retorno siempre se devuelven en la memoria.
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
Calcule la cantidad que se comprará en cada intercambio. Si el resultado es menor que el mínimo que el comerciante está dispuesto a aceptar, revierta la transacción.
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
Finalmente, transfiera el token ERC-20 inicial a la cuenta para el primer intercambio de pares y llame a _swap. Todo esto sucede en la misma transacción, por lo que el intercambio de pares sabe que cualquier token inesperado es parte de esta transferencia.
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
La función anterior, swapTokensForTokens, permite a un comerciante especificar un número exacto de tokens de entrada que está dispuesto a dar y el número mínimo de tokens de salida que está dispuesto a recibir a cambio. Esta función hace el intercambio inverso, permite a un comerciante especificar el número de tokens de salida que desea y el número máximo de tokens de entrada que está dispuesto a pagar por ellos.
En ambos casos, el comerciante primero debe otorgar a este contrato periférico una asignación para permitirle transferirlos.
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
}
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
// reembolsar el polvo de eth, si lo hay
if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}
Estas cuatro variantes implican operar entre ETH y tokens. La única diferencia es que o recibimos ETH del comerciante y lo usamos para acuñar WETH, o recibimos WETH del último intercambio en la ruta y lo quemamos, devolviendo al comerciante el ETH resultante.
// **** INTERCAMBIO (compatible con tokens con tarifa por transferencia) ****
// requiere que la cantidad inicial ya haya sido enviada al primer par
function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
Esta es la función interna para intercambiar tokens que tienen tarifas de transferencia o almacenamiento para resolver (este problema (opens in a new tab)).
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
uint amountInput;
uint amountOutput;
{ // ámbito para evitar errores de pila demasiado profunda
(uint reserve0, uint reserve1,) = pair.getReserves();
(uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
Debido a las tarifas de transferencia, no podemos confiar en la función getAmountsOut para que nos diga cuánto obtenemos de cada transferencia (de la forma en que lo hacemos antes de llamar a la función original _swap). En su lugar, tenemos que transferir primero y luego ver cuántos tokens recuperamos.
Nota: En teoría, podríamos usar esta función en lugar de _swap, pero en ciertos casos (por ejemplo, si la transferencia termina siendo revertida porque no hay suficiente al final para cumplir con el mínimo requerido) eso terminaría costando más gas. Los tokens con tarifa de transferencia son bastante raros, por lo que, si bien debemos adaptarnos a ellos, no hay necesidad de que todos los intercambios asuman que pasan por al menos uno de ellos.
}
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
pair.swap(amount0Out, amount1Out, to, new bytes(0));
}
}
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) {
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
payable
ensure(deadline)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
uint amountIn = msg.value;
IWETH(WETH).deposit{value: amountIn}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
ensure(deadline)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
_swapSupportingFeeOnTransferTokens(path, address(this));
uint amountOut = IERC20(WETH).balanceOf(address(this));
require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).withdraw(amountOut);
TransferHelper.safeTransferETH(to, amountOut);
}
Estas son las mismas variantes utilizadas para los tokens normales, pero llaman a _swapSupportingFeeOnTransferTokens en su lugar.
// **** FUNCIONES DE BIBLIOTECA ****
function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
return UniswapV2Library.quote(amountA, reserveA, reserveB);
}
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountOut)
{
return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
}
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountIn)
{
return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
}
function getAmountsOut(uint amountIn, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsOut(factory, amountIn, path);
}
function getAmountsIn(uint amountOut, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsIn(factory, amountOut, path);
}
}
Estas funciones son solo proxies que llaman a las funciones de UniswapV2Library.
UniswapV2Migrator.sol
Este contrato se utilizó para migrar intercambios de la antigua v1 a la v2. Ahora que han sido migrados, ya no es relevante.
Las bibliotecas
La biblioteca SafeMath (opens in a new tab) está bien documentada, por lo que no hay necesidad de documentarla aquí.
Math
Esta biblioteca contiene algunas funciones matemáticas que normalmente no se necesitan en el código de Solidity, por lo que no forman parte del lenguaje.
pragma solidity =0.5.16;
// una biblioteca para realizar varias operaciones matemáticas
library Math {
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// método babilónico (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
Comience con x como una estimación que es mayor que la raíz cuadrada (esa es la razón por la que necesitamos tratar 1-3 como casos especiales).
while (x < z) {
z = x;
x = (y / x + x) / 2;
Obtenga una estimación más cercana, el promedio de la estimación anterior y el número cuya raíz cuadrada estamos tratando de encontrar dividido por la estimación anterior. Repita hasta que la nueva estimación no sea menor que la existente. Para obtener más detalles, consulte aquí (opens in a new tab).
}
} else if (y != 0) {
z = 1;
Nunca deberíamos necesitar la raíz cuadrada de cero. Las raíces cuadradas de uno, dos y tres son aproximadamente uno (usamos números enteros, por lo que ignoramos la fracción).
}
}
}
Fracciones de punto fijo (UQ112x112)
Esta biblioteca maneja fracciones, que normalmente no forman parte de la aritmética de Ethereum. Lo hace codificando el número x como x*2^112. Esto nos permite usar los códigos de operación originales de suma y resta sin ningún cambio.
pragma solidity =0.5.16;
// una biblioteca para manejar números binarios de punto fijo (https://wikipedia.org/wiki/Q_(number_format))
// rango: [0, 2**112 - 1]
// resolución: 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
Q112 es la codificación para uno.
// codificar un uint112 como un UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // nunca se desborda
}
Debido a que y es uint112, lo máximo que puede ser es 2^112-1. Ese número todavía se puede codificar como un UQ112x112.
// divide un UQ112x112 por un uint112, devolviendo un UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
Si dividimos dos valores UQ112x112, el resultado ya no se multiplica por 2^112. Así que en su lugar tomamos un número entero para el denominador. Habríamos necesitado usar un truco similar para hacer la multiplicación, pero no necesitamos hacer la multiplicación de valores UQ112x112.
UniswapV2Library
Esta biblioteca es utilizada solo por los contratos de la periferia
pragma solidity >=0.5.0;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import "./SafeMath.sol";
library UniswapV2Library {
using SafeMath for uint;
// devuelve direcciones de token ordenadas, utilizado para manejar valores de retorno de pares ordenados en este orden
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}
Ordena los dos tokens por dirección, para que podamos obtener la dirección del intercambio del par para ellos. Esto es necesario porque de lo contrario tendríamos dos posibilidades, una para los parámetros A,B y otra para los parámetros B,A, lo que llevaría a dos intercambios en lugar de uno.
// calcula la dirección CREATE2 para un par sin hacer ninguna llamada externa
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // hash del código de inicialización
))));
}
Esta función calcula la dirección del intercambio del par para los dos tokens. Este contrato se crea utilizando el código de operación CREATE2 (opens in a new tab), por lo que podemos calcular la dirección utilizando el mismo algoritmo si conocemos los parámetros que utiliza. Esto es mucho más barato que preguntarle a la fábrica, y
// obtiene y ordena las reservas para un par
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
Esta función devuelve las reservas de los dos tokens que tiene el intercambio del par. Tenga en cuenta que puede recibir los tokens en cualquier orden y los ordena para uso interno.
// dada una cantidad de un activo y las reservas del par, devuelve una cantidad equivalente del otro activo
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
Esta función le da la cantidad del token B que obtendrá a cambio del token A si no hay ninguna tarifa involucrada. Este cálculo tiene en cuenta que la transferencia cambia el tipo de cambio.
// dada una cantidad de entrada de un activo y las reservas del par, devuelve la cantidad máxima de salida del otro activo
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
La función quote anterior funciona muy bien si no hay tarifa para usar el intercambio del par. Sin embargo, si hay una tarifa de intercambio del 0,3 %, la cantidad que realmente obtiene es menor. Esta función calcula la cantidad después de la tarifa de intercambio.
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
Solidity no maneja fracciones de forma nativa, por lo que no podemos simplemente multiplicar la cantidad por 0,997. En su lugar, multiplicamos el numerador por 997 y el denominador por 1000, logrando el mismo efecto.
// dada una cantidad de salida de un activo y las reservas del par, devuelve una cantidad de entrada requerida del otro activo
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}
Esta función hace aproximadamente lo mismo, pero obtiene la cantidad de salida y proporciona la entrada.
// realiza cálculos encadenados de getAmountOut en cualquier número de pares
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
// realiza cálculos encadenados de getAmountIn en cualquier número de pares
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
}
Estas dos funciones se encargan de identificar los valores cuando es necesario pasar por varios intercambios de pares.
Transfer Helper
Esta biblioteca (opens in a new tab) agrega comprobaciones de éxito en torno a las transferencias de ERC-20 y Ethereum para tratar una reversión y un retorno de valor false de la misma manera.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.6.0;
// métodos auxiliares para interactuar con tokens ERC-20 y enviar ETH que no devuelven consistentemente true/false
library TransferHelper {
function safeApprove(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('approve(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
Podemos llamar a un contrato diferente de dos maneras:
- Usar una definición de interfaz para crear una llamada a una función
- Usar la interfaz binaria de aplicación (ABI) (opens in a new tab) "manualmente" para crear la llamada. Esto es lo que el autor del código decidió hacer.
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeApprove: approve failed'
);
}
En aras de la compatibilidad con versiones anteriores con los tokens que se crearon antes del estándar ERC-20, una llamada ERC-20 puede fallar ya sea revirtiendo (en cuyo caso success es false) o al tener éxito y devolver un valor false (en cuyo caso hay datos de salida, y si los decodifica como un booleano, obtiene false).
function safeTransfer(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeTransfer: transfer failed'
);
}
Esta función implementa la funcionalidad transfer de ERC-20 (opens in a new tab), que permite a una cuenta gastar la asignación proporcionada por una cuenta diferente.
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}
Esta función implementa la funcionalidad transferFrom de ERC-20 (opens in a new tab), que permite a una cuenta gastar la asignación proporcionada por una cuenta diferente.
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
}
}
Esta función transfiere ether a una cuenta. Cualquier llamada a un contrato diferente puede intentar enviar ether. Debido a que en realidad no necesitamos llamar a ninguna función, no enviamos ningún dato con la llamada.
Conclusión
Este es un artículo largo de unas 50 páginas. Si llegaste hasta aquí, ¡felicidades! Esperamos que a estas alturas hayas entendido las consideraciones al escribir una aplicación de la vida real (a diferencia de los programas de muestra cortos) y estés mejor preparado para escribir contratos para tus propios casos de uso.
Ahora ve y escribe algo útil y sorpréndenos.
