La función swap de Uniswap V2 está diseñada de manera inteligente, pero muchos desarrolladores encuentran su lógica poco intuitiva la primera vez que se topan con ella. Este artículo explica a fondo cómo funciona.
A continuación, se reproduce el código:

Es cierto que esto es un muro de código, pero vamos a desglosarlo.
-
En las líneas 170-171 (indicadas con un recuadro amarillo), la función transfiere directamente la cantidad de tokens que el trader solicitó en los argumentos de la función. No hay ningún lugar dentro de la función donde se transfieran tokens hacia adentro. Revisa el código y mira si puedes encontrar dónde se transfieren los tokens hacia adentro; no existe. ¡Pero esto no significa que simplemente podamos llamar a swap y vaciar todos los tokens que queramos!
-
La razón por la que podemos retirar tokens de inmediato es para poder realizar flash loans. Por supuesto, la declaración require en la línea 182 (flecha naranja) requerirá que devolvamos el flash loan con intereses.
-
En la parte superior de la función, hay un comentario que dice que la función debe ser llamada desde otro smart contract que implemente importantes controles de seguridad. Eso significa que a esta función en particular le faltan controles de seguridad (subrayado rojo). Querremos determinar cuáles son.
-
Las variables _reserve0 y _reserve1 (subrayado azul) se leen en las líneas 161, 176-177 y 182, pero no se escriben en esta función.
-
La línea 182 (flecha naranja) no verifica estrictamente si X × Y = K. Verifica si balance1Adjusted × balance2Adjusted ≥ K. Esta es la única declaración require que hace algo “interesante”. Las otras declaraciones require verifican que los valores no sean cero o que no estés enviando los tokens a la dirección de su propio contrato.
-
balance0 y balance1 se leen directamente del balance real del contrato del par usando balanceOf de ERC20.
-
La línea 172 (debajo del recuadro amarillo) solo se ejecuta si data no está vacío, de lo contrario no se ejecuta.
Usando estas observaciones, daremos sentido a esta función una característica a la vez.
Flash Borrowing
Los usuarios no tienen que usar la función swap para intercambiar tokens, puede usarse puramente como un flash loan.

El contrato que toma el préstamo simplemente solicita la cantidad de tokens que desea tomar prestados (A) sin colateral y se transferirán al contrato (B).
Los datos que deben proporcionarse con la llamada a la función se pasan como un argumento de la función (C), y esto se pasará a una función que implemente
IUniswapV2Callee. La función uniswapV2Call debe devolver el flash loan más la tarifa o la transacción se revertirá.
Swap requiere el uso de un smart contract
Si no se utiliza un flash loan, los tokens entrantes deben enviarse como parte de la llamada a la función swap.
Debería quedar claro que solo un smart contract es capaz de interactuar con una función swap, porque una EOA no puede enviar simultáneamente los tokens ERC20 entrantes y llamar a swap en una sola transacción sin la ayuda de otro smart contract.
Midiendo la cantidad de tokens entrantes
La forma en que Uniswap V2 “mide” la cantidad de tokens enviados hacia adentro se realiza en las líneas 176 y 177, marcadas con el recuadro amarillo a continuación.

Recuerda, _reserve0 y _reserve1 no se actualizan dentro de esta función. Reflejan el balance del contrato antes de que el nuevo conjunto de tokens fuera enviado hacia adentro como parte del swap.
Una de dos cosas puede suceder para cada uno de los dos tokens en el par:
-
El pool tuvo un aumento neto en la cantidad de un token en particular.
-
El pool tuvo una disminución neta (o ningún cambio) en la cantidad de un token en particular.
La forma en que el código determina qué situación ocurrió es con la siguiente lógica:
currentContractbalanceX > _reserveX - _amountXOut
// alternatively
currentContractBalanceX > previousContractBalanceX - _amountXOut
Si mide una disminución neta, el operador ternario devuelve cero; de lo contrario, medirá la ganancia neta de tokens entrantes.
amountXIn = balanceX - (_reserveX - amountXOut)
Siempre se cumple que _reserveX > amountXOut debido a la declaración require en la línea 162.

Algunos ejemplos.
-
Supongamos que nuestro balance anterior era 10, amountOut es cero y currentBalance es 12. Eso significa que el usuario depositó 2 tokens. amountXIn será 2.
-
Supongamos que nuestro balance anterior era 10, amountOut es 7 y currentBalance es 3. amountXIn será 0.
-
Supongamos que nuestro balance anterior era 10, amountOut es 7 y currentBalance es 2. amountXIn seguirá siendo cero, no -1. Es cierto que el pool tuvo una pérdida neta de 8 tokens, pero amountXIn no puede ser negativo.
-
Supongamos que nuestro balance anterior era 10 y amountOut es 6. Si el currentBalance es 18, entonces el usuario “tomó prestados” 6 tokens pero devolvió 8 tokens.
Conclusión: amount0In y amount1In reflejarán la ganancia neta si hubo una ganancia neta para el token, y serán cero si hubo una pérdida neta de ese token.
Balanceando XY = K
Ahora que sabemos cuántos tokens envió el usuario hacia adentro, veamos cómo hacer cumplir XY = K.
El código nuevamente es

