DEV Community

Cover image for ๐Ÿงฎ Day 25 of #30DaysOfSolidity โ€” Build an Automated Market Maker (AMM) ๐Ÿ’ง
Saurav Kumar
Saurav Kumar

Posted on

๐Ÿงฎ Day 25 of #30DaysOfSolidity โ€” Build an Automated Market Maker (AMM) ๐Ÿ’ง

Welcome to Day 25 of the #30DaysOfSolidity challenge!
Today, weโ€™ll build a decentralized Automated Market Maker (AMM) โ€” the core engine behind protocols like Uniswap and Balancer.

Youโ€™ll learn how to create a liquidity pool, calculate swap prices, and enable token trading without an order book โ€” all using Solidity and Foundry.


๐Ÿง  What is an Automated Market Maker (AMM)?

An AMM is a decentralized exchange mechanism that uses liquidity pools instead of buyers and sellers.
Users provide liquidity (token pairs) and earn fees when others trade using the pool.

It uses the constant product formula:

[
x * y = k
]

Where:

  • x = reserve of Token A
  • y = reserve of Token B
  • k = constant (liquidity)

This ensures the pool stays balanced after every trade.


โš™๏ธ Tech Stack

  • Solidity v0.8.20
  • Foundry (for testing and scripting)
  • ERC20 Tokens
  • Custom AMM Smart Contracts

๐Ÿ—๏ธ Project Setup

Step 1: Initialize Foundry Project

forge init day-25-amm
cd day-25-amm
Enter fullscreen mode Exit fullscreen mode

Step 2: Project Structure

day-25-amm/
โ”œโ”€ src/
โ”‚  โ”œโ”€ AMMFactory.sol
โ”‚  โ”œโ”€ AMMPair.sol
โ”‚  โ”œโ”€ MockERC20.sol
โ”‚  โ””โ”€ interfaces/IERC20Minimal.sol
โ”œโ”€ script/
โ”‚  โ”œโ”€ Deploy.s.sol
โ”‚  โ””โ”€ AddLiquidity.s.sol
โ”œโ”€ test/
โ”‚  โ”œโ”€ Pair.t.sol
โ”œโ”€ lib/
โ”‚  โ””โ”€ (optional dependencies)
โ”œโ”€ foundry.toml
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Step 3: Create a Minimal ERC20 Token

Weโ€™ll use a mock ERC20 token for testing the AMM.

๐Ÿ“„ src/MockERC20.sol

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

import "./interfaces/IERC20Minimal.sol";

contract MockERC20 is IERC20Minimal {
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    uint256 public override totalSupply;

    mapping(address => uint256) public override balanceOf;
    mapping(address => mapping(address => uint256)) public override allowance;

    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        _mint(msg.sender, _initialSupply);
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        _transfer(msg.sender, to, value);
        return true;
    }

    function approve(address spender, uint256 value) public override returns (bool) {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value) public override returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        require(allowed >= value, "Insufficient allowance");
        allowance[from][msg.sender] = allowed - value;
        _transfer(from, to, value);
        return true;
    }

    function _transfer(address from, address to, uint256 value) internal {
        require(balanceOf[from] >= value, "Insufficient balance");
        balanceOf[from] -= value;
        balanceOf[to] += value;
        emit Transfer(from, to, value);
    }

    function _mint(address to, uint256 value) internal {
        totalSupply += value;
        balanceOf[to] += value;
        emit Transfer(address(0), to, value);
    }

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฎ Step 4: Build the AMM Pair Contract

This contract manages the liquidity pool, swaps, and reserve balances.

๐Ÿ“„ src/AMMPair.sol

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

import "./interfaces/IERC20Minimal.sol";

