Ir al contenido principal

Un recorrido por el contrato Uniswap-v2

solidity
Intermedio
Ori Pomerantz
1 de mayo de 2021
62 minuto leído minute read

Introducción

Uniswap V2(opens in a new tab) puede crear un mercado de intercambio entre dos tókenes ERC-20 cualquiera. En este artículo vamos a revisar el código fuente de los contratos que implementan este protocolo y ver por qué están escritos de esta manera.

¿Qué hace Uniswap?

Básicamente hay dos tipos de usuario: los proveedores de liquidez y los que compran y venden.

Los proveedores de liquidez proporcionan la reserva con los dos tókenes que se pueden intercambiar (los llamaremos Token0 y Token1). A cambio, estos reciben un tercer token que representa la propiedad parcial de la reserva, llamado token de liquidez.

Los traders envían un tipo de token al grupo y reciben el otro (por ejemplo, envían un Token0 y reciben un Token1) del grupo proporcionado por los proveedores de liquidez. El tipo de cambio viene determinado por el número relativo del Token0 y del Token1 que tiene el grupo. Además, la reserva toma un pequeño porcentaje como recompensa para la reserva de liquidez.

Cuando los proveedores de liquidez quieren recuperar sus activos, pueden quemar los tókenes de la reserva y así recuperar sus tókenes, incluyendo su parte de la recompensa.

Haga clic aquí para ver una descripción completa(opens in a new tab).

¿Por qué V2? ¿Por qué no V3?

Uniswap V3(opens in a new tab) es una actualización mucho más complicada que la V2. Es más fácil aprender la V2 primero y luego pasar a la V3.

Contratos Principales vs Contratos Periféricos

Uniswap v2 se divide en dos componentes: uno principal y otro periférico. Esta división permite que los contratos principales —que guardan los activos— y, por lo tanto tienen que ser seguros, simples y fáciles de auditar. Toda la funcionalidad adicional que necesitan los agentes pueden proporcionarla los contratos periféricos.

Flujos de control y de datos

Este es el flujo de datos y de control que se produce cuando se realizan las tres acciones principales de Uniswap:

  1. Intercambio entre diferentes tókenes
  2. Aporte liquidez al mercado, y será recompensado con el intercambio de un par de tókenes de liquidez ERC-20
  3. Queme los tókenes de liquidez ERC-20, y recupere los tókenes ERC-20 de intercambio de par permite a los agentes que intercambien.

Intercambiar

Este es el flujo más común, utilizado por los agentes:

Solicitante

  1. Proporciona a la cuenta periférica una asignación en el importe por canjear.
  2. Activar una de las muchas funciones de intercambio de los contratos periféricos (que depende de si hay ETH involucrados o no, si el trader especifica la cantidad de tókenes por depositar, o la cantidad de tókenes por recuperar, etc.). Cada función de intercambio acepta una ruto, una matriz de intercambios por la que pasar.

En el contrato periférico (UniswapV2Router02.sol)

  1. Identifique, durante el proceso, las cantidades que se necesitan negociar en cada intercambio.
  2. Se repite por el camino. Por cada intercambio producido durante el proceso, se envia el token de entrada, y luego se activa la función de intercambio. En la mayoría de los casos, la dirección de destino para los tókenes es el siguiente par en la ruta. En el intercambio final, es la dirección proporcionada por el agente.

En el contrato principal (UniswapV2Pair.sol)

  1. Verifique que no se está estafando el contrato principal y que puede mantener la suficiente liquidez despúes del intercambio.
  2. Compruebe cuántos tókenes adicionales tiene, además de las reservas conocidas. Esa cantidad, es el numero de tókenes de entrada recibidos para intercambiar.
  3. Envíe los tókenes de salida al destino.
  4. Active _update para actualizar la cantidad de reserva

Retomando los contratos periféricos (UniswapV2Router02.sol)

  1. Realice cualquier limpieza necesaria (por ejemplo, quemar tókenes WETH para recuperar ETH para enviar al comprador o vendedor).

Añadir liquidez

Llamar

  1. Proporcione a las cuentas periféricas una cantidad adicional para añadirla a la reserva de liquidez.
  2. Active una de las funciones addLiquidity del contrato periférico.

En el contrato periférico (UniswapV2Router02.sol)

  1. Cree un nuevo par de intercambio si es necesario.
  2. Si hay un par de intercambio existente, calcule la cantidad de tókenes que debe añadir. Se supone que este valor es idéntico para ambos tókenes, es decir, la misma relación entre tókenes nuevos y existentes.
  3. Compruebe si los importes son aceptables (los solicitantes pueden especificar un importe mínimo, por debajo del cual prefieren no añadir liquidez).
  4. Active el contrato principal.

En el contrato principal (UniswapV2Pair.sol)

  1. Acumule tókenes de liquidez y envíelos al solicitante.
  2. Llamar _update para actualizar la cantidad de reserva

Suprimir la liquidez

Llamador

  1. Proporcione a la cuenta periférica una cantidad de tókenes de liquidez para quemar a cambio de los tókenes subyacentes.
  2. Active una de las funciones removeLiquidity del contrato periférico.

En el contrato periférico (UniswapV2Router02.sol)

  1. Envíe los tókenes de liquidez al intercambio de par.

En el contrato principal (UniswapV2Pair.sol)

  1. Envíe a la dirección de destino los tókenes subyacentes proporcionales a los tókenes quemados. Por ejemplo, si hay 1.000 tókenes A en la reserva, 500 tókenes B y 90 tókenes de liquidez, y recibimos 9 tókenes para quemar, estamos quemando el 10 % de los tókenes de liquidez, por lo tanto, enviamos al usuario 100 tókenes A y 50 tókenes B.
  2. Queme los tókenes de liquidez.
  3. Llmar _update para actualizar el monto de reserva

Los contratos principales

Estos son los contratos seguros que mantienen la liquidez.

UniswapV2Pair.sol

Este contrato(opens in a new tab) implementa una reserva real que intercambia tókenes. Esta es la principal función de Uniswap.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Pair.sol';
4import './UniswapV2ERC20.sol';
5import './libraries/Math.sol';
6import './libraries/UQ112x112.sol';
7import './interfaces/IERC20.sol';
8import './interfaces/IUniswapV2Factory.sol';
9import './interfaces/IUniswapV2Callee.sol';
Mostrar todo
Copiar

Estas son todas las interfaces que el contrato necesita conocer, ya sea porque el contrato las implementa (IUniswapV2Pair and UniswapV2ERC20) o porque activa los contratos que las implementan.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
Copiar

Este contrado hereda de UniswapV2ERC20, que proporciona las funciones de ERC-20 para los tókenes de liquidez.

1 using SafeMath for uint;
Copiar

La biblioteca SafeMath(opens in a new tab) se usa para evitar excedentes y faltantes. Esto es importante, porque de otra manera podría darse una situación donde un valor debería ser -1, pero en cambio es 2^256-1.

1 using UQ112x112 for uint224;
Copiar

Muchos de los cálculos en el contrato de reserva 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 íntegra y 112 bits para las fracciones. Entonces 1.0 se representa como 2^112, 1,5 se representa como 2^112 + 2^111, etc.

Más detalles sobre esta biblioteca están disponibles en el siguiente documento.

Variables

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
Copiar

Para evitar casos de división entre cero, hay un número mínimo de tókenes de liquidez que siempre existe (aunque son propiedad de la cuenta cero). Ese número es LIQUIDEZ_MINIMA mil.

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
Copiar

Este es el seleccionador de ABI para la función de transferencia de ERC-20. Se usa para transferir tókenes ERC-20 en dos cuentas de token.

