Back to Tutorials
Advanced1.5 hours

DEX Fundamentals

Understand decentralized exchange concepts and build a simple swap

DEX Fundamentals

In this advanced tutorial, you'll learn the core concepts behind Decentralized Exchanges (DEX) and build a simple token swap contract on Beatoz.

Prerequisites

Complete the ERC-20 Token tutorial first.

What is a DEX?

A Decentralized Exchange (DEX) allows users to trade tokens directly from their wallets without intermediaries. Key concepts:

  • Automated Market Maker (AMM): Uses mathematical formulas to price assets
  • Liquidity Pools: Token pairs locked in smart contracts
  • Liquidity Providers (LPs): Users who deposit tokens and earn fees
Testnet

AMM Formula

The constant product formula (used by Uniswap):

x * y = k

Where:

  • x = Reserve of token A
  • y = Reserve of token B
  • k = Constant product (invariant)

When you swap token A for token B:

new_x = x + input_amount
new_y = k / new_x
output_amount = y - new_y

Simple Swap Contract

SimpleSwap.sol

// 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/utils/ReentrancyGuard.sol";

contract SimpleSwap is ERC20, ReentrancyGuard {
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;
    
    uint256 public reserveA;
    uint256 public reserveB;
    
    uint256 public constant FEE_NUMERATOR = 3;
    uint256 public constant FEE_DENOMINATOR = 1000; // 0.3% fee
    
    event LiquidityAdded(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 liquidity
    );
    
    event LiquidityRemoved(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 liquidity
    );
    
    event Swap(
        address indexed user,
        address tokenIn,
        uint256 amountIn,
        address tokenOut,
        uint256 amountOut
    );
    
    constructor(
        address _tokenA,
        address _tokenB
    ) ERC20("SimpleSwap LP", "SLP") {
        require(_tokenA != _tokenB, "Identical addresses");
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }
    
    // Add liquidity to the pool
    function addLiquidity(
        uint256 amountA,
        uint256 amountB
    ) external nonReentrant returns (uint256 liquidity) {
        tokenA.transferFrom(msg.sender, address(this), amountA);
        tokenB.transferFrom(msg.sender, address(this), amountB);
        
        uint256 totalSupply = totalSupply();
        
        if (totalSupply == 0) {
            // First liquidity provision
            liquidity = sqrt(amountA * amountB);
        } else {
            // Proportional to existing liquidity
            liquidity = min(
                (amountA * totalSupply) / reserveA,
                (amountB * totalSupply) / reserveB
            );
        }
        
        require(liquidity > 0, "Insufficient liquidity minted");
        
        _mint(msg.sender, liquidity);
        
        reserveA += amountA;
        reserveB += amountB;
        
        emit LiquidityAdded(msg.sender, amountA, amountB, liquidity);
    }
    
    // Remove liquidity from the pool
    function removeLiquidity(
        uint256 liquidity
    ) external nonReentrant returns (uint256 amountA, uint256 amountB) {
        require(balanceOf(msg.sender) >= liquidity, "Insufficient LP tokens");
        
        uint256 totalSupply = totalSupply();
        
        amountA = (liquidity * reserveA) / totalSupply;
        amountB = (liquidity * reserveB) / totalSupply;
        
        require(amountA > 0 && amountB > 0, "Insufficient liquidity burned");
        
        _burn(msg.sender, liquidity);
        
        reserveA -= amountA;
        reserveB -= amountB;
        
        tokenA.transfer(msg.sender, amountA);
        tokenB.transfer(msg.sender, amountB);
        
        emit LiquidityRemoved(msg.sender, amountA, amountB, liquidity);
    }
    
    // Swap tokenA for tokenB
    function swapAForB(uint256 amountIn) external nonReentrant returns (uint256 amountOut) {
        require(amountIn > 0, "Invalid input amount");
        
        amountOut = getAmountOut(amountIn, reserveA, reserveB);
        require(amountOut > 0, "Insufficient output amount");
        
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        
        reserveA += amountIn;
        reserveB -= amountOut;
        
        emit Swap(msg.sender, address(tokenA), amountIn, address(tokenB), amountOut);
    }
    
    // Swap tokenB for tokenA
    function swapBForA(uint256 amountIn) external nonReentrant returns (uint256 amountOut) {
        require(amountIn > 0, "Invalid input amount");
        
        amountOut = getAmountOut(amountIn, reserveB, reserveA);
        require(amountOut > 0, "Insufficient output amount");
        
        tokenB.transferFrom(msg.sender, address(this), amountIn);
        tokenA.transfer(msg.sender, amountOut);
        
        reserveB += amountIn;
        reserveA -= amountOut;
        
        emit Swap(msg.sender, address(tokenB), amountIn, address(tokenA), amountOut);
    }
    
    // Calculate output amount with fee
    function getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut
    ) public pure returns (uint256) {
        require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");
        
        // Apply fee
        uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_NUMERATOR);
        
        // Constant product formula
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = (reserveIn * FEE_DENOMINATOR) + amountInWithFee;
        
        return numerator / denominator;
    }
    
    // Get current price of token A in terms of token B
    function getPriceAtoB() external view returns (uint256) {
        require(reserveA > 0, "No liquidity");
        return (reserveB * 1e18) / reserveA;
    }
    
    // Helper functions
    function sqrt(uint256 x) internal pure returns (uint256) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        uint256 y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
        return y;
    }
    
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
}

