Ir al contenido principal

Un recorrido por el contrato Uniswap-v2

Solidity
Intermedio
Ori Pomerantz
1 de mayo de 2021
62 minuto leído

Introducción

Uniswap v2opens in a new tab puede crear un mercado de intercambio entre dos tókenes ERC-20 cualesquiera. En este artículo revisaremos 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: los proveedores de liquidez y los traders.

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

Los traders envían un tipo de token a la reserva y reciben el otro (por ejemplo, envían Token0 y reciben Token1) de la reserva proporcionada por los proveedores de liquidez. El tipo de cambio se determina por la cantidad relativa de Token0 y Token1 que tiene la reserva. Además, la reserva toma un pequeño porcentaje como recompensa para el fondo de liquidez.

Cuando los proveedores de liquidez quieren recuperar sus activos, pueden quemar los tókenes de la reserva y recibir de vuelta sus tókenes, incluida su parte de las recompensas.

Haga clic aquí para obtener una descripción más completaopens in a new tab.

¿Por qué v2? ¿Por qué no v3?

Uniswap v3opens in a new tab es una actualización mucho más complicada que la v2. Es más fácil aprender primero v2 y luego pasar a v3.

Contratos principales frente a 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 contienen los activos y, por lo tanto, tienen que ser seguros, sean más simples y fáciles de auditar. Toda la funcionalidad adicional que requieren los traders puede ser proporcionada por los contratos periféricos.

Flujos de datos y de control

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

  1. Intercambiar entre diferentes tókenes
  2. Añadir liquidez al mercado y ser recompensado con tókenes de liquidez ERC-20 de intercambio de pares
  3. Quemar tókenes de liquidez ERC-20 y recuperar los tókenes ERC-20 que el intercambio de pares permite que los traders intercambien

Intercambio

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

Emisor

  1. Proporcionar a la cuenta periférica una asignación por el importe que se va a intercambiar.
  2. Llamar a una de las muchas funciones de intercambio del contrato periférico (cuál dependerá de si hay ETH involucrado o no, si el trader especifica la cantidad de tókenes a depositar o la cantidad de tókenes a recuperar, etc.). Cada función de intercambio acepta una path (ruta), una matriz de intercambios por los que pasar.

En el contrato periférico (UniswapV2Router02.sol)

  1. Identificar las cantidades que se deben negociar en cada intercambio a lo largo de la ruta.
  2. Iterar sobre la ruta. Por cada intercambio en el camino, envía el token de entrada y luego llama a la función swap del intercambio. En la mayoría de los casos, la dirección de destino de los tókenes es el siguiente intercambio de par en la ruta. En el intercambio final, es la dirección proporcionada por el trader.

En el contrato principal (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2}5. Verificar que el contrato principal no está siendo engañado y que puede mantener suficiente liquidez después del intercambio.

  1. Ver cuántos tókenes adicionales tenemos además de las reservas conocidas. Esa cantidad es el número de tókenes de entrada que recibimos para intercambiar.
  2. Enviar los tókenes de salida al destino.
  3. Llamar a _update para actualizar las cantidades de la reserva

De vuelta en el contrato periférico (UniswapV2Router02.sol)

  1. Realizar cualquier limpieza necesaria (por ejemplo, quemar tókenes WETH para recuperar ETH y enviarlos al trader).

Añadir liquidez

Emisor

  1. Proporcionar a la cuenta periférica una asignación en las cantidades que se añadirán al fondo de liquidez.
  2. Llamar a una de las funciones addLiquidity del contrato periférico.

En el contrato periférico (UniswapV2Router02.sol)

  1. Crear un nuevo intercambio de pares si es necesario.
  2. Si existe un intercambio de pares, calcular la cantidad de tókenes a añadir. Se supone que este es un valor idéntico para ambos tókenes, por lo que se mantiene la misma proporción de tókenes nuevos con respecto a los existentes.
  3. Comprobar si las cantidades son aceptables (los emisores pueden especificar una cantidad mínima por debajo de la cual prefieren no añadir liquidez).
  4. Llamar al contrato principal.

En el contrato principal (UniswapV2Pair.sol)

  1. Acuñar tókenes de liquidez y enviarlos al emisor
  2. Llamar a _update para actualizar las cantidades de la reserva

Retirar liquidez