1 address public factory;
Copiar

Este es el contrato de fábrica que crea esta reserva. Cada reserva es un intercambio entre dos tókenes ERC-20, la fábrica es el punto central que conecta todas estas reservas.

1 address public token0;
2 address public token1;
Copiar

Existen direcciones de los contratos para los dos tipos de tókenes ERC-20 que esta reserva puede intercambiar.

1 uint112 private reserve0; // uses single storage slot, accessible via getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
Copiar

Los fondos que tiene la reserva para cada tipo de token. Asumimos que los dos representan el mismo monto de valor y por ello cada token0 vale reserve1/reserve0 token1's.

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
Copiar

La marca de tiempo para el último bloque en el que el intercambio ocurre se usa para rastrear los tipos de cambio a través del tiempo.

Uno de los gastos de gas más grandes de los contratos Ethereum es el almacenamiento, que persiste de una activación del contrato a la siguiente. Cada celda de almacenamiento mide 256 bits. Por lo tanto, tres variables, reserve0, reserve1 y blockTimestampLast se asignan de tal manera que un único valor de almacenamiento puede incluir las tres (112 112 32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;
Copiar

Estas variables mantienen los costes acumulados para cada token (cada uno en término del otro). Se pueden utilizar para calcular el tipo de cambio medio durante un periodo de tiempo.

1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
Copiar

La manera en que el intercambio de pares decide la tasa de cambio entre token0 y token1 consiste en mantener el múltiplo de las dos reservas constante durante las transacciones. Este valor es kLast. Cambia cuando un proveedor de liquidez deposita o retira tókenes, e incrementa ligeramente por la comisión del mercado del 0,3%.

He aquí un ejemplo. Tenga en cuenta que, en aras de la simplicidad, la tabla solo tiene tres dígitos tras el punto decimal, e ignoramos la comisión de negociación del 0,3 %, por eso los números no son precisos.

Eventoreserve0reserve1reserve0 * reserve1Tipo de cambio medio (token1 / token0)
Configuración inicial1.000,0001.000,0001.000,000
Agente A intercambia 50 token0 por 47,619 token11.050,000952.3811.000.0000,952
Agente B intercambia 10 token0 por 8,984 token11.060,000943,3961.000.0000,898
Agente C intercambia 40 token0 por 34,305 token11.100,000909,0901,000,0000,858
Agente D intercambia 100 token1 por 109,01 token0990,9901.009,0901.000.0000,917
Agente E intercambia 10 token0 por 10,079 token11.000,990999.0101,000,0001.008

Al proveer los agentes más token0, el valor relativo del token1 incrementa y vice versa, en función de la oferta y demanda.

Bloqueo

1 uint private unlocked = 1;
Copiar

Hay una clase de vulnerabilidades de seguridad que están basadas en abusos de reentrada(opens in a new tab). Uniswap necesita transferir tókenes ERC-20 arbitrarios, lo que significa activar los contratos ERC-20 que podrían intentar abusar del mercado Uniswap que los activa. Al tener una variable unlocked como parte del contrato, podemos evitar la activación de las funciones mientras se están ejecutando (dentro de la misma transacción).

1 modifier lock() {
Copiar

Esta función es un modificador(opens in a new tab), una función que se envuelve alrededor de otra función para cambiar de alguna manera su comportamiento.

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
Copiar

Si unlocked es igual a uno, configúrelo a cero. Si ya es cero, revierta la activación y trúnquela.

1 _;
Copiar

En un modificador _; es la función de activación original (con todos los parámetros). Esto significa que la activación de la función solo ocurre si la variable unlocked tenía asignado 1 cuando se activó, y mientras se ejecuta, el valor de unlocked es 0.

1 unlocked = 1;
2 }
Copiar

Después de que la función principal retorne, libere el bloqueo.

Funciones variadas

1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
2 _reserve0 = reserve0;
3 _reserve1 = reserve1;
4 _blockTimestampLast = blockTimestampLast;
5 }
Copiar

Esta función proporciona a los solicitantes el estado actual del intercambio. Nótese que las funciones de Solidity pueden proporcionar valores múltiples(opens in a new tab).

1 function _safeTransfer(address token, address to, uint value) private {
2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
Copiar

Esta función interna transfiere una cantidad de tókenes ERC-20 desde el intercambio a alguien más. SELECTOR especifica que la función que estamos activando es transfer(address,uint) (véase la definición arriba).

Para evitar el tener que importar una interfaz para la función del token, creamos «manualmente» la activación usando una de las funciones de ABI(opens in a new tab).

1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
2 }
Copiar

Hay dos maneras en las que una tranferencia ERC-20 puede informar de un fallo:

  1. Revertir. Si la activación de un contrato externo se revierte, el valor de retorno booleano es false.
  2. Termina de forma normal, pero informa de un fracaso. En ese caso, el buffer de valor devuelto tiene una longitud diferente de cero, y cuando se decodifica como un valor booleano es false.

Si ocurre alguna de estas condiciones, reviértala.

Eventos

1 event Mint(address indexed sender, uint amount0, uint amount1);
2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
Copiar

Estos dos eventos se emiten cuando un proveedor de liquidez deposita liquidez (Mint) o retira la liquidez (Quemar). En cualquier caso, los montos de Token0 y Token1 que se depositan o retiran son parte del evento, así como la identidad de la cuenta que los activó (sender). En caso de retirada, el evento además incluye al objetivo que recibió los tókenes (to) que puede no ser el mismo que el emisor.

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );
Copiar

El evento se emite cuando un agente intercambia un token por otro. Nuevamente, el emisor y el destinatario pueden no coincidir. Cada token puede ser enviado al Exchange o recibido desde allí.

1 event Sync(uint112 reserve0, uint112 reserve1);
Copiar

Finalmente, Sync se emite cada vez que se añaden o retiran tókenes, sin importar la razón, para proveer la información de reserva más actualizada (y por lo tanto el tipo de cambio).

Funciones de configuración

Se supone que estas funciones se llaman una vez cuando se establece el nuevo par de intercambio.

1 constructor() public {
2 factory = msg.sender;
3 }
Copiar

El constructor se asegura de que mantengamos el seguimiento de la dirección de la fábrica que creó el par. initialize y la tasa de fábrica (si existe) requieren esta información.

1 // called once by the factory at time of deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }
Copiar

Esta función le permite a la fábrica (y sólo a la fábrica) especificar los dos tókenes ERC-20 que este par intercambiará.

Funciones de actualización interna

_update
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
Copiar

Se realiza esta función cada vez que se depositan o retiran tókenes.

1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
Copiar

Si el balance0 o el balance1 (uint256) es mayor que uint112(-1) (=2^112-1) (por lo que overflows & se vuelve a 0 cuando se convierte en uint112) se niegan a continuar el _update para prevenir excedentes. Con un token normal que puede subdividirse en 10^18 unidades, esto significa que cada intercambio está limitado a alrededor de 5,1*10^15 de cada token. Hasta ahora no ha presentado ningún problema.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
Copiar

Si el tiempo transcurrido no es cero, significa que somos la primera transacción de cambio en este bloque. En ese caso, tenemos que actualizar los acumuladores de costes.

1 // * never overflows, and + overflow is desired
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }
Copiar

Cada acumulador de coste se actualiza con el último costo (reserva del otro token/reserva de este token) por el tiempo transcurrido en segundos. Para obtener el precio medio, se lee el precio acumulado en dos puntos en el tiempo y se divide entre la diferencia de tiempo entre ellos. Por ejemplo, asumir esta secuencia de eventos:

