DeFi · Intermediate

Automated market makers, slippage and oracles

4 min read

Why it matters

DeFi appeared in almost half of the Blockchain Developer job descriptions we read. Most DeFi code is a small amount of Solidity around a precise piece of maths. Developers who understand that maths, and how it can be pushed around by other traders, write safer protocols and better front ends.

The concept

  • An automated market maker (AMM) replaces an order book with a pool of two tokens. The Uniswap v2 design keeps the product of the reserves constant: x times y equals k. Buying token X adds Y to the pool and removes X, so X gets more expensive with every unit bought.

  • Fees (0.30% in this design) stay in the pool and are earned by liquidity providers, so k slowly grows.

  • Price impact is how far a trade moves the price; it grows with trade size relative to the pool. Slippage protection lets a user set the minimum they will accept, and the transaction reverts if the price moved against them before it was mined.

  • Lending protocols let users borrow against collateral, and liquidate positions when the collateral's value falls too low. They depend on oracles for prices.

  • Oracle manipulation: if a protocol reads the price from a single pool's current reserves, an attacker can move that pool (often with a flash loan) within one transaction and borrow against a fake price. Use time-weighted averages or robust oracle networks.

  • Front-running and MEV: transactions wait in a public mempool, so others can trade ahead of them. Slippage limits cap the damage.

Worked example: a constant-product pool

amm.py
"""A constant-product AMM (x * y = k), the design behind Uniswap v2 pools. Integer maths, like on chain."""


class Pool:
    FEE_BPS = 30  # 0.30% trading fee, paid to liquidity providers

    def __init__(self, reserve_token: int, reserve_usdc: int):
        self.x = reserve_token  # token reserve (whole units for readability)
        self.y = reserve_usdc   # USDC reserve

    def price(self) -> float:
        return self.y / self.x

    def quote_buy_token(self, usdc_in: int) -> int:
        usdc_after_fee = usdc_in * (10_000 - self.FEE_BPS) // 10_000
        return self.x * usdc_after_fee // (self.y + usdc_after_fee)

    def buy_token(self, usdc_in: int) -> int:
        out = self.quote_buy_token(usdc_in)
        self.y += usdc_in
        self.x -= out
        return out


for trade in (1_000, 10_000, 100_000):
    pool = Pool(reserve_token=1_000_000, reserve_usdc=2_000_000)  # 1M tokens, price 2.00 USDC
    k_before = pool.x * pool.y
    start = pool.price()
    got = pool.buy_token(trade)
    paid = trade / got
    print(f"spend {trade:>7,} USDC -> {got:>6,} tokens | avg price {paid:.4f} "
          f"({(paid / start - 1) * 100:5.2f}% above spot) | pool price after {pool.price():.4f} | k grew: {pool.x * pool.y >= k_before}")

# Slippage protection: the caller states the minimum they accept.
pool = Pool(1_000_000, 2_000_000)
expected = pool.quote_buy_token(10_000)
min_out = expected * 995 // 1000  # accept at most 0.5% worse than quoted
pool.buy_token(50_000)  # someone else trades first, moving the price
actual = pool.quote_buy_token(10_000)
print(f"quoted {expected:,}, minimum {min_out:,}, now {actual:,} -> {'execute' if actual >= min_out else 'revert: slippage too high'}")
shellsession
$ python3 amm.py
spend   1,000 USDC ->    498 tokens | avg price 2.0080 ( 0.40% above spot) | pool price after 2.0020 | k grew: True
spend  10,000 USDC ->  4,960 tokens | avg price 2.0161 ( 0.81% above spot) | pool price after 2.0200 | k grew: True
spend 100,000 USDC -> 47,482 tokens | avg price 2.1061 ( 5.30% above spot) | pool price after 2.2047 | k grew: True
quoted 4,960, minimum 4,935, now 4,722 -> revert: slippage too high

In a pool with 1 million tokens at 2.00 USDC, a 1,000 USDC purchase pays about 0.4% over the spot price, most of it the 0.30% fee (whole-token rounding adds a little in this toy). A 100,000 USDC purchase, 5% of the pool's USDC, pays 5.3% over spot and moves the price to 2.20. k grows after every trade because of the fee.

In the last case, a user asked for at least 4,935 tokens (0.5% below the quote). Another trader bought first, the price moved, and the swap would now return only 4,722, so it reverts instead of giving a bad fill. Contracts do the same with a minAmountOut parameter and a deadline.

Common mistakes

  • Using spot pool prices as an oracle. They can be moved within one transaction.

  • No slippage or deadline parameters, leaving users open to front-running and stale transactions.

  • Rounding in the user's favour. Round against the user in protocol maths (as // does here), or value leaks out over many trades.

  • Ignoring token quirks: fee-on-transfer tokens, rebasing tokens and tokens with different decimals break naive accounting.

Interview angle

"Explain how a constant-product AMM prices a trade." "What is impermanent loss?" (the value difference for liquidity providers compared with holding the tokens, as prices move). "How can an oracle be manipulated and how do you defend against it?" "What is MEV?"

Practice task

  1. Add sell_token and add_liquidity functions, and check that k never decreases across a random sequence of trades.

  2. Compute impermanent loss for a liquidity provider when the token's price doubles, and compare with the formula.

Further reading