Emisor

  1. Proporcionar a la cuenta periférica una asignación de tókenes de liquidez para ser quemados a cambio de los tókenes subyacentes.
  2. Llamar a una de las funciones removeLiquidity del contrato periférico.

En el contrato periférico (UniswapV2Router02.sol)

  1. Enviar los tókenes de liquidez al intercambio de pares.

En el contrato principal (UniswapV2Pair.sol)

  1. Enviar a la dirección de destino los tókenes subyacentes en proporción a los tókenes quemados. Por ejemplo, si hay 1000 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 que devolvemos al usuario 100 tókenes A y 50 tókenes B.
  2. Quemar los tókenes de liquidez.
  3. Llamar a _update para actualizar las cantidades de la reserva

Los contratos principales

Estos son los contratos seguros que contienen la liquidez.

UniswapV2Pair.sol

Este contratoopens in a new tab implementa la reserva real que intercambia tókenes. Es la funcionalidad principal 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

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.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

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

1 using SafeMath for uint;

La librería SafeMathopens in a new tab se utiliza para evitar desbordamientos y subdesbordamientos. 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.

1 using UQ112x112 for uint224;

Muchos cálculos en el contrato de la 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 entera y 112 bits para la fracción. Por lo tanto, 1.0 se representa como 2^112, 1.5 se representa como 2^112 + 2^111, etc.

Hay más detalles sobre esta librería más adelante en el documento.

Variables

1 uint public constant MINIMUM_LIQUIDITY = 10**3;

Para evitar casos de división por cero, hay un número mínimo de tókenes de liquidez que siempre existen (pero son propiedad de la cuenta cero). Ese número es MINIMUM_LIQUIDITY, mil.

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

Este es el selector de ABI para la función de transferencia de ERC-20. Se utiliza para transferir tókenes ERC-20 en las dos cuentas de tókenes.

1 address public factory;

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

1 address public token0;
2 address public token1;

Son las 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

Las reservas que la reserva tiene para cada tipo de token. Asumimos que los dos representan la misma cantidad de valor y, por lo tanto, cada token0 vale reserve1/reserve0 de token1.

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

La marca de tiempo del último bloque en el que se produjo 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 256 bits de longitud. Por lo tanto, tres variables, reserve0, reserve1 y blockTimestampLast, se asignan de tal manera que un único valor de almacenamiento puede incluirlas a las tres (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;

Estas variables contienen los costes acumulados para cada token (cada uno en términos del otro). Se pueden utilizar para calcular el tipo de cambio promedio durante un período de tiempo.

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

La forma en que el intercambio de pares decide el tipo de cambio between 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 tókenes, y aumenta ligeramente debido a la comisión de mercado del 0,3 %.

Aquí tiene un ejemplo sencillo. Tenga en cuenta que, para simplificar, la tabla solo tiene tres dígitos después del punto decimal e ignoramos la comisión de negociación del 0,3 %, por lo que los números no son precisos.

Eventoreserva0reserva1reserva0 * reserva1Tipo de cambio promedio (token1 / token0)
Configuración inicial1000,0001000,0001.000.000
El trader A intercambia 50 token0 por 47,619 token11050,000952,3811.000.0000,952
El trader B intercambia 10 token0 por 8,984 token11060,000943,3961.000.0000,898
El trader C intercambia 40 token0 por 34,305 token11100,000909,0901.000.0000,858
El trader D intercambia 100 token1 por 109,01 token0990,9901009,0901.000.0000,917
El trader E intercambia 10 token0 por 10,079 token11000,990999,0101.000.0001,008

A medida que los traders proporcionan más token0, el valor relativo del token1 aumenta, y viceversa, en función de la oferta y la demanda.

Bloqueo

1 uint private unlocked = 1;

Hay una clase de vulnerabilidades de seguridad que se basan en el abuso de reentradaopens in a new tab. Uniswap necesita transferir tókenes 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 llamen funciones mientras se están ejecutando (dentro de la misma transacción).

1 modifier lock() {

Esta función es un modificadoropens in a new tab, una función que envuelve a una función normal para cambiar su comportamiento de alguna manera.

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

Si unlocked es igual a uno, se establece en cero. Si ya es cero, se revierte la llamada y se produce un fallo.

1 _;

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 ejecuta, el valor de unlocked es cero.

1 unlocked = 1;
2 }

Después de que la función principal devuelva un valor, se libera el bloqueo.

Miscelánea funciones

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

Esta función proporciona a los emisores el estado actual del intercambio. Tenga en cuenta que las funciones de Solidity pueden devolver múltiples valoresopens 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));