Eventoreserva0reserva1marca de tiempoTipo de cambio marginal (reserve1 / reserve0)price0CumulativeLast
Configuración Inicial1.000,0001.000,0005.0001,0000
Agente A deposita 50 token0 y obtiene de vuelta 47,619 token 11.050,000952,3815.0200,90720
Agente B deposita 10 token0 y obtiene de vuelta 8,984 token11.060,000943,3965.0300,89020+10*0,907 = 29,07
Agente C deposita 40 token0 y obyiene de vuelta 34,305 token11.100,000909,0905.1000,82629,07+70*0,890 = 91,37
Agente D deposita 100 token1 y obtiene de vuelta 109,01 token0990,9901.009,0905.1101,01891,37+10*0,826 = 99,63
Agente E deposita 10 token0 y obtiene de vuelta 10,079 token11.000,990999,0105.1500,99899,63+40*1,1018 = 143,702

Pongamos que queremos calcular el precio medio de Token0 entre entre la marca 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 o largo de dos minutos (120 segundos). Por lo tanto, el precio medio es de 114,632/120 = 0,955.

Este cálculo de precios es la razón por la que necesitamos conocer los antiguos tamaños de reserva.

1 reserve0 = uint112(balance0);
2 reserve1 = uint112(balance1);
3 blockTimestampLast = blockTimestamp;
4 emit Sync(reserve0, reserve1);
5 }
Copiar

Finalmente, actualice las variables globales y emita un evento Sync.

_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
Copiar

En Uniswap 2.0 los agentes pagan una comisión del 0,30 % por usar el mercado. La mayoría de esas comisiones (el 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 comisión del protocolo, el cual le paga a Uniswap por el esfuerzo de desarrollo.

Para reducir estos cálculos (y por lo tanto los costes de gas), esta comisión sólo se calcula cuando se añade o elimina la liquidez de la reserva, en lugar de hacerlo en cada transacción.

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
Copiar

Lea el destino de las comisiones de la fábrica. Si es cero, entonces no hay ninguna comisión de protocolo y no hay necesidad de calcularla.

1 uint _kLast = kLast; // gas savings
Copiar

La variable del estado kLast se encuentra en el almacenamiento, por lo que tendrá un valor entre diferentes activaciones al contrato. El acceso al almacenamiento es mucho más caro que el acceso a la memoria volátil que se libera cuando finaliza la activación de función al contrato, por lo que utilizamos una variable interna para ahorrar gas.

1 if (feeOn) {
2 if (_kLast != 0) {
Copiar

Los proveedores de liquidez obtienen su parte simplemente por la apreciación de sus tókenes de liquidez. Pero la comisión del protocolo requiere que se acuñen nuevos tókenes de liquidez y se suministren a la dirección feeTo.

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
Copiar

Si hay nueva liquidez sobre la que cobrar una tasa de protocolo. Puede ver la función raíz cuadrada más adelante en este articulo.

1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
2 uint denominator = rootK.mul(5).add(rootKLast);
3 uint liquidity = numerator / denominator;
Copiar

Este cálculo complicado de las comisiones se explica en el informe(opens in a new tab) en la página 5. Sabemos que entre el tiempo en que se calculó kLast y el presente no ha añadido ni eliminado ninguna liquidez (porque ejecutamos este cálculo cada vez que se añade o elimina liquidez, antes de que cambie realmente), por eso cualquier cambio en reserve0 * reserve1 tiene que provenir de las comisiones de transacción (sin ellas mantendremos reserve0 * reserve1 constante).

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
Copiar

Use la función UniswapV2ERC20._mint para crear los tókenes de liquidez adicionales y dárselos a la dirección feeTo.

1 } else if (_kLast != 0) {
2 kLast = 0;
3 }
4 }
Copiar

Si no hay tasa establecida kLast a cero (en caso de ser otro valor). 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 total del estado de Ethereum al eliminar el almacenamiento que no necesitaban. Este código recibe ese reembolso cuando sea posible.

Funciones accesibles externamente

Tenga en cuenta que mientras que cualquier transacción o contrato puede activar estas funciones, están diseñadas para activarse desde el contrato de la periferia. Si las activa directamente, no podrá hacer trampas al intercambio de pares, pero podrá perder valor por un error.

mint (acuñar)
1 // this low-level function should be called from a contract which performs important safety checks
2 function mint(address to) external lock returns (uint liquidity) {
Copiar

Esta función se activa cuando un proveedor de liquidez añade liquidez a la reserva. Se acuñan tókenes de liquidez adicionales como recompensa. Debe activarse desde un contrato de periferia que lo activa después de añadir la liquidez en la misma transacción (para que nadie más pueda presentar una transacción que reclame la nueva liquidez antes que el dueño legítimo).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
Copiar

Esta es la manera 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 los necesitamos.

1 uint balance0 = IERC20(token0).balanceOf(address(this));
2 uint balance1 = IERC20(token1).balanceOf(address(this));
3 uint amount0 = balance0.sub(_reserve0);
4 uint amount1 = balance1.sub(_reserve1);
Copiar

Obtenga los saldos actuales y vea cuánto se añadió de cada tipo de token.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
Copiar

Calcule las comisiones del protocolo por cobrar, si las hay, y acuñe los tókenes de liquidez respectivos. Debido a que los parámetros de _mintFee son los valores de reserva antiguos, la tasa se calcula con precisión basándose solo en los cambios de la reserva debido a las tasas.

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
Copiar

Si este es el primer depósito, cree tókenes de MINIMUM_LIQUIDITY y envíelos a la dirección cero para bloquearlos. Estos nunca se pueden canjear, lo que significa que la reserva nunca se vaciará completamente (esto nos salva de la división de cero en algunos lugares). El valor de MINIMUM_LIQUIDITY es un millar, que considerando que la mayoría de ERC-20 están subdivididos en unidades de 10^-18'th de un token, como ETH se divide en wei, es 10^-15 al valor de un solo token. No es un coste elevado.

En el momento del primer depósito no conocemos el valor relativo de los dos tókenes, así que simplemente multiplicamos las cantidades y tomamos una raíz cuadrada, suponiendo que el depósito nos proporciona el mismo valor en ambos tókenes.

Podemos confiar en esto, porque es de interés para el depositante proporcionar el mismo valor, para evitar perder valor debido a arbitrajes. Pongamos que el valor de nuestros dos tókenes es idéntico, pero nuestro depositante ha depositado 4 veces más del Token1 que del Token0. Un agente puede usar el hecho de que el intercambio de pares piensa que el Token0 es más valioso para extraer valor de él.

Eventoreserva0reserva1reserva0 * reserva1Valor de la reserva (reserve0 + reserve1)
Configuración inicial83225640
El agente depósita 8 tókenes Token0, obtiene 16 Token1161625632

Como puede ver, el agente ganó 8 tókenes extra, que provienen de una reducción en el valor de la reserva, lastimando al depositante que los posee.

1 } else {
2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
Copiar

Con cada depósito posterior ya conocemos el tipo de cambio entre ambos activos, y esperamos que los proveedores de liquidez proporcionen el mismo valor en ambos. Si no lo hacen, les damos tókenes de liquidez en función del valor menor que proporcionaron como castigo.

Ya sea el depósito inicial o uno posterior, el número de tókenes 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 obtengamos un depósito que no tenga los mismos valores de ambos tipos, en cuyo caso la «multa» se comparte). Este es otro ejemplo con dos tokens que tienen el mismo valor. con tres buenos depósitos y uno malo (depósito de un solo tipo de token, por lo que no produce ningún token de liquidez).

Eventoreserve0reserve1reserve0 * reserve1Valor de la reserva (reserve0 + reserve1)Tókenes de liquidez acuñados para este depósitoTókenes de liquidez totalesvalor de cada token de liquidez
Configuración inicial8,0008,0006416,000882,000
Depósito 4 para cada tipo12,00012,00014424,0004122,000
Depósito 2 para cada tipo14,00014,00019628,0002142,000
Depósito de valor desigual18,00014,00025232,000014~2,286
Después del arbitraje~15,874~15,874252~31,748014~2,267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);
Copiar