Interacting with the DEX

Adding Liquidity

const { Web3, TrxProtoBuilder } = require('@beatoz/web3');
require('dotenv').config();

async function addLiquidity(dexAddress, amountA, amountB) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const provider = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  const dex = new web3.beatoz.Contract(DEX_ABI, dexAddress);
  
  // First, approve tokens
  const tokenA = new web3.beatoz.Contract(ERC20_ABI, await dex.methods.tokenA().call());
  const tokenB = new web3.beatoz.Contract(ERC20_ABI, await dex.methods.tokenB().call());
  
  await approveToken(web3, tokenA, dexAddress, amountA, provider);
  await approveToken(web3, tokenB, dexAddress, amountB, provider);
  
  // Add liquidity
  const data = dex.methods.addLiquidity(amountA, amountB).encodeABI();
  
  const account = await web3.beatoz.getAccount(provider.address);
  const status = await web3.beatoz.status();
  
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: provider.address,
    to: dexAddress,
    nonce: account.nonce + 1,
    gas: 500000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: data,
    chainId: status.node_info.network,
  });
  
  const signedTx = provider.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  console.log('✅ Liquidity added!');
  console.log('TX:', result.hash);
}

Performing a Swap

async function swap(dexAddress, amountIn, swapAtoB = true) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const user = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  const dex = new web3.beatoz.Contract(DEX_ABI, dexAddress);
  
  // Get expected output
  const reserveA = await dex.methods.reserveA().call();
  const reserveB = await dex.methods.reserveB().call();
  
  const [reserveIn, reserveOut] = swapAtoB 
    ? [reserveA, reserveB] 
    : [reserveB, reserveA];
  
  const expectedOut = await dex.methods.getAmountOut(
    amountIn, 
    reserveIn, 
    reserveOut
  ).call();
  
  console.log('Expected output:', expectedOut);
  
  // Approve input token
  const tokenIn = swapAtoB 
    ? await dex.methods.tokenA().call()
    : await dex.methods.tokenB().call();
  
  const token = new web3.beatoz.Contract(ERC20_ABI, tokenIn);
  await approveToken(web3, token, dexAddress, amountIn, user);
  
  // Execute swap
  const swapMethod = swapAtoB ? 'swapAForB' : 'swapBForA';
  const data = dex.methods[swapMethod](amountIn).encodeABI();
  
  const account = await web3.beatoz.getAccount(user.address);
  const status = await web3.beatoz.status();
  
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: user.address,
    to: dexAddress,
    nonce: account.nonce + 1,
    gas: 300000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: data,
    chainId: status.node_info.network,
  });
  
  const signedTx = user.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  console.log('✅ Swap executed!');
  console.log('TX:', result.hash);
}

Price Impact Calculation

function calculatePriceImpact(amountIn, reserveIn, reserveOut) {
  // Spot price before swap
  const spotPrice = reserveOut / reserveIn;
  
  // Actual output
  const amountInWithFee = amountIn * 0.997; // 0.3% fee
  const amountOut = (amountInWithFee * reserveOut) / (reserveIn + amountInWithFee);
  
  // Execution price
  const executionPrice = amountOut / amountIn;
  
  // Price impact
  const priceImpact = ((spotPrice - executionPrice) / spotPrice) * 100;
  
  return {
    spotPrice,
    executionPrice,
    priceImpact: priceImpact.toFixed(2) + '%',
    amountOut,
  };
}

// Example
const impact = calculatePriceImpact(
  1000e18,  // 1000 tokens in
  10000e18, // 10000 token A reserve
  20000e18  // 20000 token B reserve
);
console.log('Price Impact:', impact);

Slippage Protection

async function swapWithSlippage(dexAddress, amountIn, maxSlippage = 0.5) {
  const dex = new web3.beatoz.Contract(DEX_ABI, dexAddress);
  
  const reserveA = await dex.methods.reserveA().call();
  const reserveB = await dex.methods.reserveB().call();
  
  const expectedOut = await dex.methods.getAmountOut(
    amountIn, 
    reserveA, 
    reserveB
  ).call();
  
  // Calculate minimum output with slippage
  const minOutput = BigInt(expectedOut) * BigInt(1000 - maxSlippage * 10) / 1000n;
  
  console.log('Expected:', expectedOut);
  console.log('Minimum (with slippage):', minOutput.toString());
  
  // Execute swap with minimum check
  // Note: Would need contract modification to support minOutput parameter
}

Production Considerations

  • Implement proper slippage protection
  • Add minimum output parameters
  • Consider flash loan attacks
  • Audit thoroughly before mainnet deployment

Key Concepts Learned

ConceptDescription
AMMAutomated pricing via mathematical formulas
Constant Productx * y = k invariant
Liquidity PoolsToken pairs locked in contracts
LP TokensProof of liquidity provision
SlippagePrice difference from trade size
Price ImpactHow trades affect pool prices

You now understand DEX fundamentals! 🔄

Next Steps

Beatoz Developer Portal