Esta función interna transfiere una cantidad de tókenes ERC-20 del intercambio a otra persona. SELECTOR especifica que la función 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 usando una de las funciones de ABIopens in a new tab.

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

Hay dos formas en que una llamada de transferencia de ERC-20 puede informar de un fallo:

  1. Revertir. Si una llamada a un contrato externo se revierte, el valor de retorno booleano es false
  2. Terminar normalmente pero informar de un fallo. En ese caso, el búfer de valor de retorno tiene una longitud distinta de cero y, cuando se decodifica como un valor booleano, es false

Si se da alguna de estas condiciones, revertir.

Eventos

1 event Mint(address indexed sender, uint amount0, uint amount1);
2 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 forman parte del evento, así como la identidad de la cuenta que nos llamó (sender). En el caso de una retirada, el evento también incluye el 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 );

Este evento se emite cuando un trader intercambia un token por otro. Una vez más, el emisor y el destino pueden no ser los mismos. Cada token puede enviarse al intercambio o recibirse de él.

1 event Sync(uint112 reserve0, uint112 reserve1);

Finalmente, Sync se emite cada vez que se agregan o retiran tókenes, 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 deben llamar una vez cuando se configura el nuevo intercambio de pares.

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

El constructor se asegura de que hagamos un seguimiento de la dirección de la fábrica que creó el par. Esta información es necesaria para initialize y para la comisión de la fábrica (si existe)

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 }

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

Funciones de actualización internas

_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 {

Esta función se llama cada vez que se depositan o retiran tókenes.

1 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 la _actualización para evitar desbordamientos. Con un token normal que se puede subdividir en 10^18 unidades, esto significa que cada intercambio está limitado a unos 5.1*10^15 de cada token. Hasta ahora eso no ha sido un 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) {

Si el tiempo transcurrido no es cero, significa que somos la primera transacción de intercambio 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 }

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

Eventoreserva0reserva1marca de tiempoTipo de cambio marginal (reserva1 / reserva0)price0CumulativeLast
Configuración inicial1000,0001000,00050001,0000
El trader A deposita 50 token0 y recibe 47,619 token1 de vuelta1050,000952,38150200,90720
El trader B deposita 10 token0 y recibe 8,984 token1 de vuelta1060,000943,39650300,89020+10*0,907 = 29,07
El trader C deposita 40 token0 y recibe 34,305 token1 de vuelta1100,000909,09051000,82629,07+70*0,890 = 91,37
El trader D deposita 100 token1 y recibe 109,01 token0 de vuelta990,9901009,09051101,01891,37+10*0,826 = 99,63
El trader E deposita 10 token0 y recibe 10,079 token1 de vuelta1000,990999,01051500,99899,63+40*1,1018 = 143,702

Supongamos que queremos calcular el precio promedio de Token0 entre las marcas de tiempo 5030 y 5150. 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 precios es la razón por la que necesitamos conocer los tamaños de reserva antiguos.

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

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) {

En Uniswap 2.0 los traders pagan una comisión del 0,30 % por usar el mercado. La mayor parte de esa comisión (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, que paga a Uniswap por su esfuerzo de desarrollo.

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

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

Leer el destino de la comisión 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

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 caro que el acceso a la memoria volátil que se libera cuando finaliza la llamada de función al contrato, por lo que utilizamos una variable interna para ahorrar en gas.

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

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) {

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

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

Este complicado cálculo de comisiones se explica en el libro blancoopens in a new tab en la página 5. Sabemos que entre el momento en que se calculó kLast y el presente no se añadió ni eliminó liquidez (porque ejecutamos este cálculo cada vez que se añade o elimina liquidez, antes de que cambie realmente), por lo que cualquier cambio en reserve0 * reserve1 tiene que provenir de las comisiones de transacción (sin ellas mantendríamos reserve0 * reserve1 constante).

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

Utilice la función UniswapV2ERC20._mint para crear los tókenes de liquidez adicionales y asignarlos a feeTo.

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

Si no hay ninguna comisión, establezca kLast en cero (si no lo está ya). Cuando se escribió este contrato, había una función de reembolso de gasopens in a new tab que animaba a los contratos a reducir el tamaño total 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.

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) {

Esta función se llama cuando un proveedor de liquidez añade liquidez a la reserva. Se acuñan tókenes de liquidez adicionales como recompensa. Debe llamarse desde un contrato periférico que lo llama 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 propietario legítimo).

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

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.

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);

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

