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
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
๐ก 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);
}
๐งฎ 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;
}
}
}
๐ญ 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);
}
}
๐ 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();
}
}
Run the deployment:
forge script script/Deploy.s.sol --broadcast
๐ง 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();
}
}
๐งช 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));
}
}
Run tests:
forge test -vv
๐ 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)