Añadir liquidez a un AMM significa depositar tokens en el pool del AMM. Los proveedores de liquidez hacen esto con la esperanza de ganar comisiones de los usuarios que realizan intercambios con ese pool.
En Uniswap v2, cuando un LP añade liquidez, recibe participaciones del pool en forma de tokens de Proveedor de Liquidez (tokens LP), que representan el porcentaje de tokens en el pool a los que tienen derecho, incluidas las comisiones. Estos tokens LP son tokens ERC-20 fungibles.
En Uniswap v3, este enfoque no funciona porque el LP elige el rango donde quiere depositar la liquidez: los ticks inferior y superior. Por lo tanto, el protocolo necesita rastrear cada depósito de forma individual y de manera no fungible. Esto nos lleva al concepto de posiciones.
Cuando un LP añade liquidez en un rango, decimos que abre o modifica una posición. Se abre cuando la posición aún no existe, y se modifica cuando ya existe.
Un rango consta de dos ticks: un tick inferior y un tick superior. Depositar liquidez en un rango significa aumentar las reservas reales dentro de ese rango. Esto se logra utilizando la función mint en UniswapV3Pool.sol, cuya interfaz se muestra a continuación.
// UniswapV3Pool.sol
function mint(
address recipient, // the owner of the position
int24 tickLower,
int24 tickUpper,
uint128 amount, // amount in liquidity
bytes calldata data // will be explained in a future chapter, it is not necessary for our discussion
) external override lock returns (uint256 amount0, uint256 amount1) {
El nombre mint de esta función recuerda a Uniswap v2, donde añadir liquidez acuñaba tokens ERC-20 para el proveedor de liquidez. Aunque esto ya no ocurre en v3, el nombre se ha conservado, esta vez para referirse a la acuñación de una posición.
El objetivo de este capítulo es examinar cómo y dónde almacena el protocolo la información sobre estas posiciones.
El mapping positions
Las posiciones se almacenan en un mapping llamado positions, ubicado en el contrato UniswapV3Pool, como se muestra en la siguiente imagen.

La clave que identifica una posición está formada por el hash Keccak de la dirección del propietario de la posición, el tick inferior y el tick superior.
Por lo tanto, si esta es la primera vez que se deposita liquidez entre los ticks -10 y 10 para el propietario 0xA, se creará una posición con la clave keccak(0xA, -10, 10).
A continuación se muestra un código en Solidity utilizado para generar la clave de una posición.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Position {
function getKey(
address owner,
int24 tickLower,
int24 tickUpper)
public pure returns (bytes32 key) {
key = keccak256(abi.encodePacked(owner, tickLower, tickUpper));
}
}
Una vez creada, si se deposita (o retira) más liquidez entre los ticks -10 y 10 para el propietario 0xA, la posición será modificada, ya que ya existe.
El valor del mapping es de tipo Position.Info, que es un struct llamado Info ubicado en la biblioteca Position dentro del contrato Position.sol.
Este struct, que se muestra a continuación, almacena la liquidity de la posición (recuadro naranja), así como otros cuatro campos relacionados con las comisiones y los tokens que posee la posición (recuadro verde). Pospondremos la discusión sobre estos otros campos hasta que cubramos las comisiones y el retiro de liquidez de una posición.

Por ahora, podemos pensarlo de esta manera: si un determinado propietario añade liquidez dentro de un rango, se crea una posición. Después de eso, esta posición se puede modificar añadiendo o eliminando liquidez.
Las posiciones son no fungibles
En Uniswap v2, cuando un LP provee liquidez, se acuñan tokens LP ERC-20 para el LP. Estos tokens LP son fungibles y representan una participación en los activos del pool. Esto significa que, si dos LP poseen 1000 tokens LP cada uno y usan sus tokens LP para canjear sus participaciones en el pool, los tokens de activos que recibirán serán los mismos.
En Uniswap v3, las posiciones son no fungibles. Esto significa que si dos LP tienen posiciones diferentes y usan estas posiciones para canjear sus participaciones en el pool, probablemente no recibirán la misma cantidad de activos, ya que es probable que no representen el mismo rango de ticks o las reservas reales que los LP aportaron entre esos ticks.
Es posible manejar estas posiciones no fungibles como tokens no fungibles ERC-721 a través de un contrato periférico, pero el contrato core en sí no hace esto: no es un contrato ERC-721 y no permite transferir posiciones a terceros. El contrato core solo permite abrir y modificar posiciones.
Comisiones en Uniswap v3
Las comisiones en Uniswap v3 también funcionan de manera muy diferente a las de Uniswap v2. En Uniswap v2, cada vez que ocurre un intercambio, las comisiones se añaden al pool, aumentando la cantidad de tokens a los que tiene derecho cada participación.
En Uniswap v3, una posición solo tiene derecho a las comisiones de los intercambios que ocurrieron (parcial o totalmente) dentro de su rango de ticks. Por lo tanto, si un LP abre una posición que nunca está involucrada en un intercambio, esa posición no gana comisiones. Esto incentiva a los LP a abrir posiciones en regiones donde es probable que ocurran intercambios, aumentando así la liquidez donde más se necesita.
Dado que las comisiones ya no se comparten proporcionalmente entre todos los LP, sino que se atribuyen de forma individual a cada posición, deben rastrearse por separado.
La forma en que el protocolo calcula a cuántas de las comisiones tiene derecho una posición no es sencilla y conlleva una combinación de varias variables, incluidas las variables feeGrowthInside0LastX128 y feeGrowthInside1LastX128 presentes en el struct Info de la biblioteca Position.
Como explicación de alto nivel, el protocolo hace un seguimiento de las comisiones acumuladas en el pool desde su creación, así como de cuántas comisiones se recolectaron por debajo y por encima de cada tick que sirve como límite de una posición.
Esto permite al protocolo calcular qué parte de las comisiones acumuladas se recolectaron entre los ticks inferior y superior de una posición y utilizar las variables feeGrowthInside0LastX128 y feeGrowthInside1LastX128 de la posición para determinar qué cantidad de estas comisiones pertenece a esa posición en particular.
Los detalles de cómo el protocolo logra esto se cubrirán en futuros capítulos.
Un pool está compuesto por varias posiciones
Hemos estado diciendo que un pool consta de segmentos, por lo que necesitamos conectar la idea de segmentos y posiciones. No son lo mismo, ya que las posiciones pueden superponerse. Exploremos esto a través de un ejemplo.
Consideremos solo dos posiciones:
- Entre los ticks -10 y 5 con una liquidez de 200, que se muestra en el recuadro rojo a continuación.
- Entre los ticks 0 y 10 con una liquidez de 100, ilustrada en el recuadro azul a continuación.

- Entre los ticks -10 y 0, solo hay una posición con una liquidez de 200, por lo que la liquidez para este segmento será de 200.
- Entre los ticks 0 y 5, se superponen dos posiciones: una con una liquidez de 200 y la otra con una liquidez de 100, por lo que la liquidez para este segmento será de 300.
- Entre los ticks 5 y 15, solo hay una posición con una liquidez de 100, por lo que la liquidez para este segmento será de 100.
Los segmentos se muestran en la figura de arriba a la derecha.
A continuación hay una herramienta interactiva donde el lector puede crear múltiples posiciones, y la herramienta calcula automáticamente los segmentos basándose en estas posiciones. También es posible cambiar el precio actual y ver la liquidez del segmento en el que se encuentra el precio actual.
Sin embargo, el protocolo NO calcula todos los segmentos en función de las posiciones, como acabamos de hacer. Esto sería altamente ineficiente, ya que el protocolo no necesita una visión global de este tipo.
En un capítulo posterior, discutiremos cómo el protocolo maneja eficientemente las posiciones para calcular los segmentos. Por ahora, tratamos temporalmente ese cálculo como una caja negra.
En la siguiente sección, echaremos un vistazo más de cerca a la función mint y lo que el LP debe depositar para abrir una posición.
La función mint
Para añadir liquidez a un pool, es necesario depositar tokens. Como hemos visto, esto se hace a través de la función mint, cuya interfaz se muestra nuevamente a continuación.
function mint(
address recipient, // the owner of the position
int24 tickLower,
int24 tickUpper,
uint128 amount, // amount in liquidity
bytes calldata data // will be explained in a future chapter, it is not necessary for our discussion
) external override lock returns (uint256 amount0, uint256 amount1) {
Esta función espera cinco parámetros:
- La dirección del propietario de la posición (
recipient), el tick inferior (tickLower) y el tick superior (tickUpper) de la posición. - El parámetro
data, de tipobytes, se explicará más adelante y no es importante para la discusión actual. - El parámetro
amount, que es la cantidad de liquidez que el LP quiere añadir al pool entre el tick inferior y superior.
Nota que amount es de tipo uint128, lo que significa que no puede ser negativo. La función mint se utiliza solo para añadir liquidez, no para retirarla; los retiros son manejados por la función burn, que se discutirá más adelante.
La cantidad de tokens que necesitan ser depositados para añadir este amount de liquidez entre los ticks inferior y superior debe ser calculada entonces por la función mint.
Esto es lo que veremos a continuación.
Tokens requeridos para abrir una posición
La cantidad de tokens correspondiente a la liquidez entre un tick inferior y uno superior son las reservas reales del segmento correspondiente a esa posición, es decir, un segmento con liquidez entre el tick inferior y superior.
Ya hemos aprendido que las reservas reales de un segmento dependen no solo de los límites de los ticks y la liquidez , sino también del precio actual. La regla es la siguiente:
- Cuando el precio actual es igual o superior al tick superior, el segmento tiene reservas reales solo en tokens Y.
- Cuando el precio actual es igual o inferior al tick inferior, el segmento tiene reservas reales solo en tokens X.
- Cuando el precio actual está entre los ticks inferior y superior, el segmento tiene reservas reales en ambos tokens X e Y.
Estos tres escenarios se ilustran en la siguiente figura, donde el rayo rojo representa el precio actual , representa el tick inferior y representa el tick superior.

Si el LP quiere añadir liquidez entre el tick inferior y el tick superior , el protocolo calcula (reservas reales en tokens X) y (reservas reales en tokens Y) de acuerdo con los tres escenarios descritos anteriormente.
- Para el escenario 1, las reservas reales en tokens Y son
- Para el escenario 2, las reservas reales en tokens X son
- Para el escenario 3, las reservas reales en tokens X e Y son
Estas cantidades de tokens requeridas para añadir liquidez a la posición son calculadas y devueltas por la función mint, como se muestra en el recuadro rojo a continuación.

Sin embargo, para el usuario final, la liquidez puede ser un concepto muy abstracto. Es mucho más común que el usuario final piense en términos de tokens en lugar de liquidez; por ejemplo, un usuario podría querer proveer liquidez depositando 100 tokens X sin tener idea de la cantidad de liquidez que representan esos 100 tokens X.
Añadir liquidez eligiendo cantidades de tokens puede facilitarse a través de un contrato intermediario que actúe como un puente entre el usuario final y el contrato core, y que calcule la conversión entre cantidades de tokens y liquidez.
Abrir una posición a través de un Position Manager
La función mint en el contrato core está pensada para ser llamada por otro contrato y NO por EOAs.
Cuando se llama a la función mint, esta hace una llamada de retorno (callback) a la dirección que la invocó a través de la función uniswapV3MintCallback, como podemos ver en el fragmento de código de la función mint a continuación, resaltado en un recuadro rojo.

La dirección que llama a la función mint debe implementar la función uniswapV3MintCallback, ya que es aquí cuando el invocador debe transferir las cantidades de tokens requeridas para modificar la posición. La función mint verifica el balance del pool inmediatamente antes y después de llamar a uniswapV3MintCallback, y contabiliza la diferencia.
Es por eso que la función mint no puede ser llamada por un EOA: los EOAs no pueden responder al callback para transferir las cantidades de tokens requeridas. Por lo tanto, una llamada de un EOA a la función mint se revertirá si se requiere que el LP transfiera algún token, lo cual siempre es el caso (no está permitido modificar una posición añadiendo cero liquidez).
El flujo de la transacción se ilustra a continuación, asumiendo que el contrato que llama a la función mint se llama Position Manager.

El contrato core NO es fácil de usar para el usuario. En general, el LP quiere elegir la cantidad de tokens X y/o tokens Y que desea depositar para abrir una posición, en lugar de especificar la liquidez. El rol del contrato intermediario es proporcionar una interfaz amigable, donde el usuario elige una cantidad de tokens y el Position Manager la convierte en la liquidez correspondiente para esa cantidad.
Una representación de este proceso se muestra a continuación. El usuario final (un EOA) llama a la función mint en el contrato intermediario (Position Manager), pasando la cantidad de tokens que quiere transformar en liquidez como uno de los parámetros. El Position Manager luego convierte esta cantidad de tokens en liquidez y abre una posición para el usuario final llamando a la función mint en el contrato core.

En este libro no entraremos en los detalles de cómo funciona un Position Manager. Uniswap proporciona un contrato que funciona como Position Manager, llamado NonfungiblePositionManager, en su biblioteca periphery, ubicada en un repositorio diferente al de la biblioteca core.
Resumen
- Añadir liquidez en Uniswap v3 significa abrir o modificar una posición. Las posiciones están definidas por la dirección de su propietario y sus ticks inferior y superior.
- Las posiciones se almacenan en un mapping llamado
positions, cuya clave es el hash Keccak de la dirección del propietario y los ticks inferior y superior, y cuyo valor es un struct ubicado en la bibliotecaPosition. - Para abrir o añadir liquidez a una posición, se utiliza la función
mint. Esta función no es amigable para el usuario y recibe, como argumento, la cantidad de liquidez que el usuario quiere depositar entre el tick inferior y superior. - La función
mintdebe ser llamada por otros contratos, no por EOAs. Estos contratos intermediarios actúan entre los EOAs y el contrato core, y una de sus funciones puede ser convertir una cantidad de tokens definida por el LP en una cantidad de liquidez esperada por la funciónmintdel contrato core. - Para depositar liquidez entre un tick inferior y uno superior, es necesario depositar la cantidad de tokens correspondiente a las reservas reales de un segmento con liquidez entre estos ticks.