1 bool feeOn = _mintFee(_reserve0, _reserve1);

Calcular las comisiones del protocolo a cobrar, si las hay, y acuñar los tókenes de liquidez correspondientes. Como los parámetros de _mintFee son los valores antiguos de la reserva, la comisión se calcula con precisión basándose únicamente en los cambios de la reserva debidos a las comisiones.

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

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 por cero en algunos lugares). El valor de MINIMUM_LIQUIDITY es mil, que considerando que la mayoría de los ERC-20 se subdividen en unidades de 10^-18 de un token, como el ETH se divide en wei, es 10^-15 del 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 al depositante le interesa proporcionar el mismo valor, para evitar perder valor por el arbitraje. Digamos que el valor de los dos tókenes es idéntico, pero nuestro depositante depositó cuatro veces más Token1 que Token0. Un trader puede utilizar el hecho de que el intercambio de pares piense que el Token0 es más valioso para extraer valor de él.

Eventoreserva0reserva1reserva0 * reserva1Valor de la reserva (reserva0 + reserva1)
Configuración inicial83225640
El trader deposita 8 tókenes Token0 y recibe 16 Token1161625632

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

1 } else {
2 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 tókenes de liquidez en función del valor menor que proporcionaron como castigo.

Tanto si se trata del depósito inicial como de 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 valores iguales de ambos tipos, en cuyo caso la «multa» se distribuye). Aquí hay otro ejemplo con dos tókenes 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).

Eventoreserva0reserva1reserva0 * reserva1Valor de la reserva (reserva0 + reserva1)Tókenes de liquidez acuñados para este depósitoTotal de tókenes de liquidezvalor de cada token de liquidez
Configuración inicial8,0008,0006416,000882,000
Deposite cuatro de cada tipo12,00012,00014424,0004122,000
Deposite dos de 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);

Utilice 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 }

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

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) {

Esta función se llama cuando se retira liquidez y es preciso quemar los tókenes de liquidez apropiados. También debería llamarse desde una cuenta periférica.

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)];

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 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');

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

El resto de la función burn es una imagen especular de la función mint anterior.

intercambiar
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 {

Se supone que esta función también se llama desde un contrato periférico.

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

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, usaremos menos gas. Para más detalles, consulte el libro amarillo, las especificaciones formales de Ethereumopens in a new tab, p. 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

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 tarde en la llamada, la revertimos junto con cualquier cambio que haya creado.

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

Informar al receptor sobre el intercambio si se solicita.

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

Obtenga los saldos actuales. El contrato periférico 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 que tiene que ocurrir en el contrato principal (porque podemos ser llamados por otras entidades además de nuestro contrato periférico).

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');

Esta es una comprobación para asegurarnos de que no perdemos en el intercambio. No hay ninguna circunstancia en la que un intercambio deba reducir reserve0*reserve1. Aquí es también donde nos aseguramos de que se envíe una comisión del 0,3 % en el intercambio; antes de comprobar el valor de K, multiplicamos ambos saldos por 1000 y le restamos las cantidades multiplicadas por 3, lo que significa que se deduce un 0,3 % (3/1000 = 0,003 = 0,3 %) del saldo antes de comparar su valor K con el valor K de las reservas actuales.

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

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

Sincronizar o desnatar

Es posible que los saldos reales se desincronicen con las reservas que el intercambio de pares cree que tiene. 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 llamar a mint o swap.

En ese caso hay dos soluciones:

  • sync, actualizar las reservas a los saldos actuales
  • skim, retirar la cantidad extra. Tenga en cuenta que cualquier cuenta puede llamar a skim porque no sabemos quién depositó los tókenes. Esta información se emite en un evento, pero los eventos no son accesibles 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

UniswapV2Factory.sol

Este contratoopens in a new tab crea los intercambios de pares.

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;

Estas variables de estado son necesarias para implementar la comisión del protocolo (consulte el libro blancoopens in a new tab, p. 5). La dirección feeTo acumula los tókenes de liquidez para la comisión del protocolo, y feeToSetter es la dirección autorizada a cambiar feeTo por una dirección diferente.

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

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

La primera, getPair, es una asignación 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 todos direcciones. Para obtener la dirección del intercambio de pares que le permite convertir de tokenA a tokenB, utilice getPair[<dirección de tokenA>][<dirección de tokenB>] (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 una asignación, 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 pueden iterar todas las claves de una asignación es que el almacenamiento de datos del contrato es costoso, por lo que cuanto menos lo usemos, mejor, y cuanto menos lo cambiemos , mejor. Puede crear asignaciones que admitan la iteraciónopens in a new tab, pero requieren almacenamiento adicional para una lista de claves. En la mayoría de las aplicaciones no se necesita.

1 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 tókenes, la dirección del intercambio de pares y el número total de intercambios gestionados por la fábrica.

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

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

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

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

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

Esta es la función principal de la fábrica, crear un intercambio de pares entre dos tókenes 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.

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (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 las transacciones de capa 2). Para ello necesitamos tener 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

Los fondos de liquidez grandes son mejores que los pequeños, porque tienen precios más estables. No queremos tener más de un fondo 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;

Para crear un nuevo contrato, necesitamos el código que lo crea (tanto la función constructora como el código que escribe en la memoria el código de bytes de la EVM del contrato real). Normalmente en Solidity solo usamos addr = new <nombre del contrato>(<parámetros del constructor>) 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 CREATE2opens 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 era necesario obtener el código manualmente. Esto ya no es un problema, porque Solidity ahora es compatible con CREATE2opens 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 }

Cuando un código de operación aún no es compatible con Solidity, podemos llamarlo usando ensamblado en líneaopens in a new tab.

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

Llamar a la función initialize para indicar al nuevo intercambio qué dos tókenes 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);

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.

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

