Uniswap is the largest decentralized exchange (DEX) on Ethereum. It is not a company running a matching engine. It is a set of smart contracts that anyone can interact with to swap tokens without an intermediary. There is no order book, no registration, no custody. You connect a wallet, choose the tokens, and the protocol executes the trade on-chain.
Uniswap is an Automated Market Maker (AMM). Instead of matching buyers and sellers, it holds reserves of two tokens in a liquidity pool and prices trades using a mathematical formula. The formula determines how much of one token you receive when you deposit another. Every swap changes the reserves, which changes the price.
Since its launch in 2018, Uniswap has processed hundreds of billions of dollars in volume. V2 established the constant product model that most AMMs still follow. V3 introduced concentrated liquidity, fundamentally changing how capital efficiency works in DeFi.
Uniswap V2 uses the simplest possible AMM formula. A pool holds reserves of two tokens, \(x\) and \(y\). The product of these reserves must remain constant after every trade (ignoring fees).
\[x \cdot y = k\]If the pool holds 1,000 ETH (\(x\)) and 2,000,000 USDC (\(y\)), then \(k = 2{,}000{,}000{,}000\). The spot price of ETH is \(y / x = 2{,}000\) USDC. When someone buys ETH, they add USDC to the pool and remove ETH. The ratio shifts, the price rises.
The key insight is that the formula makes it progressively more expensive to buy more of a token as its reserve shrinks. A small trade barely moves the price. A large trade pushes it significantly. This is the source of slippage in AMMs.
When you swap \(\Delta x_{\text{in}}\) tokens of X into the pool (paying the 0.3% fee), the output amount of Y is
\[\Delta y_{\text{out}} = \frac{y \cdot \Delta x_{\text{in}} \cdot 997}{x \cdot 1000 + \Delta x_{\text{in}} \cdot 997}\]The 997/1000 factor is the 0.3% fee. 0.3% of every trade stays in the pool as earnings for liquidity providers.
// Calculate output amount using the constant product formula (with 0.3% fee)
func getAmountOut(amountIn, reserveIn, reserveOut float64) float64 {
amountInWithFee := amountIn * 997
numerator := amountInWithFee * reserveOut
denominator := reserveIn*1000 + amountInWithFee
return numerator / denominator
}
func main() {
// Pool: 1,000 ETH and 2,000,000 USDC
reserveETH := 1000.0
reserveUSDC := 2000000.0
// Swap 100,000 USDC for ETH
ethOut := getAmountOut(100000, reserveUSDC, reserveETH)
fmt.Printf("Input: 100,000 USDC\n")
fmt.Printf("Output: %.4f ETH\n", ethOut)
fmt.Printf("Effective price: $%.2f per ETH\n", 100000/ethOut)
fmt.Printf("Spot price was: $%.2f per ETH\n", reserveUSDC/reserveETH)
// Output
// Input: 100,000 USDC
// Output: 47.4109 ETH
// Effective price: $2109.24 per ETH
// Spot price was: $2000.00 per ETH
}
A Uniswap pool does not create liquidity from nothing. Liquidity providers (LPs) deposit equal value of both tokens into the pool. If ETH is trading at $2,000, an LP depositing 10 ETH must also deposit 20,000 USDC. In return, they receive LP tokens representing their proportional share of the pool.
When traders swap, the 0.3% fee stays in the pool, increasing the value of the reserves. LP tokens represent a claim on a growing pie. When an LP withdraws, they burn their LP tokens and receive their proportional share of both tokens, including accumulated fees.
The risk is impermanent loss. When the price of one token moves relative to the other, the pool rebalances. LPs end up holding more of the token that decreased in value and less of the token that increased. Compared to simply holding both tokens, the LP position is worth less. The loss is "impermanent" because it reverses if the price returns to the original ratio. But if the price diverges permanently, the loss becomes real. Fees earned must exceed the impermanent loss for LP positions to be profitable.
An important point about Uniswap pools: any ERC-20 token can be paired with any other ERC-20 token. A pool does not need a stablecoin. You can have ETH/USDC, but also ETH/WBTC, UNI/LINK, or any combination you can think of. If someone creates a pool and adds liquidity, that pair becomes tradeable. In practice, the most liquid pools tend to involve WETH or stablecoins (USDC, DAI, USDT) because they serve as convenient base currencies, but this is a market preference, not a protocol requirement. When no direct pool exists for two tokens, the router handles it by chaining swaps through intermediate tokens (covered in the routing section below).
Uniswap V2 spreads liquidity uniformly across all possible prices, from zero to infinity. Most of that liquidity sits at price ranges that will never be used. If ETH is trading at $2,000, the liquidity provisioned at $10 or $100,000 is idle capital earning nothing.
Uniswap V3 solved this with concentrated liquidity. LPs choose a specific price range in which to deploy their capital. An LP might provide liquidity only between $1,800 and $2,200. Within that range, their capital is far more effective than in V2, because the same dollar amount provides much deeper liquidity where trades actually happen.
The improvement is dramatic. An LP concentrating in a narrow range can provide the same depth as a V2 LP with 4,000x less capital. The tradeoff is that if the price moves outside the LP's range, their position becomes inactive (it holds 100% of the less valuable token) and earns no fees until the price returns.
V3 divides the price space into discrete ticks. Each tick represents a 0.01% price increment. LP positions are defined by a lower tick and an upper tick. The pool tracks the aggregate liquidity at each tick, and as the price moves through ticks during a swap, different liquidity positions activate and deactivate.
Uniswap V3 introduced multiple fee tiers so that LPs can match the risk of the asset pair they are providing liquidity for.
Multiple pools can exist for the same token pair at different fee tiers. The Uniswap router automatically selects the pool offering the best execution for each trade.
Not every token pair has a direct pool. If you want to swap TOKEN_A for TOKEN_B but there is no TOKEN_A/TOKEN_B pool, the Uniswap router can find a path through intermediate pools. The most common intermediate token is WETH, so the route becomes TOKEN_A → WETH → TOKEN_B, executing two swaps atomically in a single transaction.
The V3 router exposes two main functions. exactInputSingle swaps through a single pool. exactInput takes an encoded path and routes through multiple pools in sequence. The entire multi-hop swap either succeeds completely or reverts entirely.
// Execute a swap through the Uniswap V3 SwapRouter
var swapRouter = common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")
// Single-hop swap via exactInputSingle
func swapExactInput(
client *ethclient.Client,
auth *bind.TransactOpts,
tokenIn, tokenOut common.Address,
fee *big.Int,
amountIn *big.Int,
) (*types.Transaction, error) {
deadline := big.NewInt(time.Now().Unix() + 300)
// For production, query the pool to estimate amountOut first
// Then apply slippage tolerance to get amountOutMinimum
amountOutMin := big.NewInt(0) // set properly in production!
type ExactInputSingleParams struct {
TokenIn common.Address
TokenOut common.Address
Fee *big.Int
Recipient common.Address
Deadline *big.Int
AmountIn *big.Int
AmountOutMinimum *big.Int
SqrtPriceLimitX96 *big.Int
}
params := ExactInputSingleParams{
TokenIn: tokenIn,
TokenOut: tokenOut,
Fee: fee, // e.g. 3000 for 0.30%
Recipient: auth.From,
Deadline: deadline,
AmountIn: amountIn,
AmountOutMinimum: amountOutMin,
SqrtPriceLimitX96: big.NewInt(0), // no price limit
}
data, _ := routerABI.Pack("exactInputSingle", params)
return sendTx(client, auth, swapRouter, data)
}
// Multi-hop swap: TOKEN_A --(0.3%)--> WETH --(0.05%)--> USDC
func swapMultiHop(
client *ethclient.Client,
auth *bind.TransactOpts,
tokenA, weth, usdc common.Address,
amountIn *big.Int,
) (*types.Transaction, error) {
// Encode path as packed bytes
// [address (20B)] [fee (3B)] [address (20B)] [fee (3B)] [address (20B)]
path := encodePath(
[]common.Address{tokenA, weth, usdc},
[]*big.Int{big.NewInt(3000), big.NewInt(500)},
)
type ExactInputParams struct {
Path []byte
Recipient common.Address
Deadline *big.Int
AmountIn *big.Int
AmountOutMinimum *big.Int
}
params := ExactInputParams{
Path: path,
Recipient: auth.From,
Deadline: big.NewInt(time.Now().Unix() + 300),
AmountIn: amountIn,
AmountOutMinimum: big.NewInt(0), // set properly in production!
}
data, _ := routerABI.Pack("exactInput", params)
return sendTx(client, auth, swapRouter, data)
}
Uniswap V2 introduced flash swaps, a feature that lets you receive tokens from a pool before paying for them. You borrow tokens, use them for whatever you need, and repay (in the same token or the other token in the pair) all within a single transaction. If you fail to repay, the entire transaction reverts. No collateral is required because the atomicity of the transaction guarantees the protocol cannot lose funds.
Flash swaps enable powerful patterns. The most common is atomic arbitrage. If ETH is priced at $2,000 on Uniswap and $2,050 on Sushiswap, an arbitrageur can flash-borrow ETH from Uniswap, sell it on Sushiswap for USDC, use some of that USDC to repay the Uniswap pool, and keep the profit. All in one transaction, with zero capital at risk. If the arbitrage does not produce enough profit to cover the repayment, the transaction simply reverts.
In V3, the equivalent mechanism is the flash function on the pool contract. It lends any amount of either token, calls a callback on the borrower's contract, and then verifies that the pool has been repaid (plus a fee) before the transaction completes.
Flash swaps are one reason AMM prices stay aligned with the broader market. Arbitrageurs continuously correct price discrepancies, and flash swaps lower the barrier to performing this arbitrage by removing the need for upfront capital.
For the broader framework of trading mechanics and how order books compare to AMMs, see Market Microstructure. For the implicit cost of every trade, see The Bid-Ask Spread. For why large trades cost more and how MEV exploits your tolerance, see Slippage. For how the AMM contracts get deployed and executed, see Ethereum Smart Contracts. For the full lifecycle of a swap as an on-chain transaction, see Sending Transactions on Ethereum.