Uniswap V2 cobra un 0.3% hardcoded por swap, que es la razón por la que vemos los números 1000 y 3 en juego, pero simplifiquemos esto cambiándolo al caso donde Uniswap V2 no cobrara tarifas. Esto significa que podemos eliminar el término .sub(amountXIn.mul(3)) y no multiplicar por 1000 en las líneas 180 a 181 o por 1000**2 en la línea 182.
El nuevo código sería
require(balance0 * balance1 >= reserve0 * reserve1, "K");
Esto quiere decir:
K no es realmente constante
Es un poco engañoso decir que “K permanece constante” a pesar de que la fórmula del AMM a veces se conoce como una “fórmula de producto constante”.
Piénsalo de esta manera: si alguien donara tokens al pool y cambiara el valor de K, no querríamos detenerlos porque nos hicieron más ricos a los proveedores de liquidez, ¿verdad?
Uniswap V2 no te impide “pagar demasiado”, es decir, transferir demasiados tokens hacia adentro durante el swap (esto está relacionado con uno de los controles de seguridad, al que llegaremos más adelante).
Nos molestaríamos si hubiera una pérdida neta en el pool, que es lo que está verificando la declaración require. Si K se hace más grande, significa que el pool se hizo más grande, y como proveedores de liquidez, eso es lo que queremos.
Contabilizando las tarifas
Pero no solo queremos que K se haga más grande, queremos que se haga más grande al menos en una cantidad que aplique la tarifa del 0.3%.
Específicamente, la tarifa del 0.3% se aplica al tamaño de nuestro trade, no al tamaño del pool. Solo se aplica a los tokens que entran, no a los tokens que salen. Algunos ejemplos:
-
Supongamos que ponemos 1000 de token0 y retiramos 1000 de token1. Necesitaríamos pagar una tarifa de 3 en token0 y ninguna tarifa en token1.
-
Supongamos que tomamos prestados 1000 de token0 y no tomamos prestado token1. Tendremos que volver a poner 1000 de token0 adentro, y tendremos que pagar una tarifa del 0.3% sobre eso — 3 de token0.
Observa que si tomamos un préstamo flash (flash borrow) de uno de los tokens, resulta en la misma tarifa que hacer un swap de ese token por la misma cantidad. Pagas tarifas por los tokens que entran, no por los tokens que salen. Pero si no introduces tokens, no hay forma de que puedas pedir prestado o hacer swap.
Recuerda, reserve0 y reserve1 representan los balances antiguos, y balance0 y balance1 representan los balances actualizados.
Con esto en mente, el código a continuación debería ser autoexplicativo. La multiplicación por 1000 y 3 es simplemente para lograr una multiplicación “fraccionaria”, ya que se anula al final.

El código está logrando la siguiente fórmula:
Es decir, el nuevo balance debe aumentar en un 0.3% de la cantidad entrante (amount in). En el código, la fórmula se escala multiplicando cada término por 1,000 porque Solidity no tiene números de punto flotante, pero la fórmula matemática muestra lo que el código intenta lograr.
Actualizando las Reservas
Ahora que el trade está completo, el “balance anterior” debe ser reemplazado por el balance actual. Esto ocurre en la llamada a la función _update() al final de swap().

La función _update()

Hay mucha lógica aquí para manejar el oráculo TWAP, pero todo lo que nos importa por ahora son las líneas 82 y 83, donde las variables de almacenamiento (storage variables) reserve0 y reserve1 se actualizan para reflejar los balances modificados. Los argumentos _reserve0 y _reserve1 se usan para actualizar el oráculo, pero no se almacenan.
Controles de Seguridad
Hay dos cosas que pueden salir mal:
-
No se obliga a que el amountIn sea óptimo, por lo que el usuario podría pagar de más por el swap.
-
AmountOut no tiene flexibilidad, ya que se suministra como un argumento de parámetro. Si resulta que el amountIn no es suficiente en relación con el amountOut, la transacción se revertirá y se desperdiciará gas.
Estas circunstancias pueden ocurrir si alguien hace frontrunning a una transacción (intencionalmente o no) y cambia la proporción de activos en el pool en una dirección indeseable.
Aprende más con RareSkills
Este artículo es parte de nuestro Solidity Bootcamp avanzado. Por favor, consulta el plan de estudios para aprender más.
Publicado originalmente el 28 de octubre de 2023