contract AMMPair {
    IERC20Minimal public token0;
    IERC20Minimal public token1;

    uint256 public reserve0;
    uint256 public reserve1;

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

    function addLiquidity(uint256 amount0, uint256 amount1) external {
        require(amount0 > 0 && amount1 > 0, "Invalid amount");
        token0.transferFrom(msg.sender, address(this), amount0);
        token1.transferFrom(msg.sender, address(this), amount1);

        reserve0 += amount0;
        reserve1 += amount1;
    }

    function getAmountOut(uint256 amountIn, address inputToken) public view returns (uint256 amountOut) {
        require(amountIn > 0, "Invalid input");
        bool isToken0 = inputToken == address(token0);
        (uint256 reserveIn, uint256 reserveOut) = isToken0 ? (reserve0, reserve1) : (reserve1, reserve0);

        uint256 amountInWithFee = amountIn * 997;
        amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
    }

    function swap(uint256 amountIn, address inputToken) external {
        require(amountIn > 0, "Invalid input");
        bool isToken0 = inputToken == address(token0);

        IERC20Minimal input = isToken0 ? token0 : token1;
        IERC20Minimal output = isToken0 ? token1 : token0;

        uint256 amountOut = getAmountOut(amountIn, inputToken);
        require(amountOut > 0, "Insufficient output");

        input.transferFrom(msg.sender, address(this), amountIn);
        output.transfer(msg.sender, amountOut);

        if (isToken0) {
            reserve0 += amountIn;
            reserve1 -= amountOut;
        } else {
            reserve1 += amountIn;
            reserve0 -= amountOut;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿญ Step 5: Create AMM Factory

The factory helps create new token pairs.

๐Ÿ“„ src/AMMFactory.sol

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

import "./AMMPair.sol";

contract AMMFactory {
    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, "Identical tokens");
        require(getPair[tokenA][tokenB] == address(0), "Pair exists");

        AMMPair newPair = new AMMPair(tokenA, tokenB);
        pair = address(newPair);

        getPair[tokenA][tokenB] = pair;
        getPair[tokenB][tokenA] = pair;
        allPairs.push(pair);

        emit PairCreated(tokenA, tokenB, pair, allPairs.length);
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ Step 6: Deployment Script

๐Ÿ“„ script/Deploy.s.sol

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

import "forge-std/Script.sol";
import "../src/AMMFactory.sol";
import "../src/MockERC20.sol";

contract DeployScript is Script {
    function run() external {
        vm.startBroadcast();

        MockERC20 tokenA = new MockERC20("TokenA", "TKA", 1_000_000 ether);
        MockERC20 tokenB = new MockERC20("TokenB", "TKB", 1_000_000 ether);
        AMMFactory factory = new AMMFactory();

        factory.createPair(address(tokenA), address(tokenB));

        vm.stopBroadcast();
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the deployment:

forge script script/Deploy.s.sol --broadcast
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ง Step 7: Add Liquidity Script

๐Ÿ“„ script/AddLiquidity.s.sol

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

import "forge-std/Script.sol";
import "../src/AMMPair.sol";
import "../src/MockERC20.sol";

contract AddLiquidityScript is Script {
    function run(address tokenA, address tokenB, address pairAddress) external {
        vm.startBroadcast();

        AMMPair pair = AMMPair(pairAddress);

        MockERC20(tokenA).approve(address(pair), 100 ether);
        MockERC20(tokenB).approve(address(pair), 100 ether);

        pair.addLiquidity(100 ether, 100 ether);

        vm.stopBroadcast();
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช Step 8: Testing with Foundry

๐Ÿ“„ test/Pair.t.sol

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

import "forge-std/Test.sol";
import "../src/AMMPair.sol";
import "../src/MockERC20.sol";

contract PairTest is Test {
    AMMPair pair;
    MockERC20 tokenA;
    MockERC20 tokenB;

    function setUp() public {
        tokenA = new MockERC20("TokenA", "TKA", 1_000_000 ether);
        tokenB = new MockERC20("TokenB", "TKB", 1_000_000 ether);
        pair = new AMMPair(address(tokenA), address(tokenB));

        tokenA.approve(address(pair), type(uint256).max);
        tokenB.approve(address(pair), type(uint256).max);
    }

    function testAddLiquidity() public {
        pair.addLiquidity(100 ether, 200 ether);
        assertEq(pair.reserve0(), 100 ether);
        assertEq(pair.reserve1(), 200 ether);
    }

    function testSwap() public {
        pair.addLiquidity(100 ether, 100 ether);
        tokenA.transfer(address(1), 50 ether);
        vm.prank(address(1));
        tokenA.approve(address(pair), 10 ether);
        vm.prank(address(1));
        pair.swap(10 ether, address(tokenA));
    }
}
Enter fullscreen mode Exit fullscreen mode

Run tests:

forge test -vv
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Key Learning Points

โœ… Learned about liquidity pools and AMM logic
โœ… Implemented swap and reserve balance updates
โœ… Used Foundry for testing and scripting
โœ… Built a factory for creating multiple token pairs


๐Ÿ’ซ Final Thoughts

Congratulations! ๐ŸŽ‰
Youโ€™ve built a mini Uniswap using Solidity and Foundry.

This hands-on example helps you deeply understand how decentralized exchanges work under the hood โ€” from adding liquidity to executing token swaps.

Next up: you can extend it by adding:

  • Liquidity tokens (LP tokens)
  • Swap fees distribution
  • Price oracles

๐Ÿ”— GitHub Repository

๐Ÿ‘‰ GitHub - Day 25 AMM Project


๐Ÿง  Join the Challenge

Follow my #30DaysOfSolidity journey and build your own DeFi stack step-by-step!
Letโ€™s make blockchain development fun and practical ๐Ÿš€

Top comments (0)