Use la función UniswapV2ERC20._mint para crear realmente la liquidez adicional de tókenes y dárselos a la cuenta correcta.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
4 emit Mint(msg.sender, amount0, amount1);
5 }
Copiar

Actualice las variables de estado (reserve0, reserve1, y si es necesario kLast) y emita el evento apropiado.

burn (quemar)
1 // this low-level function should be called from a contract which performs important safety checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
Copiar

Esta función se activa cuando se retira liquidez y es preciso «quemar» los tókenes de liquidez apropiados. También debería activarse desde una cuenta de la periferia.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];
Copiar

El contrato de la periferia transfirió la liquidez por quemar a este contrato antes de activarla. De esa manera sabemos cuánta liquidez quemar, y podemos asegurarnos de que se quema.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
Copiar

El proveedor de liquidez recibe un valor igual de ambos tókenes. De esta manera no cambiamos el tipo de cambio.

1 _burn(address(this), liquidity);
2 _safeTransfer(_token0, to, amount0);
3 _safeTransfer(_token1, to, amount1);
4 balance0 = IERC20(_token0).balanceOf(address(this));
5 balance1 = IERC20(_token1).balanceOf(address(this));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Mostrar todo
Copiar

El resto de las funciones burn es una imagen espejo de la función mint citada anteriormente.

swap (intercambio)
1 // this low-level function should be called from a contract which performs important safety checks
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
Copiar

Esta función también se supone que debe activarse desde un contrato de periferia.

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
Copiar

Las variables locales pueden almacenarse en la memoria o, si no hay demasiadas, directamente en la pila. Si podemos limitar el número, así usaremos la pila que gaste menos gas. Si desea conocer más detalles, consulte el protocolo, las especificaciones formales de Ethereum(opens in a new tab), pág. 26, ecuación 298.

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
Copiar

Esta tranferencia es optimista, ya que transferimos una vez que estamos seguros de que se cumplen todas las condiciones. Esto puede hacerse en Ethereum, porque si las condiciones no se cumplen más tarde en la activación, revertimos cualquier cambio que haya creado.

1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
Copiar

Informe al receptor sobre el intercambio, si así lo solicita.

1 balance0 = IERC20(_token0).balanceOf(address(this));
2 balance1 = IERC20(_token1).balanceOf(address(this));
3 }
Copiar

Obtén los saldos actuales. El contrato de la periferia nos envía los tókenes antes de llamarnos para el intercambio. Esto facilita que el contrato compruebe que no está siendo engañado, una comprobación de que tiene que pasar en el contrato central (porque se nos puede activar mediante otras entidades que no sean nuestro contrato de periferia).

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
Copiar

Esta es una comprobación para asegurarnos de que no perdemos en el intercambio. No hay ningún impedimento en el que un intercambio deba reducir reserve0*reserve1. Aquí es donde también garantizamos que se esté enviando una tarifa del 0,3 % en el intercambio; antes de que la comprobación verifique el valor de K, multiplicamos ambos saldos por 1.000 y le restamos las cantidades multiplicadas por 3, esto nos da que el 0,3 % (3/1.000 = 0,003 = 0,3 %) se está deduciendo del saldo antes de comparar su valor K con el valor actual de reservas K.

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
Copiar

Actualice reserve0 y reserve1y, si es necesario, los acumuladores de precio y la marca de tiempo y emita un evento.

Sync o Skim

Es posible que los saldos reales no estén sincronizados con las reservas que el par de intercambio cree tener. No hay forma de retirar tókenes sin el consentimiento del contrato, pero los depósitos son un asunto diferente. Una cuenta puede transferir tókenes al intercambio sin activar a mint o swap.

En ese caso hay dos soluciones:

  • sync, actualice las reservas de los saldos actuales.
  • skim, retier la cantidad extra. Tenga en cuenta que cualquier cuenta puede ejecutar skim porque no sabemos quién depositó los tókenes. Esta información se emite en un evento, no obstante a los eventos no se puede acceder desde la cadena de bloques.
1 // force balances to match reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Mostrar todo
Copiar

UniswapV2Factory.sol

Este contrato(opens in a new tab) implementa pares de intercambio.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;
Copiar

Estas variables de estado son necesarias para implementar las comisiones del protocolo (ver el papel blanco(opens in a new tab), p. 5). La dirección feeTo acumula los tókenes de liquidez por la tarifa del protocolo, y feeToSetter es la dirección permitida para cambiar feeTo a una dirección diferente.

1 mapping(address => mapping(address => address)) public getPair;
2 address[] public allPairs;
Copiar

Estas variables registran los pares, los intercambios entre dos tipos de tókenes.

El primero, getPair, es un mapeo que identifica un contrato de intercambio de pares basado en los dos tókenes ERC-20 que intercambia. Los tókenes ERC-20 se identifican por las direcciones de los contratos que los implementan, por lo que las claves y el valor son direcciones en su totalidad. Para obtener la dirección del par de intercambio que le permite pasar de tokenA a tokenB, utilice 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 repetir 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.

Aviso: la razón por la que no se pueden repetir todas las claves de un mapeo es que el almacenamiento de datos contractuales resulta costoso, por lo que cuanto menos utilicemos, mejor; y cuanto menos los cambiemos, mejor. Puede crear mapeos que soporten la iteración(opens in a new tab), pero requieren almacenamiento extra para una lista de claves. En la mayoría de las aplicaciones no es necesario.

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
Copiar

Este evento se emite cuando se crea una nuevo intercambio de pares. Incluye las direcciones de los tókenes, la dirección de intercambio de pares y el número total de intercambios gestionados por la fábrica.

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }
Copiar

Lo único que hace el constructor es especificar feeToSetter. Las fábricas empiezan sin tasa, y sólo feeSetter puede cambiar eso.

1 function allPairsLength() external view returns (uint) {
2 return allPairs.length;
3 }
Copiar

Esta función muestra el número de pares de intercambio.

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
Copiar

Esta es la función principal de la fábrica: crear un intercambio de pares entre dos tókenes ERC-20. Ten en cuenta que cualquiera puede ejecutar a esta función. No necesita permiso de Uniswap para crear un nuevo intercambio de pares.

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
Copiar

Queremos que la dirección del nuevo intercambio sea de carácter concluyente, de modo que pueda calcularse de antemano fuera de la cadena (esto puede ser útil para las transacciones de capa 2). Para ello necesitamos seguir un orden consistente de las direcciones de los tókenes, independientemente del orden en que los hayamos recibido, así que los ordenamos aquí.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
Copiar

Las reservas de liquidez más grandes son mejores que los pequeñas, porque tienen precios más estables. No queremos tener más de una reserva de liquidez por par de tókenes. Si ya existe un intercambio, no es necesario crear otro para el mismo par.

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;
Copiar

Para crear un nuevo contrato, necesitamos el código que lo crea (tanto la función que lo construye, como el código que escribe a la memoria el código de bytes de la EVM del contrato actual). Por lo general, en Solidity sólo usamos addr = new<name of contract> (<constructor parameters>) y el compilador se encarga de todo, 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, el código de operación Solidity aún no lo soportaba, por lo que era necesario obtener manualmente el código. Esto ya no es un problema, porque Solidity ahora soporta CREATE2(opens in a new tab).