Estas dos funciones permiten a feeSetter controlar el destinatario de la comisión (si lo hay) y cambiar feeSetter a una nueva dirección.

UniswapV2ERC20.sol

Este contratoopens 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 de 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 with ellos. Una solución para evitar este problema son las metatransaccionesopens in a new tab. El propietario de los tókenes firma una transacción que permite a otra persona retirar tókenes fuera de la cadena y la envía a través de Internet al destinatario. El destinatario, que sí tiene ETH, 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;

Este hash es el identificador del tipo de transacciónopens in a new tab. El único que admitimos aquí es Permit with estos parámetros.

1 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 (esto es una forma de ataque de repeticiónopens in a new tab). Para evitarlo, utilizamos un nonceopens in a new tab. Si el nonce de un nuevo Permit no es uno más que el último utilizado, asumimos que no es válido.

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

Este es el código para recuperar el identificador de cadenaopens in a new tab. Utiliza un dialecto de ensamblado de EVM llamado Yulopens in a new tab. Tenga en cuenta que en la versión actual de Yul tiene que 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

Calcular el separador de dominioopens 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 {

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

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

No acepte transacciones después de la fecha límite.

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 );

abi.encodePacked(...) es el mensaje que esperamos recibir. Sabemos cuál debe ser el nonce, por lo que no es necesario 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.

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

A partir del resumen y la firma podemos obtener la dirección que lo firmó usando ecrecoveropens in a new tab.

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

Si todo está bien, trátelo como una aprobación de ERC-20opens 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 es más complicado y podría perder valor si comete un error. Los contratos principales solo contienen pruebas para asegurarse de que no están siendo engañados, no comprobaciones de seguridad para nadie más. Estos están en la periferia para que puedan actualizarse según sea necesario.

UniswapV2Router01.sol

Este contratoopens in a new tab tiene problemas y ya no debería usarseopens in a new tab. Por suerte, los contratos periféricos no tienen estado y no contienen ningún activo, por lo que es fácil dejarlo obsoleto y sugerir que se use el reemplazo, UniswapV2Router02, en su lugar.

UniswapV2Router02.sol

En la mayoría de los casos, usaría Uniswap a través de este contratoopens in a new tab. Puede ver cómo usarlo aquíopens in a new tab.

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

La mayoría de estos los hemos encontrado antes o son bastante obvios. La única excepción es IWETH.sol. Uniswap v2 permite intercambios para cualquier par de tókenes ERC-20, pero ether (ETH) en sí mismo 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 tókenes ERC-20, la gente ideó el contrato ether envuelto (WETH)opens in a new tab. Usted envía ETH a este contrato y este le acuña una cantidad equivalente de WETH. O puede quemar WETH y recuperar ETH.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 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 inmutablesopens 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.

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

