Liquidity Pool Mechanics: How AMMs Work (Developer Guide)

TL;DR
- A liquidity pool is a smart contract holding two tokens. Trades happen against the pool, not between users. Price is set by a formula, not an order book.
- The constant product formula x * y = k keeps the pool solvent: every swap moves the ratio of reserves, so price shifts automatically with supply and demand.
- Worked example: a pool with 100 ETH and 200,000 USDC (k = 20,000,000). Buying 10 ETH leaves 90 ETH in the pool, so USDC must rise to about 222,222 to hold k constant — you pay about 22,222 USDC for 10 ETH.
- Price impact grows non-linearly with trade size relative to pool depth. Large trades move the price a lot.
- Liquidity providers earn fees but take on impermanent loss when the reserve ratio moves against them.
- Concentrated liquidity (Uniswap v3/v4) lets LPs concentrate capital around the active price for far higher efficiency — at the cost of active management.
You don't need to build an AMM to need to understand how one works. If your protocol interacts with DEX liquidity — for swaps, collateral pricing, yield strategies, or token launches — then the mechanics of how liquidity pools work are your mechanics too.
This guide explains how liquidity pools actually work: the constant product formula, price impact, fees, impermanent loss, and concentrated liquidity. It's written for developers who call into AMM infrastructure and need a working mental model of what happens underneath — the math, the tradeoffs, and the failure modes.
What a Liquidity Pool Actually Is
A liquidity pool is a smart contract that holds a reserve of two (or more) tokens and lets anyone trade between them on-chain. There's no order book, no buyer/seller matching, and no custodian holding your funds. Your tokens stay in your wallet until the moment you sign a swap transaction; the pool's smart contract then executes the trade against its reserves and enforces the terms automatically.
Because there's no company deciding which tokens get listed, listing is permissionless — anyone can create a pool for any token pair. The significance of "listing" isn't that a gatekeeper approved the token; it's that liquidity now exists in a pool anyone can swap against.
The pricing model used by most modern pools is the automated market maker (AMM). Instead of matching bids and asks, an AMM uses a mathematical formula to set the price based on the current reserves. Uniswap, Curve, and PancakeSwap are well-known examples; collectively, DEXs process over $3 billion in daily trading volume as of early 2026.
The Constant Product Formula: How Price Is Set
The foundation of Uniswap v1/v2 — and the conceptual basis for most AMM designs — is the constant product formula:
x * y = k
Where:
- x = reserve of Token A
- y = reserve of Token B
- k = a constant that must hold after every trade (before fees)
The invariant is simple: whatever you do to the reserves, the product x * y must stay equal to k. Price emerges from the ratio of the two reserves.
A worked example
Say a pool holds 100 ETH and 200,000 USDC. The constant is:
k = 100 x 200,000 = 20,000,000
A trader wants to buy 10 ETH. They add USDC and the pool gives up 10 ETH, leaving 90 ETH. To keep k constant at 20,000,000, the USDC reserve must rise so that:
90 x USDC_new = 20,000,000
USDC_new = 20,000,000 / 90 = 222,222
So the pool needs about 222,222 USDC. It started with 200,000, meaning the trader must deposit about 22,222 USDC to receive 10 ETH. That's a price of roughly 2,222 USDC/ETH — higher than the 2,000 USDC/ETH implied by the starting ratio, because the trade itself moved the price.
This is the core mechanic: every trade moves the price. Small trades in deep pools barely move it; large trades relative to pool depth move it a lot. That movement isn't a bug — it's the mechanism that keeps the pool solvent.
The swap formula
Solving the invariant for how much Token B a trader receives (dy) when adding dx of Token A:
dy = y * dx / (x + dx)
In practice, pools take a fee on the input amount before computing dy. Uniswap v2 uses a 0.3% fee, applied as a 997/1000 multiplier on the input:
// Constant product swap calculation (Uniswap v2, 0.3% fee)
function getAmountOut(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut
) public pure returns (uint256 amountOut) {
require(amountIn > 0, "Insufficient input");
require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = (reserveIn * 1000) + amountInWithFee;
amountOut = numerator / denominator;
}
Reference: The 997/1000 factor is how the 0.3% swap fee is encoded — 997 out of every 1000 units of input is used in the pricing math, 3 units go to LPs. If you're integrating against Uniswap v2, this is the exact function your call routes through. See the Uniswap v2 core contract for the canonical reference.
Price Impact and Slippage
Price impact is the change in price caused by your own trade, and it grows non-linearly with trade size relative to pool depth. A trade that's 1% of pool reserves moves the price slightly; a trade that's 30% of reserves moves it dramatically.
This is why dapps must implement slippage protection: you tell the pool the minimum output you'll accept, and if price impact (or a sandwich attack) would push you below it, the transaction reverts. Always set a non-zero amountOutMinimum in production code — a zero minimum means a sandwich bot can extract the entire value of your swap.
Impermanent Loss
Impermanent loss (IL) is the difference between holding tokens in an AMM pool versus holding them in a wallet. It appears whenever the price ratio of the pooled assets changes from the ratio at deposit.
Given a price ratio change r (new price / original price), the value of the LP position relative to simply holding is:
IL = (2 * sqrt(r)) / (1 + r) - 1
- Price change 1.25x (rose 25%): about -0.6% vs holding
- Price change 2x: about -5.7% vs holding
- Price change 4x: about -20.4% vs holding
- Price change 0.5x (fell 50%): about -5.7% (symmetric)
IL is "impermanent" because if prices return to the original ratio, the loss disappears. It becomes permanent the moment liquidity is withdrawn at a different ratio than it was deposited.
For developers, the practical implications:
- Volatile pairs (e.g., ETH/memecoin) experience significant IL; incentive designs must account for it.
- Stablecoin pairs (e.g., USDC/USDT) have minimal IL, which is why their liquidity is deep and sticky.
- Protocol-owned liquidity strategies must model IL explicitly — it's not a free lunch.
// Impermanent loss calculator (JS)
function calculateIL(priceRatioChange) {
// priceRatioChange: new_price / initial_price
const r = priceRatioChange;
const ilFactor = (2 * Math.sqrt(r)) / (1 + r);
const ilPercent = (ilFactor - 1) * 100;
return ilPercent; // negative value = loss vs holding
}
// Examples:
console.log(calculateIL(2)); // ~-5.7% vs holding at 2x price change
console.log(calculateIL(4)); // ~-20.4% vs holding at 4x price change
console.log(calculateIL(0.5)); // ~-5.7% vs holding at 50% price drop (symmetric)
Concentrated Liquidity (Uniswap v3/v4)
Uniswap v3 introduced concentrated liquidity: instead of spreading capital across all possible prices, an LP specifies a price range [pa, pb] within which their liquidity is active. Outside that range, their position earns no fees and provides no depth.
The payoff is dramatic capital efficiency — the same capital concentrated tightly around the current price does the work of far more capital spread across all prices. The tradeoff is active management: if the price moves outside your range, your position goes entirely into one asset and stops earning.
How ticks work
Prices are divided into ticks, each representing a 0.01% (1 basis point) change. Tick spacing varies by fee tier: 1 for 0.01% pools, 10 for 0.05%, 60 for 0.3%, 200 for 1%. Your position's tickLower and tickUpper must align to the pool's tick spacing or the transaction reverts.
// Uniswap v3: minting a concentrated-liquidity position
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
function provideLiquidity(
address token0,
address token1,
uint24 fee,
int24 tickLower, // lower bound of price range (must align to tick spacing)
int24 tickUpper, // upper bound of price range
uint256 amount0,
uint256 amount1
) external returns (uint256 tokenId) {
IERC20(token0).approve(address(positionManager), amount0);
IERC20(token1).approve(address(positionManager), amount1);
INonfungiblePositionManager.MintParams memory params =
INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: fee,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0,
amount1Min: 0,
recipient: msg.sender,
deadline: block.timestamp + 15 minutes
});
(tokenId, , , ) = positionManager.mint(params);
}
Reference: This is the canonical INonfungiblePositionManager.MintParams struct. If you're integrating v3 positions programmatically, this is the exact call signature. See the Uniswap v3 periphery contracts.
Fee Tiers
Uniswap v3 offers four fee tiers, each suited to a different asset type:
- 0.01% (tick spacing 1): Stablecoin/stablecoin (USDC/USDT). Tight spreads, high volume.
- 0.05% (tick spacing 10): Highly correlated assets (ETH/WBTC, stablecoin variants).
- 0.30% (tick spacing 60): Standard pairs (ETH/USDC). The historical default.
- 1.00% (tick spacing 200): Exotic or volatile pairs. High spread to offset IL risk.
Fees accrue to LP positions whose active tick range contains the current price. When the current price is within your range, you earn a proportional share of fees from trades passing through your ticks.
// Collecting accrued fees from a v3 position
INonfungiblePositionManager.CollectParams memory params =
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
(uint256 amount0, uint256 amount1) = positionManager.collect(params);
Bootstrapping Liquidity: Incentives, Bribes, and POL
Deep liquidity doesn't appear organically for new tokens. Protocols use three main mechanisms to bootstrap it:
- Liquidity mining — distributing protocol tokens to LPs as rewards. Bootstraps liquidity fast, but attracts mercenary capital that leaves when rewards end. Emission schedules must be modeled carefully; excessive emissions depress the token price and can trigger a death spiral.
- Bribing — paying veToken holders (Curve/Convex, Velodrome) to direct gauge emissions toward your pools. More targeted than broad liquidity mining.
- Protocol-owned liquidity (POL) — the protocol itself provides liquidity from its treasury, removing dependence on mercenary LPs. Pioneered by Olympus DAO. More sustainable, but requires treasury management and explicit IL accounting.
Integrating With DEXes: Routers, Quoters, and Slippage
When your protocol executes swaps, use the router contract and always implement slippage protection:
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@uniswap/v3-periphery/contracts/interfaces/IQuoterV2.sol";
contract SwapIntegration {
ISwapRouter public immutable swapRouter;
IQuoterV2 public immutable quoter;
uint256 public constant MAX_SLIPPAGE_BPS = 100; // 1%
function executeSwap(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint24 fee
) external returns (uint256 amountOut) {
// 1. Quote expected output
(uint256 expectedOut, , , ) = quoter.quoteExactInputSingle(
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: amountIn,
fee: fee,
sqrtPriceLimitX96: 0
})
);
// 2. Apply slippage tolerance
uint256 minAmountOut = expectedOut * (10000 - MAX_SLIPPAGE_BPS) / 10000;
// 3. Execute swap with slippage protection
IERC20(tokenIn).approve(address(swapRouter), amountIn);
amountOut = swapRouter.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: msg.sender,
deadline: block.timestamp + 15 minutes,
amountIn: amountIn,
amountOutMinimum: minAmountOut,
sqrtPriceLimitX96: 0
})
);
}
}
Never set amountOutMinimum to zero in production. Zero slippage protection means a sandwich bot can extract 100% of the value from your swap.
Custom Pools and Hooks (Uniswap v4)
Reference: This section covers the v4 hook API for developers integrating custom pool logic. For most readers asking how liquidity pools work, the sections above are sufficient.
Uniswap v4 introduced hooks — smart contracts that execute custom logic at key points in the pool lifecycle: before/after swaps, before/after liquidity changes, and at pool initialization.
import {BaseHook} from "v4-periphery/BaseHook.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
contract DynamicFeeHook is BaseHook {
function getHookPermissions() public pure override
returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeSwap: true,
afterSwap: false,
// ... other permissions
});
}
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override returns (bytes4, BeforeSwapDelta, uint24) {
uint24 dynamicFee = calculateDynamicFee(key, params);
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(0, 0), dynamicFee);
}
}
Hooks enable dynamic fees, on-chain limit orders, built-in TWAP oracles, MEV capture mechanisms, KYC/compliance layers, and custom liquidity curves. v4 is one of the most significant expansions of what's buildable in DeFi.
Common Developer Mistakes
- Not checking swap return values — always verify you received at least amountOutMinimum.
- Using spot prices for financial decisions — pool spot prices are manipulatable; use a TWAMM/TWAP oracle for pricing (covered in our oracle manipulation article).
- Not accounting for fee-on-transfer tokens — some ERC-20s deduct a fee on transfer, so the amount received differs from the amount sent. Standard router calls break for these.
- Hardcoding pool addresses — pools can be deprecated or replaced; use factory lookups where possible.
- Ignoring tick spacing — tick ranges must align to the pool's tick spacing or the transaction reverts.
Conclusion
Liquidity is infrastructure. The protocols that build the most durable DeFi products treat it as something to design around deliberately, not assume exists. Understanding AMM mechanics isn't optional when your protocol depends on DEX liquidity for pricing, swaps, or collateral.
The math is accessible. The tooling is excellent. The failure modes are well-documented. There's no excuse for building on AMM infrastructure you don't understand.
Autheo supports the development and deployment workflow for DeFi protocols, giving teams the infrastructure and environment management to ship liquidity-dependent protocols to mainnet with the rigor they deserve.
Gear Up with Autheo
Rep the network. Official merch from the Autheo Store.
Theo Nova
The editorial voice of Autheo
Research-driven coverage of Layer-0 infrastructure, decentralized AI, and the integration era of Web3. Written and reviewed by the Autheo content and engineering teams.
About this author →Get the Autheo Daily
Blockchain insights, AI trends, and Web3 infrastructure updates delivered to your inbox every morning.



