iToverDose/Software· 31 MAY 2026 · 16:01

How Smart Contracts Use Math to Power Uniswap-Style AMMs

A liquidity pool isn’t an order book. By replacing bids and asks with pure math, AMMs like Uniswap enable trustless trading on Ethereum. Here’s how the constant product formula works—and why rounding errors still cost traders.

DEV Community4 min read0 Comments

A vending machine holds 1,000 coffee beans and 1,000 coins. There’s no menu, no cashier, and one iron rule: the product of the two numbers inside must never decrease. That simple constraint is the foundation of Uniswap—and the automated market maker I built in 60 lines of Solidity.

The Blockchain Can’t Run an Order Book

Traditional exchanges like Binance or the NYSE rely on order books. Market makers post bids and asks, a matching engine pairs them, and millions of updates occur every second in a centralized database. On a blockchain, this model collapses under its own weight. Transactions take seconds to settle. Each state change incurs gas fees. Storing millions of constantly updating orders would erase profitability before the first trade completes.

Uniswap’s breakthrough was replacing the order book with a liquidity pool—a smart contract holding two tokens—and replacing the matching engine with a mathematical formula. The result: a system that works within blockchain constraints and operates with near-zero coordination between strangers.

x · y = k: The Formula That Powers DeFi

At the heart of every Uniswap-style AMM is the Constant Product Invariant:

x · y = k

Where x is the reserve of Token A, y is the reserve of Token B, and k must never decrease. When a trader sells Token A into the pool, x increases. To keep k constant, y must decrease—the contract sends out Token B. The price emerges automatically from the ratio of reserves.

Let’s walk through a real example. Suppose the pool holds 1,000 Token A and 1,000 Token B, so k = 1,000,000. A trader sells 100 Token A:

  • The formula calculates the output: amountOut = (reserveOut × amountIn) / (reserveIn + amountIn)
  • Plugging in the numbers: (1000 × 100) / (1000 + 100) = 100,000 / 1,100 ≈ 90.9 Token B

The trader receives about 90.9 Token B instead of 100. That gap is slippage—not a bug, but the formula protecting the pool. The larger the trade relative to the pool size, the worse the price becomes. It’s not arbitrary; it’s mathematically inevitable.

After the swap, the pool holds 1,100 Token A and approximately 909.1 Token B, and k remains roughly 1,000,000. The invariant holds.

Inside the Solidity Contract: SimpleAMM

The contract implements three core functions, each serving a distinct purpose.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleAMM {
    error ZeroAmount();
    error InvalidToken();
    error ZeroLiquidity();
    error TransferFailed();
    error InvalidRatio();

    IERC20 public immutable token0;
    IERC20 public immutable token1;
    uint256 public reserve0;
    uint256 public reserve1;

    event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1);
    event Swap(address indexed trader, address tokenIn, uint256 amountIn, uint256 amountOut);

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

The getAmountOut function is pure math—no state, no side effects. It’s intentionally separated so anyone can call it to preview a trade before executing it. In DeFi, this pattern is standard: quote first, then transact.

function getAmountOut(
    uint256 _amountIn,
    uint256 _reserveIn,
    uint256 _reserveOut
) public pure returns (uint256) {
    if (_amountIn == 0) revert ZeroAmount();
    if (_reserveIn == 0 || _reserveOut == 0) revert ZeroLiquidity();

    uint256 numerator = _reserveOut * _amountIn;
    uint256 denominator = _reserveIn + _amountIn;
    return numerator / denominator;
}

The addLiquidity function enforces price consistency. If the pool already has reserves, deposits must preserve the current price ratio. The check _amount0 * reserve1 != _amount1 * reserve0 detects any imbalance. Deposit skewed amounts and you’d instantly shift the price—effectively gifting value to arbitrageurs.

function addLiquidity(uint256 _amount0, uint256 _amount1) external {
    if (_amount0 == 0 || _amount1 == 0) revert ZeroAmount();

    if (reserve0 > 0 && reserve1 > 0) {
        if (_amount0 * reserve1 != _amount1 * reserve0) {
            revert InvalidRatio();
        }
    }

    if (!token0.transferFrom(msg.sender, address(this), _amount0)) revert TransferFailed();
    if (!token1.transferFrom(msg.sender, address(this), _amount1)) revert TransferFailed();

    reserve0 += _amount0;
    reserve1 += _amount1;
    emit LiquidityAdded(msg.sender, _amount0, _amount1);
}

The swap function uses a clean ternary tuple assignment to avoid branching. Instead of separate if/else blocks, it maps all variables in one line based on the trade direction.

function swap(address _tokenIn, uint256 _amountIn) external returns (uint256 amountOut) {
    if (_amountIn == 0) revert ZeroAmount();
    if (_tokenIn != address(token0) && _tokenIn != address(token1)) revert InvalidToken();

    bool isToken0 = _tokenIn == address(token0);
    (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = 
        isToken0 ? (token0, token1, reserve0, reserve1) : (token1, token0, reserve1, reserve0);

    if (!tokenIn.transferFrom(msg.sender, address(this), _amountIn)) revert TransferFailed();

    amountOut = getAmountOut(_amountIn, reserveIn, reserveOut);

    if (isToken0) {
        reserve0 += _amountIn;
        reserve1 -= amountOut;
    } else {
        reserve0 -= amountOut;
        reserve1 += _amountIn;
    }

    emit Swap(msg.sender, _tokenIn, _amountIn, amountOut);

    if (!tokenOut.transfer(msg.sender, amountOut)) revert TransferFailed();
}

Common Pitfalls: Where Math Meets Reality

Integer division truncates—silently.

In getAmountOut, the division happens at the end. But truncation still occurs. For example, 100,000 / 1,100 = 90, not 90.909.... The pool keeps the remainder. At scale, across millions of trades, these tiny fractions accumulate into real value. Production AMMs mitigate this with fees—typically 0.3%—by multiplying the numerator by 997 before dividing by 1000. This small adjustment ensures the pool retains value while preventing dust buildup.

Another risk: front-running arbitrage. Because x · y = k is public, anyone can observe an impending trade and adjust their own position to profit from the expected price shift. This isn’t a flaw in the formula—it’s a feature of transparent blockchains. Users must account for execution latency and potential MEV when trading on AMMs.

The Future of Automated Markets

AMMs have redefined how digital assets trade by removing intermediaries and enabling permissionless liquidity. They’re not perfect, but their elegance lies in simplicity: trust the math, not the middleman. As blockchain scalability improves and MEV mitigation tools mature, AMMs will likely become even more efficient and widely adopted. For developers stepping into Web3 from traditional stacks, understanding this formula isn’t optional—it’s foundational.

AI summary

Learn how constant product invariant formulas power Uniswap-style AMMs, how to build a simple liquidity pool in Solidity, and where rounding errors and arbitrage risks emerge in decentralized trading.

Comments

00
LEAVE A COMMENT
ID #QSCVRZ

0 / 1200 CHARACTERS

Human check

4 + 6 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.