Este modificador se asegura de que las transacciones con límite de tiempo («haga X antes de la hora Y si puede») no ocurran después de su límite de tiempo.

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

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 }

Esta función se llama cuando canjeamos tókenes del contrato WETH de nuevo en ETH. Solo el contrato WETH que usamos está autorizado a hacerlo.

Añadir liquidez

Estas funciones agregan tókenes al intercambio de pares, lo que aumenta el fondo de liquidez.

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

Esta función se utiliza para calcular la cantidad de tókenes A y B que deben depositarse en el intercambio de pares.

1 address tokenA,
2 address tokenB,

Estas son las direcciones de los contratos de tókenes ERC-20.

1 uint amountADesired,
2 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 pueden depositar.

1 uint amountAMin,
2 uint amountBMin

Estas son las cantidades mínimas aceptables para depositar. Si la transacción no puede realizarse con estas cantidades o más, reviértala. Si no quiere 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 cercano al actual. Si el tipo de cambio fluctúa demasiado, podría significar noticias que cambien los valores subyacentes, y querrán decidir manualmente qué hacer.

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

ParámetroValor
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Mientras el tipo de cambio se mantenga entre 0,9 y 1,25, la transacción se realiza. 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 de gas sea muy bajo, en cuyo caso necesitará enviar otra transacción con el mismo nonce y un precio de gas más alto para sobrescribirla). No puede controlar lo que sucede durante el intervalo entre el envío y la inclusión.

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

La función devuelve las cantidades que el proveedor de liquidez debe depositar para tener una relación igual a la relación actual entre las 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 }

Si aún no existe un intercambio para este par de tókenes, créelo.

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

Obtenga las reservas actuales en el par.

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

Si las reservas actuales están vacías, este es un nuevo intercambio de pares. Las cantidades a depositar deben ser exactamente las mismas que las que el proveedor de liquidez desea proporcionar.

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

Si necesitamos ver qué cantidades habrá, obtenemos la cantidad óptima usando esta funciónopens in a new tab. Queremos la misma proporción que las reservas actuales.

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

Si amountBOptimal es menor que la cantidad que el proveedor de liquidez quiere depositar, significa que el token B es actualmente más valioso de lo que cree el depositante de liquidez, 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);

Si la cantidad óptima de B es mayor que la cantidad deseada de B, significa que los tókenes B son actualmente menos valiosos de lo que cree 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 tókenes A para la cantidad deseada de tókenes B.

Al juntar todo, obtenemos este gráfico. Supongamos 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, tienen el mismo valor y deposita mil de cada uno. Si x=2, A vale el doble que B (se obtienen dos tókenes B por cada token A), por lo que se depositan 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.

Gráfico

Podría depositar liquidez directamente en el contrato principal (usando UniswapV2Pair::mintopens in a new tab), pero el contrato principal solo comprueba 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 utiliza el contrato periférico, este calcula la cantidad que debe depositar y la deposita 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

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 anterior, con dos excepciones:

. to es la dirección que obtiene los nuevos tókenes de liquidez acuñados para mostrar la parte del fondo del proveedor de liquidez . deadline es un límite de tiempo en 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);

Calculamos las cantidades a depositar realmente y luego encontramos la dirección del fondo de liquidez. Para ahorrar gas no lo hacemos preguntando a la fábrica, sino usando la función de librería pairFor (ver más abajo en librerías)

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

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

1 liquidity = IUniswapV2Pair(pair).mint(to);

A cambio, dé a la dirección to tókenes de liquidez para la propiedad parcial de la reserva. 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 acuña liquidez en consecuencia.

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

Cuando un proveedor de liquidez quiere proporcionar liquidez a un intercambio de pares Token/ETH, hay algunas diferencias. El contrato gestiona el empaquetado del ETH para el proveedor de liquidez. No es necesario especificar cuántos ETH quiere depositar el usuario, porque este simplemente 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

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 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 }

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.

Retirar liquidez

Estas funciones eliminarán la liquidez y la 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

El caso más sencillo de eliminación de 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);

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

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

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);

Traducir 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 }

Se puede hacer la transferencia primero y luego verificar que es legítima, porque si no lo es, se revertirán 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

Eliminar liquidez para ETH es casi lo mismo, excepto que recibimos los tókenes WETH y luego los canjeamos por ETH para devolverlos 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

Estas funciones retransmiten metatransacciones para permitir que los usuarios sin ether se retiren de la reserva, 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

