> ## Documentation Index
> Fetch the complete documentation index at: https://docs.li.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Add liquidity on Uniswap

> Add liquidity to a Uniswap V2 pair from one token and mint a Uniswap V3 position, protected by a band guard on the pool price and a slippage guard on the liquidity.

## Scenario

This page adds liquidity to the USDC/WETH pool on Ethereum twice: to the Uniswap V2 pair from USDC alone, and as a new Uniswap V3 position from USDC and WETH. The ops also work with ABI-identical forks, such as SushiSwap V2. Fee-on-transfer tokens are not supported.

The ops call the Uniswap router or position manager with zero minimums, so guards are the only protection:

* A [`band`](https://docs.li.fi/composer/composer-api/guards/band) guard on the pool price (`poolPrice` on V2, `sqrtPriceX96` on V3) reverts the transaction if the price moved, for example after a sandwich trade.
* A [`slippage`](https://docs.li.fi/composer/composer-api/guards/slippage) guard on `liquidity` reverts it if the deposit mints less than the simulated liquidity minus `bps`.

Install the SDK with `npm install @lifi/composer-sdk @lifi/compose-spec` and set `LIFI_API_KEY`; see [Before you start](https://docs.li.fi/composer/composer-api/overview.md#before-you-start).

## Add V2 liquidity from one token

A V2 deposit needs both tokens in the pool's ratio. The flow splits 2,000 USDC in half, swaps one half to WETH, and deposits both halves with `uniswapV2.addLiquidity`. Adapted from [`uniswapV2AddLiquidity.ts`](https://github.com/lifinance/composer-sdk-examples/blob/main/examples/uniswapV2AddLiquidity.ts).

```ts theme={"system"}
import {
  createComposeSdk,
  guards,
  materialisers,
  resources,
} from '@lifi/composer-sdk';

// Ethereum mainnet (chainId 1).
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const UNISWAP_V2_ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D';
const UNISWAP_V2_FACTORY = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f';
const USDC_WETH_PAIR = '0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc';

const SIGNER = '0xYourSignerAddress';

const sdk = createComposeSdk({
  baseUrl: 'https://composer.li.quest',
  apiKey: process.env.LIFI_API_KEY!,
});

const builder = sdk.flow(1, {
  name: 'uniswap-v2-add-liquidity-usdc-weth',
  inputs: {
    usdcIn: resources.erc20(USDC, 1),
  },
});

// Halve the USDC: `a` is deposited as is, `b` is swapped to WETH.
const { a: usdcHalf, b: usdcToSwap } = builder.core.split('split', {
  bind: { source: builder.inputs.usdcIn },
  config: { bps: 5000 },
});

const swap = builder.lifi.swap('swap', {
  bind: { amountIn: usdcToSwap },
  config: {
    resourceOut: resources.erc20(WETH, 1),
    slippage: 0.03, // the swap may deliver up to 3% less WETH than quoted
  },
});

builder.uniswapV2.addLiquidity('add-liquidity', {
  bind: {
    tokenA: usdcHalf,
    tokenB: swap.amountOut,
  },
  config: {
    router: UNISWAP_V2_ROUTER,
    factory: UNISWAP_V2_FACTORY,
    pair: USDC_WETH_PAIR,
  },
  guards: [
    // Revert unless the pool price is within ±50 bps of the simulated price.
    guards.band({ port: 'poolPrice', bps: 50 }),
    // Must cover the swap's 3% tolerance plus the 50 bps band.
    guards.slippage({ port: 'liquidity', bps: 350 }),
  ],
});

const result = await builder.compile({
  signer: SIGNER,
  inputs: {
    usdcIn: materialisers.directDeposit({ amount: '2000000000' }), // 2,000 USDC
  },
  // Sends the UNI-V2 LP token and any unused USDC or WETH to the signer.
  sweepTo: builder.context.sender,
  // The default price-impact check rejects this deposit; the guards protect it.
  maxPriceImpactBps: 0,
});

for (const key of ['add-liquidity.liquidity', 'add-liquidity.residualA', 'add-liquidity.residualB']) {
  const output = result.outputs[key];
  console.log(key, output?.amount.estimate?.value, output?.amount.minimum?.value);
}
```

Save it as `uniswap-v2.mts` and run `node uniswap-v2.mts`.

Two settings matter:

* **`slippage` at 350 bps, not 50.** Less WETH from the swap mints less LP. A 50 bps liquidity floor would revert a swap fill that the swap's own 3% tolerance accepts, so the floor covers 300 bps plus the 50 bps band.
* **`maxPriceImpactBps: 0`.** The compiler compares the USD value in with the USD value out and, by default, rejects anything worse than 500 bps. It values the LP token below the tokens deposited, so this deposit fails with `price_impact_exceeded` (HTTP 422). `0` disables the check; the guards are the protection.

What to check in the result:

* `outputs['add-liquidity.liquidity']` is the UNI-V2 LP token, a terminal output owned by the signer. `amount.estimate` is the simulated LP amount; `amount.minimum` (`basis: 'guard'`) is the `slippage` floor.
* `outputs['add-liquidity.residualA']` and `residualB` are the USDC and WETH the router did not use, swept to the signer. A zero residual is omitted.
* `poolPrice` is a handle, not a resource, so it has no `outputs` entry.

## Mint a V3 position

`uniswapV3.mint` opens a concentrated-liquidity position for a tick range and mints the position NFT straight to `recipient`. This example deposits 1,000 USDC and 0.5 WETH across the full range of the 0.05% pool. Adapted from [`uniswapV3Mint.ts`](https://github.com/lifinance/composer-sdk-examples/blob/main/examples/uniswapV3Mint.ts).

```ts theme={"system"}
import {
  createComposeSdk,
  guards,
  materialisers,
  resources,
} from '@lifi/composer-sdk';

// Uniswap V3 USDC/WETH 0.05% pool on Ethereum mainnet (chainId 1).
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const POSITION_MANAGER = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88';
const FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984';
const USDC_WETH_500_POOL = '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640';
// Full range: the widest ticks that are multiples of the pool's tick spacing (10).
const TICK_LOWER = -887270;
const TICK_UPPER = 887270;

const SIGNER = '0xYourSignerAddress';

const sdk = createComposeSdk({
  baseUrl: 'https://composer.li.quest',
  apiKey: process.env.LIFI_API_KEY!,
});

const builder = sdk.flow(1, {
  name: 'uniswap-v3-mint-usdc-weth',
  inputs: {
    usdc: resources.erc20(USDC, 1),
    weth: resources.erc20(WETH, 1),
  },
});

builder.uniswapV3.mint('mint', {
  bind: {
    tokenA: builder.inputs.usdc,
    tokenB: builder.inputs.weth,
    // The position NFT is minted straight to the signer, never to the proxy.
    recipient: builder.context.sender,
  },
  config: {
    positionManager: POSITION_MANAGER,
    factory: FACTORY,
    pool: USDC_WETH_500_POOL,
    fee: 500,
    tickLower: TICK_LOWER,
    tickUpper: TICK_UPPER,
  },
  guards: [
    // 25 bps on the sqrt price caps the pool price move near 50 bps.
    guards.band({ port: 'sqrtPriceX96', bps: 25 }),
    guards.slippage({ port: 'liquidity', bps: 50 }),
  ],
});

const result = await builder.compile({
  signer: SIGNER,
  inputs: {
    usdc: materialisers.directDeposit({ amount: '1000000000' }), // 1,000 USDC
    weth: materialisers.directDeposit({ amount: '500000000000000000' }), // 0.5 WETH
  },
  // Returns unused USDC or WETH to the signer.
  sweepTo: builder.context.sender,
  // Only the residuals count as priced output; the guards protect the deposit.
  maxPriceImpactBps: 0,
});

console.log('Residual USDC:', result.outputs['mint.residualA']?.amount.estimate?.value);
console.log('Residual WETH:', result.outputs['mint.residualB']?.amount.estimate?.value);
```

Save it as `uniswap-v3.mts` and run `node uniswap-v3.mts`.

What to know about `mint`:

* **`recipient` must not be the execution proxy.** The position NFT is not a resource, so `sweepTo` cannot move it out of the proxy. Bind `recipient` to the signer or another address.
* **Ticks must be multiples of the pool's tick spacing.** The compiler does not check this; the pool reverts on a misaligned tick, and the compile simulation reports the revert.
* **Token order does not matter.** The op sorts `tokenA` and `tokenB` into the pool's `token0` and `token1`. Binding the same token twice fails with `validation_error`.
* **Residuals depend on the range.** A position takes the two tokens in the ratio its range needs at the current price and returns the rest as `residualA` / `residualB`.
* **The `liquidity` floor does not show in `outputs`.** On V3, `liquidity` is a handle: the `slippage` guard enforces its floor on chain, but no `amount.minimum` appears.

The new `tokenId` is final once the transaction is mined. Read it from the ERC-721 `Transfer` event the position manager emits to `recipient` in the `execute` receipt.

## Other liquidity operations

* **`uniswapV2.removeLiquidity`** burns the LP token and returns `tokenA` and `tokenB`. The router is called with zero minimums, so put a `slippage` guard on each output.
* **`uniswapV3.increaseLiquidity`** adds two tokens to an existing position (`tokenId`). Bind `owner` to the position's owner: the op asserts on chain that `owner` owns `tokenId` and that the position holds the two input tokens. Guard it like `mint`, with `band` on `sqrtPriceX96` and `slippage` on `liquidity`.

See the [op catalog](https://docs.li.fi/composer/composer-api/ops.md) for each op's ports and config.

## Troubleshooting

* **`simulation_revert` (HTTP 422):** a contract check failed in simulation. Each op asserts its contracts belong together: on V2, that `router`, `factory` and `pair` match and the pair has reserves; on V3, that `factory`, `pool` and `fee` match the position manager. Also check that V3 ticks are multiples of the tick spacing and that `owner` owns `tokenId`.
* **Reverts at execution:** the pool price moved outside the `band`, or the deposit minted less than the `slippage` floor. Compile again at the new price.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.