1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));
2 assembly {
3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
4 }
Copiar

Cuando Solidity aún no soporta un código de operación, podemos activarlo usando el montaje en línea(opens in a new tab).

1 IUniswapV2Pair(pair).initialize(token0, token1);
Copiar

Active la función initialize para informar al intercambio cuáles son los dos tókenes que este intercambia.

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }
Copiar

Guarde el nuevo par de información en las variables de estado y transmita un evento para informar al mundo sobre el nuevo intercambio de pares.

1 function setFeeTo(address _feeTo) external {
2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
3 feeTo = _feeTo;
4 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
Mostrar todo
Copiar

Estas dos funciones permiten a feeSetter controlar la tarifa receptora (si la hay) y cambia feeSetter a una nueva dirección.

UniswapV2ERC20.sol

Este contrato(opens in a new tab) implementa el token de liquidez ERC-20. Este es bastante 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 equivale a dinero real. Si tiene tókenes 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 tókenes firma una transacción que le permite a alguien retirar tókenes fuera de la cadena y enviarlos por medio de Internet al receptor. El receptor, que tiene ETH, luego envía el permiso en nombre del propietario.

1 bytes32 public DOMAIN_SEPARATOR;
2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
Copiar

Este hash es el identificador para el tipo de transacción(opens in a new tab). El único que soportamos aquí es Permit con estos parámetros.

1 mapping(address => uint) public nonces;
Copiar

No es conveniente para un receptor falsificar una firma digital. Sin embargo, es trivial enviar la misma transacción dos veces (esto es una forma de ataque de repetición(opens in a new tab)). Para evitarlo, usamos nonce(opens in a new tab). Si el nonce de un nuevo Permit no es mayor que el último usado, se asume que este no es válido.

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }
Copiar

Este es el código para recuperar el identificador de cadena(opens in a new tab). Este usa un dialecto ensamblado por la EVM llamado Yul(opens in a new tab). Recuerde que en la versión actual de Yul necesita usar chainid(), no chainid.

1 DOMAIN_SEPARATOR = keccak256(
2 abi.encode(
3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
4 keccak256(bytes(name)),
5 keccak256(bytes('1')),
6 chainId,
7 address(this)
8 )
9 );
10 }
Mostrar todo
Copiar

Calcule el separador de dominio(opens in a new tab) para EIP-712.

1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
Copiar

Esta es la función que implementa los permisos. Esta recibe como parámetros los campos relevantes y los tres valores escalables para la firma(opens in a new tab) (v, r y s).

1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
Copiar

No acepte transacciones fuera del límite de tiempo.

1 bytes32 digest = keccak256(
2 abi.encodePacked(
3 '\x19\x01',
4 DOMAIN_SEPARATOR,
5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
6 )
7 );
Copiar

abi.encodePacked(...) es el mensaje que esperamos obtener. Sabemos que el nonce debería estar, por lo que es necesario obtenerlo como parámetro.

El algoritmo de firma de Ethereum espera obtener 256 bits para firmar y poder usar la función de hash keccak256.

1 address recoveredAddress = ecrecover(digest, v, r, s);
Copiar

Desde el resumen y la firma podemos obtener la dirección que la firmó usando ecrecover(opens in a new tab).

1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
2 _approve(owner, spender, value);
3 }
4
Copiar

Si todo está bien, trate esto como una aprobación ERC-20(opens in a new tab).

Los contratos periféricos

Los contratos periféricos son la API (interfaz del programa de aplicación) para Uniswap. Estos están disponibles para activaciones externas, ya sea desde otros contratos o aplicaciones descentralizadas. Podría activar los contratos principales directamente, pero es más complicado y podría perder valor si comete un error. Los contratos principales solo contienen pruebas para asegurarnos de que no están trucados, no revisiones sanitarias para otras cosas. Estos están en la periferia, por lo que pueden actualizarse según se necesite.

UniswapV2Router01.sol

Este contrato(opens in a new tab) tiene problemas y no debería utilizarse(opens in a new tab). Afortunadamente, los contratos periféricos no tienen estado y no almacenan ningún activo, lo que los hace fáciles de desaprobar y sugerir a las personas usar 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). Aquí(opens in a new tab) puede ver cómo usarlo.

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Mostrar todo
Copiar

La mayoría de estos los hemos encontrado antes o son muy obvios. La única excepción es IWETH.sol. Uniswap v2 permite el intercambio de cualquier par de tókenes ERC-20, pero ether (ETH) no es un token ERC-20. Este antecede al estándar y se transfiere usando mecanismos únicos. Para habilitar el uso de ETH en contratos aplicables a los tókenes ERC-20, las personas pueden usar el contrato wrapped ether (WETH)(opens in a new tab). Puede enviar ETH a este contrato y este acuña una cantidad equivalente de WETH. También puede quemar WETH y obtener ETH después de esto.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;
Copiar

El enrutador necesita conocer qué fábrica debe usar y para transacciones que requieren WETH, qué contrato de WETH debe usar. Estos valores son inmutables(opens in a new tab), lo que significa que sólo se pueden establecer en el constructor. Esto le proporciona a los usuarios la garantía de que nadie podría cambiarlos para apuntar a contratos poco honestos.

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
Copiar

Este modificador se asegura de que las transacciones con límite de tiempo («si puede, haga X antes del tiempo X») no sucedan antes de su límite de tiempo.

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }
Copiar

El constructor solo establece las variables de estado inmutables.

1 receive() external payable {
2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
3 }
Copiar

Esta función se activa cuando canjeamos tókenes del contrato WETH en ETH. Sólo el contrato WETH que usamos está autorizado para hacer eso.

Agregue liquidez

Estas funciones agregan tókenes al intercambio de pares, que incrementa la reserva de liquidez.

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
Copiar

Esta función se usa para calcular la cantidad de tókenes A y B que se debería depositar en el intercambio de pares.

1 address tokenA,
2 address tokenB,
Copiar

Estas son las direcciones de los contratos de token ERC-20.

1 uint amountADesired,
2 uint amountBDesired,
Copiar

Estas son las cantidades que el proveedor de liquidez quiere depositar. También son las cantidades máximas de A y B que se pueden depositar.

1 uint amountAMin,
2 uint amountBMin
Copiar

Estas son las cantidades mínimas aceptadas para depositar. Si la transacción no se puede realizar con estas cantidades o más, debe revertirla. Si no quiere esta característica, especifique únicamente cero.

Los proveedores de liquidez especifican un mínimo, por lo general, porque ellos quieren limitar la transacción a un tipo de cambio que es cercano al actual. Si el tipo de cambio fluctúa demasiado, podría significar que cambien los valores subyacentes y quieran decidir manualmente qué hacer.

Por ejemplo, imagine un caso donde el tipo de cambio es de uno a uno y el proveedor de liquidez especifica estos valores:

ParámetroValor
amountADesired1.000
amountBDesired1.000
amountAMin900
amountBMin800

Mientras el tipo de cambio se encuentre entre 0,9 y 1,25, la transacción se realiza. Si el tipo de cambio está fuera de ese rango, la transacción se cancela.

El motivo de esta precaución es que las transacciones no son inmediatas, usted las envías y, eventualmente, un validador las incluirá en un bloque (a menos que su precio de gas sea muy bajo, en cuyo caso necesitará enviar otra transacción con el mismo valor y un precio de gas más alto para sobrescribirla). No puedes controlar lo que sucede durante el intervalo entre el envío y la inclusión.