Esta función se puede utilizar para tókenes que tienen comisiones de transferencia o almacenamiento. Cuando un token tiene tales comisiones 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

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

Operar

1 // **** INTERCAMBIO ****
2 // requiere que la cantidad inicial ya haya sido enviada al primer par
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {

Esta función realiza el procesamiento interno necesario para las funciones que se exponen a los operadores.

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

Mientras escribo esto, hay 388.160 tokens ERC-20opens in a new tab. Si hubiera un intercambio de pares por cada par de tokens, habría más de 150 mil millones de intercambios de pares. La cadena entera, por el momento, solo tiene el 0,1 % de ese número de cuentasopens in a new tab. En su lugar, las funciones de intercambio soportan el concepto de una ruta. Un operador puede intercambiar A por B, B por C y C por D, por lo que no hay necesidad de un intercambio de pares A-D directo.

Los precios en estos mercados tienden a estar sincronizados, porque cuando no están sincronizados, se crea una oportunidad de arbitraje. Imagine, por ejemplo, tres tokens, A, B y C. Hay tres intercambios de pares, uno para cada par.

  1. La situación inicial
  2. Un operador vende 24,695 tokens A y obtiene 25,305 tokens B.
  3. El operador vende 24,695 tokens B por 25,305 tokens C y se queda con aproximadamente 0,61 tokens B como ganancia.
  4. Luego, el operador vende 24,695 tokens C por 25,305 tokens A y se queda con aproximadamente 0,61 tokens C como ganancia. El operador también tiene 0,61 tokens A adicionales (los 25,305 que obtiene al final, menos la inversión original de 24,695).
PasoIntercambio A-BIntercambio B-CIntercambio A-C
1A:1000 B:1050 A/B=1,05B:1000 C:1050 B/C=1,05A:1050 C:1000 C/A=1,05
2A:1024,695 B:1024,695 A/B=1B:1000 C:1050 B/C=1,05A:1050 C:1000 C/A=1,05
3A:1024,695 B:1024,695 A/B=1B:1024,695 C:1024,695 B/C=1A:1050 C:1000 C/A=1,05
4A:1024,695 B:1024,695 A/B=1B:1024,695 C:1024,695 B/C=1A:1024,695 C:1024,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];

Obtener el par que estamos manejando, ordenarlo (para usarlo con el par) y obtener la cantidad de salida esperada.

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

Obtener las cantidades de salida esperadas, ordenadas de la forma en que el intercambio de pares espera que estén.

1 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 es así, 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 }

Llamar realmente al intercambio de pares para intercambiar los tokens. No necesitamos una retrollamada que nos informe sobre el intercambio, por lo que no enviamos ningún byte en ese campo.

1 function swapExactTokensForTokens(

Esta función es utilizada directamente por los operadores para intercambiar un token por otro.

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

Este parámetro contiene las direcciones de los contratos ERC-20. Como se explicó anteriormente, esto es una matriz porque es posible que deba pasar por varios intercambios de pares para llegar desde el activo que tiene al activo que desea.

Un parámetro de función en Solidity se puede almacenar en memory o en calldata. Si la función es un punto de entrada al contrato, llamada directamente por un usuario (mediante una transacción) o desde un contrato diferente, el valor del parámetro puede tomarse directamente de los datos de la llamada. Si se llama a la función internamente, como _swap más arriba, 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 se encarga de elegir el almacenamiento por nosotros, pero con las matrices, que son más largas y costosas, especificamos el tipo de almacenamiento que se va a utilizar.

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

Los valores de retorno siempre se devuelven en la memoria.

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

Calcular la cantidad a comprar en cada intercambio. Si el resultado es menor que el mínimo que el operador está dispuesto a aceptar, se revierte la transacción.

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

Finalmente, transfiera el token ERC-20 inicial a la cuenta del 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.

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

La función anterior, swapTokensForTokens, permite a un operador 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 realiza el intercambio inverso: permite a un operador 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 operador debe primero dar a este contrato de periferia una autorización para permitirle transferirlos.

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

Estas cuatro variantes implican el intercambio entre ETH y tokens. La única diferencia es que o bien recibimos ETH del operador y lo usamos para acuñar WETH, o recibimos WETH del último intercambio en la ruta y lo quemamos, devolviendo al operador el ETH resultante.

1 // **** INTERCAMBIO (admite tokens con comisión por transferencia) ****
2 // requiere que la cantidad inicial ya haya sido enviada al primer par
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Esta es la función interna para intercambiar tokens que tienen tarifas de transferencia o de almacenamiento para resolver (este problemaopens 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 { // el ámbito para evitar errores de pila demasiado profunda
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

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 al _swap original). En su lugar, tenemos que transferir primero y luego ver cuántos tokens recibimos.

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, así que, aunque tenemos que darles soporte, no es necesario 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

Estas son las mismas variantes que se utilizan para los tokens normales, pero en su lugar llaman a _swapSupportingFeeOnTransferTokens.

1 // **** FUNCIONES DE BIBLIOTECA ****
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

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

UniswapV2Migrator.sol

Este contrato se utilizó para migrar los intercambios de la antigua v1 a la v2. Ahora que han sido migrados, ya no es relevante.

Las bibliotecas

La biblioteca SafeMathopens in a new tab está bien documentada, así 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.

1pragma solidity =0.5.16;
2
3// una biblioteca para realizar varias operaciones matemáticas
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // método babilónico (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

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;

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 inferior a la existente. Para más detalles, consulte aquíopens in a new tab.

1 }
2 } else if (y != 0) {
3 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).

