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
AMM Formula
The constant product formula (used by Uniswap):
x * y = k
Where:
x= Reserve of token Ay= Reserve of token Bk= 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
| Concept | Description |
|---|---|
| AMM | Automated pricing via mathematical formulas |
| Constant Product | x * y = k invariant |
| Liquidity Pools | Token pairs locked in contracts |
| LP Tokens | Proof of liquidity provision |
| Slippage | Price difference from trade size |
| Price Impact | How trades affect pool prices |
You now understand DEX fundamentals! 🔄