1 ) internal virtual returns (uint amountA, uint amountB) {
Copiar

La función muestra las cantidades que el proveedor de liquidez debería depositar para tener una proporción igual a la proporción actual entre reservas.

1 // create the pair if it doesn't exist yet
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }
Copiar

Si no hay un intercambio para este par de tókenes, créelo.

1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
Copiar

Obtenga las reservas actuales en el par.

1 if (reserveA == 0 && reserveB == 0) {
2 (amountA, amountB) = (amountADesired, amountBDesired);
Copiar

Si las reservas actuales están vacías, entonces esto es un nuevo intercambio de pares. Las cantidades por depositar deberían ser exactamente iguales a las que el proveedor de liquidez desea proporcionar.

1 } else {
2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
Copiar

Si necesitamos ver de cuánto serían las cantidades, obtenemos el valor adecuado usando esta función(opens in a new tab). Queremos la misma proporción de las reservas actuales.

1 if (amountBOptimal <= amountBDesired) {
2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 (amountA, amountB) = (amountADesired, amountBOptimal);
Copiar

Si amountBOptimal es menor que la cantidad que el proveedor de liquidez quiere depositar, esto significa que el token B es más valioso que lo que el depositante piensa, por lo que se requiere una cantidad menor.

1 } else {
2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
3 assert(amountAOptimal <= amountADesired);
4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
5 (amountA, amountB) = (amountAOptimal, amountBDesired);
Copiar

Si la cantidad óptima B es mayor que la cantidad B deseada, esto significa que los tókenes B son menos valiosos que lo que el depositante de liquidez piensa, por lo que se requiere una cantidad más alta. 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 tókenes A para la cantidad deseada de tókenes B.

Al unirlo todo, obtenemos este gráfico. Pongamos que está intentando depositar mil tókenes A (línea azul) y mil tókenes B (línea roja). El eje x es el tipo de cambio, A/B. Si x=1, son iguales en valor y deposita mil de cada uno. Si x=2, A es el doble del valor de B (obtienes dos tókenes B por cada token A), por lo que deposita mil tókenes B, pero solo 500 tókenes A. Si x=0.5, la situación se invierte, mil tókenes A y quinientos tókenes B.

Graph

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 en sí mismo, 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 utiliza el contrato periférico, calcule la cantidad que debe depositar y deposítela inmediatamente, para que el tipo de cambio no cambie y no pierda nada.

1 function addLiquidity(
2 address tokenA,
3 address tokenB,
4 uint amountADesired,
5 uint amountBDesired,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
Mostrar todo
Copiar

Esta función puede activarse mediante una transacción para depositar liquidez. La mayoría de los parámetros son los mismos que en _addLiquidity anterior, con dos excepciones:

. to es la dirección que obtiene los nuevos tókenes de liquidez acuñados para mostrar la parte de la reserva del proveedor de liquidez . fecha límite es un límite de tiempo para la transacción

1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
Copiar

Calculamos las cantidades para 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 las bibliotecas)

1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
Copiar

Transfiera las cantidades correctas de tókenes del usuario al intercambio de pares.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
Copiar

A cambio, dé los tókenes de liquidez de la dirección to para la propiedad parcial del grupo. La función mint del contrato principal ve cuántos tókenes adicionales tiene (en comparación con los que tenía la última vez que cambió la liquidez) y la liquidez de las acuñaciones en consecuencia.

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,
Copiar

Cuando un proveedor de liquidez quiere proporcionar liquidez a un intercambio de par Token/ETH, hay algunas diferencias. El contrato maneja la envoltura del ETH para el proveedor de liquidez. No hay necesidad de especificar cuántos ETH quiere depositar el usuario, porque el usuario solo los envía con la transacción (la cantidad está disponible en msg.value).

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint deadline
5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
6 (amountToken, amountETH) = _addLiquidity(
7 token,
8 WETH,
9 amountTokenDesired,
10 msg.value,
11 amountTokenMin,
12 amountETHMin
13 );
14 address pair = UniswapV2Library.pairFor(factory, token, WETH);
15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
16 IWETH(WETH).deposit{value: amountETH}();
17 assert(IWETH(WETH).transfer(pair, amountETH));
Mostrar todo
Copiar

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 una reinvindicación. Esto significa que si la transferencia falla, esta activación de contrato también falla y, por lo tanto, la envoltura realmente no ocurre.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }
Copiar

El usuario ya nos ha enviado el ETH, por lo que si queda algo extra (porque el otro token es menos valioso de lo que el usuario pensaba), tenemos que emitir un reembolso.

Remover la liquidez

Estas funciones eliminarán la liquidez y lo devolverán al proveedor de liquidez.

1 // **** REMOVE LIQUIDITY ****
2 function removeLiquidity(
3 address tokenA,
4 address tokenB,
5 uint liquidity,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
Mostrar todo
Copiar

El caso más sencillo de eliminar la liquidez. Hay una cantidad mínima de cada token que el proveedor de liquidez acepta, y debe ocurrir antes de la fecha límite.

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
Copiar

La función burn del contrato principal se encarga de devolver al usuario los tókenes.

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
Copiar

Cuando una función devuelve varios valores, pero solo estamos interesados en algunos de ellos, así es como solo obtenemos esos valores. Es algo más barato en términos de gas que leer un valor y nunca usarlo.

1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
Copiar

Traduce las cantidades de la forma en que el contrato principal las devuelve (primero el token de dirección inferior) a la forma en que el usuario las espera (correspondiente a tokenA y tokenB).

1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 }
Copiar

Se puede hacer la transferencia primero y luego verificar que es legítima, porque si no lo es, revertiremos todos los cambios de estado.

1 function removeLiquidityETH(
2 address token,
3 uint liquidity,
4 uint amountTokenMin,
5 uint amountETHMin,
6 address to,
7 uint deadline
8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
9 (amountToken, amountETH) = removeLiquidity(
10 token,
11 WETH,
12 liquidity,
13 amountTokenMin,
14 amountETHMin,
15 address(this),
16 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
Mostrar todo
Copiar

Eliminar la liquidez para ETH es casi la misma, excepto que recibimos los tókenes WETH y luego los canjeamos para que ETH los devuelva al proveedor de liquidez.

1 function removeLiquidityWithPermit(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline,
9 bool approveMax, uint8 v, bytes32 r, bytes32 s
10 ) external virtual override returns (uint amountA, uint amountB) {
11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
12 uint value = approveMax ? uint(-1) : liquidity;
13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
15 }
16
17
18 function removeLiquidityETHWithPermit(
19 address token,
20 uint liquidity,
21 uint amountTokenMin,
22 uint amountETHMin,
23 address to,
24 uint deadline,
25 bool approveMax, uint8 v, bytes32 r, bytes32 s
26 ) external virtual override returns (uint amountToken, uint amountETH) {
27 address pair = UniswapV2Library.pairFor(factory, token, WETH);
28 uint value = approveMax ? uint(-1) : liquidity;
29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
31 }
Mostrar todo
Copiar

Estas funciones retransmiten metatransacciones para permitir que los usuarios sin ether se retiren del grupo, utilizando el mecanismo de permiso.

1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
3 function removeLiquidityETHSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
Mostrar todo
Copiar

Esta función se puede utilizar para tókenes que tienen tarifas de transferencia o almacenamiento. Cuando un token tiene tales tarifas, no podemos confiar en la función removeLiquidity para decirnos cuánto del token recibimos, por lo que primero tenemos que retirar y luego obtener el saldo.

1
2
3 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline,
10 bool approveMax, uint8 v, bytes32 r, bytes32 s
11 ) external virtual override returns (uint amountETH) {
12 address pair = UniswapV2Library.pairFor(factory, token, WETH);
13 uint value = approveMax ? uint(-1) : liquidity;
14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
16 token, liquidity, amountTokenMin, amountETHMin, to, deadline
17 );
18 }
Mostrar todo
Copiar

La función final combina las tarifas de almacenamiento con las metatransacciones.

Comercio

1 // **** SWAP ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
Copiar

Esta función realiza el procesamiento interno que se requiere para las funciones que están expuestas a los comerciantes.

1 for (uint i; i < path.length - 1; i++) {
Copiar

A la edición de este artículo, hay 388.160 tókenes ERC-20(opens in a new tab). Si hubiera un par de intercambio por cada par de tókenes, sería de más de 150 mil millones de pares de intercambio. Toda la cadena, por el momento, solo tiene un 0,1 % de ese número de cuentas(opens in a new tab). En lugar, las funciones de intercambio soportan el concepto de una ruta. Un comerciante puede cambiar A por B, B por, C, y C por D, entonces, no hay necesidad para un cambio directo de pares A-D.

Los precios en estos mercados tienden a sincronizarse, debido a que cuando ellos están desincronizados, se crea una oportunidad para el arbitraje. Imagine, por ejemplo, tres tókenes, A, B y C. Hay tres intercambios de pares, uno para cada par.

  1. La situación inicial
  2. Un comerciante vende 24,695 tókenes A, y recibe 25,305 tókenes B.
  3. El comerciante vende 24,695 tókenes B por 25,305 tókenes C, manteniendo aproximadamente 0,61 de tókenes B como ganancia.
  4. El comerciante vende 24,695 tókenes C por 25,305 tókenes A, manteniendo aproximadamente 0,61 tókenes C como ganancia. El comerciante también tiene un 0,61 tókenes A adicionales (los 25,305 con los que el comerciante termina, menos la inversión original de 26,695).
PasoIntercambio A-BIntercambio B-CIntercambio A-C
1A:1.000 B:1.050 A/B=1,05B:1.000 C:1.050 B/C=1,05A:1.050 C:1.000 C/A=1,05
2A:1.024,695 B:1.024,695 A/B=1B:1.000 C:1.050 B/C=1,05A:1.050 C:1.000 C/A=1,05
3A:1.024,695 B:1.024,695 A/B=1B:1,024,695 C:1.024,695 B/C=1A:1.050 C:1.000 C/A=1,05
4A:1.024,695 B:1.024,695 A/B=1B:1,024,695 C:1.024,695 B/C=1A:1,024,695 C:1.024,695 C/A=1
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];
Copiar

Obtiene el par que actualmente estamos manejando, lo ordena (para usarlo con el par) y obtiene la cantidad esperada de salida.

1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
Copiar

Obtiene las cantidades esperadas de salida, ordenadas de la manera esperada por el intercambio de pares.

1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
Copiar

¿Este es el último intercambio? Si es así, envíe los tókenes recibidos para el intercambio al destino. Si no, envíelo al siguiente intercambio de pares.

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }
Copiar

