Building an AMM DEX for African Markets
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
