AMM 101
AMM

Build Your AMM

This is the main event. You will build a constant-product AMM pool from scratch.

The Math Behind It

An Automated Market Maker replaces order books with a simple formula:

x * y = k

x is the reserve of Token A, y is the reserve of Token B, and k is a constant that must never increase after a swap.

Here is a concrete example:

  • The pool holds 100 TokenA and 100 TokenB.
  • Therefore k = 100 * 100 = 10,000.
  • A user sends in 10 TokenA.
  • New reserve0 = 110.
  • To keep k = 10,000, new reserve1 = 10,000 / 110 = 90 (Solidity truncates decimals).
  • The user receives 100 - 90 = 10 TokenB.
  • After the swap, check the invariant: 110 * 90 = 9,900, which is <= the original k. It holds.

Your job is to turn this math into Solidity.

What You Need to Build

Before you start, deploy two ERC20 tokens from the ERC20 page. You will need their addresses.

Create a contract called SimpleAMM with these functions:

  • constructor(address _token0, address _token1), stores the two token contracts.
  • addLiquidity(uint256 amount0, uint256 amount1), pulls both tokens from the caller, updates reserves, and sets k.
  • getSwapAmountOut(uint256 amountIn, bool isToken0In), a view function that returns the output amount using the x * y = k math.
  • swap(uint256 amountIn, bool isToken0In), executes the actual token exchange.
  • getReserves(), a view function that returns (reserve0, reserve1).

Starter Skeleton

Do not just copy-paste. Read the TODOs and think through each one before you write the code.

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

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

contract SimpleAMM {
    IERC20 public token0;
    IERC20 public token1;

    uint256 public reserve0;
    uint256 public reserve1;
    uint256 public k;

    // TODO: constructor. Store _token0 and _token1.

    function addLiquidity(uint256 amount0, uint256 amount1) external {
        // TODO:
        // 1. Reject if either amount is 0.
        // 2. If reserves already exist, enforce the same ratio.
        //    Hint: cross-multiply to avoid division.
        //    amount0 * reserve1 == amount1 * reserve0
        // 3. Pull both tokens from msg.sender using transferFrom.
        // 4. Update reserve0 and reserve1.
        // 5. Update k = reserve0 * reserve1.
    }

    function getSwapAmountOut(uint256 amountIn, bool isToken0In)
        public
        view
        returns (uint256 amountOut)
    {
        // TODO:
        // 1. Reject if amountIn is 0 or if there is no liquidity (k == 0).
        // 2. If isToken0In:
        //      newReserve0 = reserve0 + amountIn
        //      newReserve1 = k / newReserve0
        //      amountOut   = reserve1 - newReserve1
        //    Else do the mirror math for token1 -> token0.
    }

    function swap(uint256 amountIn, bool isToken0In) external {
        // TODO:
        // 1. Call getSwapAmountOut to know the output.
        // 2. Reject if amountOut is 0.
        // 3. Use transferFrom to pull the input token from the user.
        // 4. Use transfer to push the output token to the user.
        // 5. Update the reserves.
    }

    function getReserves() external view returns (uint256, uint256) {
        // TODO: return both reserves.
    }
}

Remix Track

Your Task

Fill in the TODOs above. Compile your contract in Remix. If it compiles, you are halfway there.

Deploy and Verify

Deploy SimpleAMM with your two token addresses.

  1. Approve the AMM to spend 100 tokens of each type.
  2. Call addLiquidity(100000000000000000000, 100000000000000000000).
  3. Call getReserves. You should see two equal numbers.
  4. Call k. It should equal the product of those two numbers.
  5. Call getSwapAmountOut with 10000000000000000000 and true. This is a view function, it costs no gas. Note the predicted output.
  6. Approve the input token again, then call swap with the same arguments.
  7. Call getReserves again. Multiply the two numbers. Is the product <= the original k? If yes, your invariant holds.

If you get stuck, re-read the math example at the top of the page. The contract is just that math translated into code.

Foundry Track

Your Task

Create src/SimpleAMM.sol and fill in the TODOs from the skeleton above.

Use forge build to check compilation. Fix errors until it compiles cleanly.

The Test

Here is a test file. Do not modify it. Your contract must make every assertion pass.

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

import "forge-std/Test.sol";
import "../src/MyToken.sol";
import "../src/SimpleAMM.sol";

contract SimpleAMMTest is Test {
    MyToken tokenA;
    MyToken tokenB;
    SimpleAMM amm;

    address alice = address(0x1);

    function setUp() public {
        tokenA = new MyToken("Token A", "TKA", 10000);
        tokenB = new MyToken("Token B", "TKB", 10000);
        amm = new SimpleAMM(address(tokenA), address(tokenB));

        tokenA.transfer(alice, 1000 * 10 ** 18);
        tokenB.transfer(alice, 1000 * 10 ** 18);
    }

    function testAddLiquidity() public {
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100 * 10 ** 18);
        tokenB.approve(address(amm), 100 * 10 ** 18);
        amm.addLiquidity(100 * 10 ** 18, 100 * 10 ** 18);
        vm.stopPrank();

        (uint256 r0, uint256 r1) = amm.getReserves();
        assertEq(r0, 100 * 10 ** 18);
        assertEq(r1, 100 * 10 ** 18);
        assertEq(amm.k(), r0 * r1);
    }

    function testSwapMaintainsInvariant() public {
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100 * 10 ** 18);
        tokenB.approve(address(amm), 100 * 10 ** 18);
        amm.addLiquidity(100 * 10 ** 18, 100 * 10 ** 18);

        uint256 predictedOut = amm.getSwapAmountOut(10 * 10 ** 18, true);
        assertGt(predictedOut, 0);

        tokenA.approve(address(amm), 10 * 10 ** 18);
        amm.swap(10 * 10 ** 18, true);
        vm.stopPrank();

        (uint256 r0, uint256 r1) = amm.getReserves();
        uint256 oldK = 100 * 10 ** 18 * 100 * 10 ** 18;

        assertLe(r0 * r1, oldK);
        assertGt(tokenB.balanceOf(alice), 900 * 10 ** 18);
    }
}

Save it as test/SimpleAMM.t.sol and run:

forge test -vv

If you see green checkmarks, your AMM works. If a test fails, read the failure message, it tells you exactly which assertion broke.

Next Step

Once your tests pass, head to the Deployment page to deploy your AMM to a real testnet.