En realidad, lo que hace es activar el intercambio de pares para intercambiar los tókenes. No necesitamos devolver la activación para que nos informen sobre el intercambio, por lo que no enviamos ningún byte en ese campo.

1 function swapExactTokensForTokens(
Copiar

Esta función la usan directamente los comerciantes para intercambiar un token por otro.

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,
Copiar

Este parámetro contiene las direcciones de los contratos ERC-20. Como se ha explicado previamente, este es una matriz, porque puede que necesite recorrer varios intercambios de pares para obtener el activo que quiere, desde el activo que ya tiene.

Un parámetro de función en Solidity se puede almacenar ya sea en memory o calldata. Si la función es un punto de entrada al contrato, activada directamente por el usuario (usando una transacción) o desde un contrato diferente, entonces el valor del parámetro puede tomarse directamente desde calldata. Si la función se activa internamente, como _swap anteriormente, entonces los parámetros se deben almacenar en memory. Desde la perspectiva del contrato activado, calldata es de sólo lectura.

Con tipos escalables como uint o address, el compilador maneja por nosotros la elección de almacenamiento, pero con matrices que son más largos y más costosos, nosotros especificamos el tipo de almacenamiento por utilizar.

1 address to,
2 uint deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
Copiar

Los valores de retorno siempre son devueltos en la memoria.

1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
Copiar

Calcule la cantidad por comprarse en cada intercambio. Si el resultado es menor que lo mínimo aceptado por el agente, la transacción se revierte.

1 TransferHelper.safeTransferFrom(
2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
3 );
4 _swap(amounts, path, to);
5 }
Copiar

Por último, transfiera el primer token ERC-20 a la cuenta para el primer intercambio de pares y active _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.

1 function swapTokensForExactTokens(
2 uint amountOut,
3 uint amountInMax,
4 address[] calldata path,
5 address to,
6 uint deadline
7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
10 TransferHelper.safeTransferFrom(
11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
12 );
13 _swap(amounts, path, to);
14 }
Mostrar todo
Copiar

La función anterior, swapTokensForTokens, permite a un agente especificar una cantidad exacta de tókenes de entrada que está dispuesto a dar y la cantidad de tókenes de salida que está dispuesto a recibir. Esta función hace el intercambio a la inversa: permite al agente especificar la cantidad de tókenes de salida que quiere y la cantidad máxima de tókenes de entrada que está dispuesto a pagar por ellos.