1 }
2 }
3}

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

1pragma solidity =0.5.16;
2
3// una biblioteca para manejar números binarios de punto fijo (https://wikipedia.org/wiki/Q_(number_format))
4
5// rango: [0, 2**112 - 1]
6// resolución: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Mostrar todo

Q112 es la codificación para uno.

1 // codificar un uint112 como UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // nunca se desborda
4 }

Como y es uint112, lo máximo que puede ser es 2^112-1. Ese número todavía se puede codificar como un UQ112x112.

1 // divide un UQ112x112 por un uint112, devolviendo un UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}

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 // devuelve las direcciones de los tokens ordenadas, que se usan para manejar los valores de retorno de los pares ordenados en este orden
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

Ordene los dos tokens 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 // calcula la dirección CREATE2 para un par sin realizar ninguna llamada externa
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' // hash del código de inicialización
9 ))));
10 }
Mostrar todo

Esta función calcula la dirección del intercambio de pares para los dos tokens. Este contrato se crea utilizando el código de operación CREATE2opens 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

1 // obtiene y ordena las reservas de un par
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 }

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

1 // dada una cantidad de un activo y las reservas de un par, devuelve una cantidad equivalente del otro activo
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 }

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 // dada una cantidad de entrada de un activo y las reservas de un par, devuelve la cantidad máxima de salida del otro activo
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

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 }

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 1000, logrando el mismo efecto.

1 // dada una cantidad de salida de un activo y las reservas de un par, devuelve la cantidad de entrada requerida del otro activo
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 }

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

1
2 // realiza cálculos encadenados de getAmountOut en cualquier número de pares
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 // realiza cálculos encadenados de getAmountIn en cualquier número de pares
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

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

TransferHelper

Esta bibliotecaopens in a new tab agrega comprobaciones de éxito en torno a las transferencias de ERC-20 y Ethereum para tratar una reversión y la devolución de un valor false de la misma manera.

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// métodos de ayuda para interactuar con tokens ERC20 y enviar ETH que no devuelven consistentemente verdadero/falso
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

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 }

En aras de la compatibilidad con versiones anteriores de tokens creados antes del estándar ERC-20, una llamada a ERC-20 puede fallar, ya sea revirtiendo (en cuyo caso success es false) o teniendo éxito y devolviendo un valor false (en cuyo caso hay datos de salida, y si los 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

Esta función implementa la funcionalidad de transferencia de ERC-20opens in a new tab, que permite a una cuenta gastar la autorizació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

Esta función implementa la funcionalidad transferFrom de ERC-20opens in a new tab, que permite a una cuenta gastar la autorizació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}

Esta función transfiere ether a una cuenta. Cualquier llamada a un contrato diferente puede intentar enviar ether. Como 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 ha llegado hasta aquí, ¡enhorabuena! Esperamos que a estas alturas haya entendido las consideraciones a la hora de escribir una aplicación real (a diferencia de los programas de ejemplo cortos) y esté mejor preparado para escribir contratos para sus propios casos de uso.

Ahora, vaya y escriba algo útil y sorpréndanos.

Vea aquí más de mi trabajoopens in a new tab.

Última actualización de la página: 1 de octubre de 2025

¿Le ha resultado útil este tutorial?