Skip to main content

Command Palette

Search for a command to run...

Building an AMM DEX for African Markets

Updated
7 min read

An Automated Market Maker (AMM) DEX lets users trade tokens without order books — using liquidity pools and a pricing formula instead. This guide covers implementation for African markets.

1. AMM Core — Constant Product Formula

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

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

/**
 * @title AfricanAMM
 * @notice AMM DEX for African token pairs (USDT/NGN-token etc)
 * Uses constant product formula: x * y = k
 */
contract AfricanAMM is ERC20, ReentrancyGuard {

    IERC20 public immutable token0; // e.g. USDT
    IERC20 public immutable token1; // e.g. African stablecoin

    uint256 public reserve0;
    uint256 public reserve1;

    uint256 public constant FEE_NUMERATOR = 997;   // 0.3% fee
    uint256 public constant FEE_DENOMINATOR = 1000;

    // African liquidity providers earn fee revenue
    mapping(address => uint256) public liquidityProvided;

    event Swap(
        address indexed user,
        address tokenIn,
        uint256 amountIn,
        uint256 amountOut
    );

    event LiquidityAdded(
        address indexed provider,
        uint256 amount0,
        uint256 amount1,
        uint256 lpTokens
    );

    constructor(
        address _token0,
        address _token1
    ) ERC20("African-LP", "ALP") {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    // Add liquidity to earn trading fees
    function addLiquidity(
        uint256 amount0Desired,
        uint256 amount1Desired
    ) external nonReentrant returns (uint256 lpTokens) {

        token0.transferFrom(msg.sender, address(this), amount0Desired);
        token1.transferFrom(msg.sender, address(this), amount1Desired);

        uint256 totalSupply = totalSupply();

        if (totalSupply == 0) {
            // First liquidity provider sets initial price
            lpTokens = sqrt(amount0Desired * amount1Desired);
        } else {
            // Subsequent providers get LP tokens proportional to contribution
            lpTokens = min(
                (amount0Desired * totalSupply) / reserve0,
                (amount1Desired * totalSupply) / reserve1
            );
        }

        require(lpTokens > 0, "Insufficient liquidity minted");

        _mint(msg.sender, lpTokens);
        _update(
            token0.balanceOf(address(this)),
            token1.balanceOf(address(this))
        );

        emit LiquidityAdded(msg.sender, amount0Desired, amount1Desired, lpTokens);
    }

    // Swap token0 for token1 or vice versa
    function swap(
        address tokenIn,
        uint256 amountIn,
        uint256 minAmountOut
    ) external nonReentrant returns (uint256 amountOut) {

        require(
            tokenIn == address(token0) || tokenIn == address(token1),
            "Invalid token"
        );
        require(amountIn > 0, "Amount must be positive");

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

        tokenInContract.transferFrom(msg.sender, address(this), amountIn);

        // Apply 0.3% fee — goes to liquidity providers
        uint256 amountInWithFee = amountIn * FEE_NUMERATOR;

        // Constant product formula: (x + dx) * (y - dy) = x * y
        amountOut = (amountInWithFee * reserveOut) /
            ((reserveIn * FEE_DENOMINATOR) + amountInWithFee);

        require(amountOut >= minAmountOut, "Slippage too high");
        require(amountOut < reserveOut, "Insufficient liquidity");

        tokenOutContract.transfer(msg.sender, amountOut);

        _update(
            token0.balanceOf(address(this)),
            token1.balanceOf(address(this))
        );

        emit Swap(msg.sender, tokenIn, amountIn, amountOut);
    }

    // Remove liquidity and receive tokens back
    function removeLiquidity(
        uint256 lpTokens
    ) external nonReentrant returns (uint256 amount0, uint256 amount1) {

        uint256 totalSupply = totalSupply();
        amount0 = (lpTokens * reserve0) / totalSupply;
        amount1 = (lpTokens * reserve1) / totalSupply;

        require(amount0 > 0 && amount1 > 0, "Insufficient liquidity burned");

        _burn(msg.sender, lpTokens);
        token0.transfer(msg.sender, amount0);
        token1.transfer(msg.sender, amount1);

        _update(
            token0.balanceOf(address(this)),
            token1.balanceOf(address(this))
        );
    }

    // Get price quote for a swap
    function getAmountOut(
        uint256 amountIn,
        address tokenIn
    ) external view returns (uint256 amountOut) {
        bool isToken0 = tokenIn == address(token0);
        (uint256 reserveIn, uint256 reserveOut) = isToken0
            ? (reserve0, reserve1)
            : (reserve1, reserve0);

        uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
        amountOut = (amountInWithFee * reserveOut) /
            ((reserveIn * FEE_DENOMINATOR) + amountInWithFee);
    }

    function _update(uint256 balance0, uint256 balance1) private {
        reserve0 = balance0;
        reserve1 = balance1;
    }

    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) { z = x; x = (y / x + x) / 2; }
        } else if (y != 0) { z = 1; }
    }

    function min(uint256 x, uint256 y) internal pure returns (uint256) {
        return x < y ? x : y;
    }
}

2. African Yield Farming Contract

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

/**
 * @title AfricanYieldFarm
 * @notice Stake LP tokens to earn additional rewards
 * African farmers earn yield on their liquidity
 */