En ambos casos, el agente debe otorgar un permiso al contrato de la periferia para permitir las transferencias.

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 external
3 virtual
4 override
5 payable
6 ensure(deadline)
7 returns (uint[] memory amounts)
8 {
9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
12 IWETH(WETH).deposit{value: amounts[0]}();
13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
14 _swap(amounts, path, to);
15 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 ensure(deadline)
23 returns (uint[] memory amounts)
24 {
25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
28 TransferHelper.safeTransferFrom(
29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
30 );
31 _swap(amounts, path, address(this));
32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
34 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 ensure(deadline)
43 returns (uint[] memory amounts)
44 {
45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
48 TransferHelper.safeTransferFrom(
49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
50 );
51 _swap(amounts, path, address(this));
52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
54 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 ensure(deadline)
63 returns (uint[] memory amounts)
64 {
65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
68 IWETH(WETH).deposit{value: amounts[0]}();
69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
70 _swap(amounts, path, to);
71 // refund dust eth, if any
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Mostrar todo
Copiar

Estas cuatro variantes implican operaciones entre ETH y tókenes. La única diferencia es que o bien recibimos ETH del agente y lo usamos para acuñar WETH, o recibimos WETH del último intercambio en la ruta y lo quemamos, enviando al agente de vuelta el ETH resultante.

1 // **** SWAP (supporting fee-on-transfer tokens) ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
Copiar

Esta es la función interna para intercambiar tókenes que tienen tarifas de transferencia o almacenamiento para resolver (este problema(opens in a new tab)).

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // scope to avoid stack too deep errors
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
Mostrar todo
Copiar

A causa de las tarifas de transferencia, no podemos confiar en la función getAmountsOut para decirnos cuánto obtenemos de cada transferencia (de la forma en que lo hacemos antes de activar el _swap original). En su lugar, tenemos que transferir primero y luego ver cuántos tókenes hemos recibido.

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 tókenes de tarifas de transferencia son bastante raros, por lo que, aunque necesitamos acomodarlos, no hay necesidad de que todos los intercambios asuman que pasan por al menos uno de ellos.

1 }
2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
4 pair.swap(amount0Out, amount1Out, to, new bytes(0));
5 }
6 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
20 _swapSupportingFeeOnTransferTokens(path, to);
21 require(
22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
24 );
25 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 ensure(deadline)
39 {
40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
41 uint amountIn = msg.value;
42 IWETH(WETH).deposit{value: amountIn}();
43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
45 _swapSupportingFeeOnTransferTokens(path, to);
46 require(
47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
49 );
50 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 ensure(deadline)
64 {
65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
66 TransferHelper.safeTransferFrom(
67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
68 );
69 _swapSupportingFeeOnTransferTokens(path, address(this));
70 uint amountOut = IERC20(WETH).balanceOf(address(this));
71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
72 IWETH(WETH).withdraw(amountOut);
73 TransferHelper.safeTransferETH(to, amountOut);
74 }
Mostrar todo
Copiar

Estas son las mismas variantes que se utilizan para los tókenes normales, pero en su lugar activan a _swapSupportingFeeOnTransferTokens.

1 // **** LIBRARY FUNCTIONS ****
2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
3 return UniswapV2Library.quote(amountA, reserveA, reserveB);
4 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
Mostrar todo
Copiar

Estas funciones son solo proxies que activan a las funciones de UniswapV2Library.

UniswapV2Migrator.sol

Este contrato se ha utilizado para migrar los intercambios de la antigua v1 a la v2. Ahora que ya se 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í.

Matemáticas

Esta biblioteca contiene algunas funciones matemáticas que no suele necesitar el código Solidity, por lo que no son parte del lenguaje.

1pragma solidity =0.5.16;
2
3// a library for performing various math operations
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
11 function sqrt(uint y) internal pure returns (uint z) {
12 if (y > 3) {
13 z = y;
14 uint x = y / 2 + 1;
Mostrar todo
Copiar

Comience con x como una estimación que es más alta que la raíz cuadrada (esa es la razón por la que necesitamos tratar 1-3 como casos especiales).

1 while (x < z) {
2 z = x;
3 x = (y / x + x) / 2;
Copiar

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 entre la estimación anterior. Repita hasta que la nueva estimación no sea inferior a la existente. Para más detalles, vea aquí(opens in a new tab).

1 }
2 } else if (y != 0) {
3 z = 1;
Copiar

Nunca deberíamos necesitar la raíz cuadrada de cero. Las raíces cuadradas de uno, dos y tres son aproximadamente una (usamos números enteros, por lo que ignoramos la fracción).

1 }
2 }
3}
Copiar

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 suma y resta originales sin ningún cambio.

1pragma solidity =0.5.16;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1]
6// resolution: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Mostrar todo
Copiar

Q112 es la codificación para uno.

1 // encode a uint112 as a UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows
4 }
Copiar

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 UQ112x112.

1 // divide a UQ112x112 by a uint112, returning a UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}
Copiar

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 tenido que usar un truco similar para hacer la multiplicación, pero no necesitamos multiplicar los valores de UQ112x112.

UniswapV2Library

Esta biblioteca solo la utilizan los contratos de la periferia

1pragma solidity >=0.5.0;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order
11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
15 }
Mostrar todo
Copiar

Ordene los dos tókenes por dirección, para que podamos obtener la dirección del intercambio de pares por 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.

1 // calculates the CREATE2 address for a pair without making any external calls
2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
3 (address token0, address token1) = sortTokens(tokenA, tokenB);
4 pair = address(uint(keccak256(abi.encodePacked(
5 hex'ff',
6 factory,
7 keccak256(abi.encodePacked(token0, token1)),
8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
9 ))));
10 }
Mostrar todo
Copiar

Esta función calcula la dirección del intercambio de pares para los dos tókenes. Este contrato se crea usando el código de operación CREATE2(opens in a new tab), por lo que podemos calcular la dirección usando el mismo algoritmo si conocemos los parámetros que utiliza. Esto es mucho más barato que preguntarle a la fábrica, y

1 // fetches and sorts the reserves for a pair
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }
Copiar

Esta función devuelve las reservas de los dos tókenes que tiene el intercambio de pares. Tenga en cuenta que puede recibir los tókenes en cualquier orden y ordenarlos para uso interno.

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }
Copiar

Esta función le da la cantidad de token B que obtendrá a cambio del token A si no hay ningún cargo involucrado. Este cálculo tiene en cuenta que la transferencia cambia el tipo de cambio.

1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
Copiar

La función quote anterior funciona muy bien si no hay ninguna tarifa para usar el intercambio de pares. Sin embargo, si hay una tarifa de cambio del 0,3 %, la cantidad que realmente obtiene es menor. Esta función calcula la cantidad después de la tarifa de cambio.

1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
4 uint amountInWithFee = amountIn.mul(997);
5 uint numerator = amountInWithFee.mul(reserveOut);
6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);
7 amountOut = numerator / denominator;
8 }
Copiar

Solidity no maneja las fracciones de forma nativa, por lo que no podemos multiplicar la cantidad por 0,997. En su lugar, multiplicamos el numerador por 997 y el denominador por 1.000, logrando el mismo efecto.

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }
Copiar

Esta función hace más o menos lo mismo, pero obtiene la cantidad de salida y proporciona la entrada.

1
2 // performs chained getAmountOut calculations on any number of pairs
3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
5 amounts = new uint[](path.length);
6 amounts[0] = amountIn;
7 for (uint i; i < path.length - 1; i++) {
8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
10 }
11 }
12
13 // performs chained getAmountIn calculations on any number of pairs
14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
16 amounts = new uint[](path.length);
17 amounts[amounts.length - 1] = amountOut;
18 for (uint i = path.length - 1; i > 0; i--) {
19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
21 }
22 }
23}
Mostrar todo
Copiar

Estas dos funciones manejan la identificación de los valores cuando es necesario pasar por varios intercambios de pares.

Ayudante de transferencia

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 valor falso devuelto de la misma manera.

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
Mostrar todo
Copiar

Podemos llamar a un contrato diferente de una de dos maneras:

1 require(
2 success && (data.length == 0 || abi.decode(data, (bool))),
3 'TransferHelper::safeApprove: approve failed'
4 );
5 }
Copiar

En aras de la compatibilidad con versiones anteriores con el token que se crearon antes del estándar ERC-20, una activación 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 lo decodifica como un booleano, obtiene false).

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::safeTransfer: transfer failed'
13 );
14 }
Mostrar todo
Copiar

Esta función implementa la funcionalidad de transferencia de ERC-20(opens in a new tab), que permite a una cuenta gastar la asignación proporcionada por una cuenta diferente.

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::transferFrom: transferFrom failed'
13 );
14 }
Mostrar todo
Copiar

Esta función implementa la funcionalidad de ERC-20 transferFrom(opens in a new tab), que permite a una cuenta gastar la asignación proporcionada por una cuenta diferente.

1
2 function safeTransferETH(address to, uint256 value) internal {
3 (bool success, ) = to.call{value: value}(new bytes(0));
4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
5 }
6}
Copiar

Esta función transfiere ether a una cuenta. Cualquier activación de un contrato diferente puede intentar enviar ether. Debido a que no necesitamos activar ninguna función realmente, no enviamos ningún dato con la activación.

Conclusión

Este es un artículo largo de aproximadamente 50 páginas. Si ha llegado hasta aquí, ¡le felicitamos! Espero que a estas alturas haya entendido lo referente a escribir una aplicación de verdad (a diferencia de programas cortos de ejemplo) y ahora sea capaz de escribir mejor contratos para sus propios casos de uso.

Ahora ya puede ir y escribir algo útil que nos sorprenda.

¿Le ha resultado útil este tutorial?