contract AfricanYieldFarm {

    IERC20 public lpToken;      // LP tokens from AMM
    IERC20 public rewardToken;  // Reward token (e.g. platform token)

    uint256 public rewardRate;  // Rewards per second
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;
    mapping(address => uint256) public stakedBalance;
    uint256 public totalStaked;

    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardClaimed(address indexed user, uint256 reward);

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) return rewardPerTokenStored;
        return rewardPerTokenStored + (
            (block.timestamp - lastUpdateTime) *
            rewardRate * 1e18 / totalStaked
        );
    }

    function earned(address account) public view returns (uint256) {
        return (
            stakedBalance[account] *
            (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18
        ) + rewards[account];
    }

    // Stake LP tokens to start earning
    function stake(uint256 amount)
        external updateReward(msg.sender) {
        require(amount > 0, "Cannot stake 0");
        totalStaked += amount;
        stakedBalance[msg.sender] += amount;
        lpToken.transferFrom(msg.sender, address(this), amount);
        emit Staked(msg.sender, amount);
    }

    // Claim accumulated rewards
    function claimReward()
        external updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            rewardToken.transfer(msg.sender, reward);
            emit RewardClaimed(msg.sender, reward);
        }
    }
}

3. DEX Frontend — Price Impact Calculator

class AfricanDEXPriceCalculator {

  // Calculate price impact for a swap
  static calculatePriceImpact(
    amountIn,
    reserveIn,
    reserveOut
  ) {
    const amountInWithFee = amountIn * 997;
    const amountOut = (amountInWithFee * reserveOut) /
      (reserveIn * 1000 + amountInWithFee);

    // Market price before swap
    const marketPrice = reserveOut / reserveIn;

    // Execution price
    const executionPrice = amountOut / amountIn;

    // Price impact percentage
    const priceImpact =
      ((marketPrice - executionPrice) / marketPrice) * 100;

    return {
      amountOut: amountOut.toFixed(6),
      priceImpact: priceImpact.toFixed(2),
      executionPrice: executionPrice.toFixed(6),
      warning: priceImpact > 5 ? 'HIGH_PRICE_IMPACT' : null,
      // Nigerian context — show NGN equivalent
      ngnEquivalent: (amountOut * 1650).toLocaleString('en-NG', {
        style: 'currency', currency: 'NGN'
      })
    };
  }

  // Find best route across multiple pools
  static findBestRoute(tokenIn, tokenOut, amountIn, pools) {
    let bestAmountOut = 0;
    let bestRoute = null;

    // Direct route
    const directPool = pools.find(p =>
      (p.token0 === tokenIn && p.token1 === tokenOut) ||
      (p.token0 === tokenOut && p.token1 === tokenIn)
    );

    if (directPool) {
      const amountOut = this.getAmountOut(
        amountIn, tokenIn, directPool
      );
      if (amountOut > bestAmountOut) {
        bestAmountOut = amountOut;
        bestRoute = [directPool];
      }
    }

    // Multi-hop routes via USDT (most liquid in Nigeria)
    const usdtPools = pools.filter(p =>
      p.token0 === 'USDT' || p.token1 === 'USDT'
    );

    // Check tokenIn → USDT → tokenOut
    // ... routing logic

    return { bestRoute, amountOut: bestAmountOut };
  }
}

4. Nigerian DeFi — Subgraph for Analytics

// Query DeFi protocol data for Nigerian analytics dashboard
const { gql, request } = require('graphql-request');

const AFRICAN_DEX_SUBGRAPH =
  'https://api.thegraph.com/subgraphs/name/your-org/african-dex';

const GET_POOL_STATS = gql`
  query GetAfricanPoolStats($poolId: String!) {
    pool(id: $poolId) {
      id
      token0 { symbol decimals }
      token1 { symbol decimals }
      reserve0
      reserve1
      totalValueLockedUSD
      volumeUSD
      txCount
      poolDayData(
        first: 7
        orderBy: date
        orderDirection: desc
      ) {
        date
        volumeUSD
        feesUSD
        tvlUSD
      }
    }
  }
`;

const getDeFiAnalytics = async (poolId) => {
  const data = await request(AFRICAN_DEX_SUBGRAPH, GET_POOL_STATS, {
    poolId
  });

  const pool = data.pool;
  const tvlNGN = parseFloat(pool.totalValueLockedUSD) * 1650;

  return {
    pool: `\({pool.token0.symbol}/\){pool.token1.symbol}`,
    tvlUSD: parseFloat(pool.totalValueLockedUSD).toLocaleString(),
    tvlNGN: tvlNGN.toLocaleString('en-NG', {
      style: 'currency', currency: 'NGN'
    }),
    volume24h: parseFloat(pool.poolDayData[0]?.volumeUSD || 0)
      .toLocaleString(),
    fees24h: parseFloat(pool.poolDayData[0]?.feesUSD || 0)
      .toLocaleString(),
    transactions: pool.txCount,
    apr: calculateAPR(pool.poolDayData)
  };
};

const calculateAPR = (dayData) => {
  if (!dayData.length) return '0%';
  const avgDailyFees = dayData.reduce(
    (sum, d) => sum + parseFloat(d.feesUSD), 0
  ) / dayData.length;
  const tvl = parseFloat(dayData[0].tvlUSD);
  const apr = (avgDailyFees / tvl) * 365 * 100;
  return `${apr.toFixed(2)}%`;
};

African DeFi Deployment Checklist

  • [ ] Smart contracts audited by reputable firm

  • [ ] Deployed on testnet and battle-tested

  • [ ] Front-running protection implemented

  • [ ] Emergency pause functionality

  • [ ] Timelock on admin functions

  • [ ] Multi-sig for protocol treasury

  • [ ] Price oracle integration (Chainlink)

  • [ ] Subgraph deployed for analytics

  • [ ] Nigerian Naira price display for UX

  • [ ] Mobile-optimized interface for Nigerian users

  • [ ] Gas optimization for cheaper transactions


ZikarelHub LTD builds DeFi protocols and DEX platforms for Nigerian and African markets. zikarelhub.tech/crypto-exchange-development-nigeria