Part 4: Hands-on Lab - Build Your Own DEX
"The best way to learn is by doing. Mari kita bangun DEX sungguhan!"
๐ Overviewโ
Di Part 4 ini, kita akan membangun Mini DEX (Decentralized Exchange) yang lengkap dengan:
- โ Dua ERC-20 tokens (CampusCoin & MockUSDC)
- โ Liquidity Pool dengan AMM formula (x ร y = k)
- โ Add & Remove Liquidity functions
- โ Swap functions dengan slippage protection
- โ LP Token untuk Liquidity Providers
- โ Trading fee 0.3%
Durasi: 150 menit (2.5 jam)
Tech Stack:
- Foundry (development & testing)
- Solidity ^0.8.30
- OpenZeppelin Contracts
- Lisk Sepolia Testnet
๐๏ธ Table of Contentsโ
- Setup Project dengan Foundry
- Membuat Token ERC-20
- Implementasi SimpleDEX Contract
- Testing Contracts
- Deployment Script
- Deploy ke Lisk Sepolia
- Interaksi dengan DEX
- Troubleshooting
1. Setup Project dengan Foundryโ
1.1 Instalasi Foundryโ
macOS / Linuxโ
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
# Restart terminal, lalu jalankan:
foundryup
# Verifikasi instalasi
forge --version
cast --version
anvil --version
Expected output:
forge 0.2.0 (latest)
cast 0.2.0 (latest)
anvil 0.2.0 (latest)
Windows (menggunakan WSL)โ
# 1. Install WSL (jika belum)
# Buka PowerShell sebagai Administrator:
wsl --install
# 2. Restart komputer
# 3. Buka WSL terminal dan install Foundry:
curl -L https://foundry.paradigm.xyz | bash
source ~/.bashrc
foundryup
# 4. Verifikasi
forge --version
1.2 Membuat Project Baruโ
# Buat folder project
mkdir simple-dex-lisk
cd simple-dex-lisk
# Initialize Foundry project
forge init
# Struktur project:
# simple-dex-lisk/
# โโโ foundry.toml # Konfigurasi Foundry
# โโโ src/ # Smart contracts
# โโโ test/ # Test files
# โโโ script/ # Deployment scripts
# โโโ lib/ # Dependencies
# โโโ .env # Environment variables (buat nanti)
1.3 Install Dependenciesโ
# Install OpenZeppelin Contracts
forge install OpenZeppelin/openzeppelin-contracts --no-commit
# Verify installation
ls lib/
# Output should show: forge-std openzeppelin-contracts
1.4 Konfigurasi Foundryโ
Edit file foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.30"
optimizer = true
optimizer_runs = 200
gas_limit = "18446744073709551615"
# Remappings untuk imports
remappings = [
'@openzeppelin/=lib/openzeppelin-contracts/',
'forge-std/=lib/forge-std/src/'
]
# Lisk Sepolia configuration
[rpc_endpoints]
lisk_sepolia = "https://rpc.sepolia-api.lisk.com"
[etherscan]
lisk_sepolia = { key = "${BLOCKSCOUT_API_KEY}", url = "https://sepolia-blockscout.lisk.com/api" }
1.5 Setup Environment Variablesโ
Buat file .env:
# Lisk Sepolia RPC URL
LISK_SEPOLIA_RPC_URL=https://rpc.sepolia-api.lisk.com
# Your private key (NEVER commit this!)
PRIVATE_KEY=your_private_key_here
# Blockscout API (optional, untuk verification)
BLOCKSCOUT_API_KEY=123
# Chain ID
CHAIN_ID=4202
โ ๏ธ IMPORTANT: Tambahkan .env ke .gitignore!
echo ".env" >> .gitignore
Load environment variables:
source .env
2. Membuat Token ERC-20โ
2.1 CampusCoin (Token A)โ
Buat file src/CampusCoin.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title CampusCoin
* @dev Token sederhana untuk ekosistem kampus
*/
contract CampusCoin is ERC20, Ownable {
// Total supply maksimum
uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18; // 1 juta token
// Event untuk tracking mint
event TokensMinted(address indexed to, uint256 amount);
constructor() ERC20("Campus Coin", "CAMP") Ownable(msg.sender) {
// Mint initial supply ke deployer
uint256 initialSupply = 100_000 * 10**18; // 100 ribu token
_mint(msg.sender, initialSupply);
emit TokensMinted(msg.sender, initialSupply);
}
/**
* @dev Mint token baru (hanya owner)
* @param to Address yang menerima token
* @param amount Jumlah token yang dimint
*/
function mint(address to, uint256 amount) external onlyOwner {
require(to != address(0), "Cannot mint to zero address");
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
emit TokensMinted(to, amount);
}
/**
* @dev Burn token dari caller
* @param amount Jumlah token yang diburn
*/
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
/**
* @dev Cek sisa supply yang bisa dimint
*/
function remainingSupply() external view returns (uint256) {
return MAX_SUPPLY - totalSupply();
}
}
2.2 MockUSDC (Token B)โ
Buat file src/MockUSDC.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MockUSDC
* @dev Mock USDC token for testing (6 decimals like real USDC)
*/
contract MockUSDC is ERC20, Ownable {
constructor() ERC20("Mock USDC", "USDC") Ownable(msg.sender) {
// Mint initial supply to deployer
_mint(msg.sender, 1_000_000 * 10**6); // 1 million USDC
}
function decimals() public pure override returns (uint8) {
return 6; // USDC has 6 decimals
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}
๐ก Key Points:
- CampusCoin menggunakan 18 decimals (default ERC-20)
- MockUSDC menggunakan 6 decimals (seperti USDC asli)
- Ini penting untuk kalkulasi price di DEX!
3. Implementasi SimpleDEX Contractโ
3.1 SimpleDEX Contractโ
Buat file src/SimpleDEX.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title SimpleDEX
* @dev Mini DEX sederhana dengan AMM (Automated Market Maker)
* Menggunakan formula x * y = k (constant product)
*/
contract SimpleDEX is ERC20, ReentrancyGuard, Ownable {
// Token yang diperdagangkan
IERC20 public immutable tokenA; // Campus Coin
IERC20 public immutable tokenB; // Mock USDC
// Reserves (cadangan token di pool)
uint256 public reserveA;
uint256 public reserveB;
// Fee untuk setiap swap (0.3%)
uint256 public constant FEE_PERCENT = 3; // 0.3%
uint256 public constant FEE_DENOMINATOR = 1000; // 100%
// Minimum liquidity untuk mencegah division by zero
uint256 public constant MINIMUM_LIQUIDITY = 10**3;
// Events
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,
uint256 amountAIn,
uint256 amountBIn,
uint256 amountAOut,
uint256 amountBOut
);
constructor(address _tokenA, address _tokenB)
ERC20("SimpleDEX LP", "SDEX-LP")
Ownable(msg.sender)
{
require(_tokenA != _tokenB, "Identical tokens");
require(_tokenA != address(0) && _tokenB != address(0), "Zero address");
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
/**
* @dev Tambah likuiditas ke pool
* @param amountA Jumlah token A yang ingin ditambahkan
* @param amountB Jumlah token B yang ingin ditambahkan
* @return liquidity Jumlah LP token yang diterima
*/
function addLiquidity(uint256 amountA, uint256 amountB)
external
nonReentrant
returns (uint256 liquidity)
{
require(amountA > 0 && amountB > 0, "Amounts must be greater than 0");
// Transfer token dari user
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
uint256 totalLiquidity = totalSupply();
if (totalLiquidity == 0) {
// Pool pertama kali - set initial price
liquidity = sqrt(amountA * amountB) - MINIMUM_LIQUIDITY;
_mint(address(0xdead), MINIMUM_LIQUIDITY); // Lock minimum liquidity to dead address
} else {
// Pool sudah ada - maintain price ratio
liquidity = min(
(amountA * totalLiquidity) / reserveA,
(amountB * totalLiquidity) / reserveB
);
}
require(liquidity > 0, "Insufficient liquidity minted");
// Mint LP token ke user
_mint(msg.sender, liquidity);
// Update reserves
reserveA += amountA;
reserveB += amountB;
emit LiquidityAdded(msg.sender, amountA, amountB, liquidity);
}
/**
* @dev Hapus likuiditas dari pool
* @param liquidity Jumlah LP token yang ingin diburn
* @return amountA Jumlah token A yang diterima
* @return amountB Jumlah token B yang diterima
*/
function removeLiquidity(uint256 liquidity)
external
nonReentrant
returns (uint256 amountA, uint256 amountB)
{
require(liquidity > 0, "Liquidity must be greater than 0");
require(balanceOf(msg.sender) >= liquidity, "Insufficient LP tokens");
uint256 totalLiquidity = totalSupply();
// Calculate token amounts berdasarkan proporsi
amountA = (liquidity * reserveA) / totalLiquidity;
amountB = (liquidity * reserveB) / totalLiquidity;
require(amountA > 0 && amountB > 0, "Insufficient liquidity burned");
// Burn LP tokens
_burn(msg.sender, liquidity);
// Transfer tokens ke user
tokenA.transfer(msg.sender, amountA);
tokenB.transfer(msg.sender, amountB);
// Update reserves
reserveA -= amountA;
reserveB -= amountB;
emit LiquidityRemoved(msg.sender, amountA, amountB, liquidity);
}
/**
* @dev Swap token A untuk token B
* @param amountAIn Jumlah token A yang diswap
* @param minAmountBOut Minimum token B yang diharapkan (slippage protection)
*/
function swapAforB(uint256 amountAIn, uint256 minAmountBOut)
external
nonReentrant
{
require(amountAIn > 0, "Amount must be greater than 0");
require(reserveA > 0 && reserveB > 0, "Insufficient liquidity");
// Calculate output amount menggunakan formula AMM
uint256 amountBOut = getAmountOut(amountAIn, reserveA, reserveB);
require(amountBOut >= minAmountBOut, "Slippage too high");
// Transfer input token dari user
tokenA.transferFrom(msg.sender, address(this), amountAIn);
// Transfer output token ke user
tokenB.transfer(msg.sender, amountBOut);
// Update reserves
reserveA += amountAIn;
reserveB -= amountBOut;
emit Swap(msg.sender, amountAIn, 0, 0, amountBOut);
}
/**
* @dev Swap token B untuk token A
* @param amountBIn Jumlah token B yang diswap
* @param minAmountAOut Minimum token A yang diharapkan
*/
function swapBforA(uint256 amountBIn, uint256 minAmountAOut)
external
nonReentrant
{
require(amountBIn > 0, "Amount must be greater than 0");
require(reserveA > 0 && reserveB > 0, "Insufficient liquidity");
// Calculate output amount
uint256 amountAOut = getAmountOut(amountBIn, reserveB, reserveA);
require(amountAOut >= minAmountAOut, "Slippage too high");
// Transfer input token dari user
tokenB.transferFrom(msg.sender, address(this), amountBIn);
// Transfer output token ke user
tokenA.transfer(msg.sender, amountAOut);
// Update reserves
reserveB += amountBIn;
reserveA -= amountAOut;
emit Swap(msg.sender, 0, amountBIn, amountAOut, 0);
}
/**
* @dev Calculate output amount untuk swap (dengan fee)
* @param amountIn Jumlah token input
* @param reserveIn Reserve token input
* @param reserveOut Reserve token output
* @return amountOut Jumlah token output setelah fee
*/
function getAmountOut(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut
) public pure returns (uint256 amountOut) {
require(amountIn > 0, "Amount must be greater than 0");
require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");
// Apply fee (0.3%)
uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_PERCENT);
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = (reserveIn * FEE_DENOMINATOR) + amountInWithFee;
amountOut = numerator / denominator;
}
/**
* @dev Get current price (token B per token A)
*/
function getPrice() external view returns (uint256) {
require(reserveA > 0, "No liquidity");
// Price dengan 18 decimals untuk precision
return (reserveB * 1e18) / reserveA;
}
/**
* @dev Get pool info untuk UI
*/
function getPoolInfo() external view returns (
uint256 _reserveA,
uint256 _reserveB,
uint256 _totalLiquidity,
uint256 _price
) {
_reserveA = reserveA;
_reserveB = reserveB;
_totalLiquidity = totalSupply();
_price = reserveA > 0 ? (reserveB * 1e18) / reserveA : 0;
}
// === UTILITY 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;
}
}
๐ Key Concepts dalam Code:
- LP Tokens: User mendapat LP tokens sebagai proof of liquidity
- Minimum Liquidity: 1000 wei di-lock forever untuk mencegah division by zero
- Fee Mechanism: 0.3% fee dari setiap swap masuk ke pool (benefit untuk LPs)
- Slippage Protection: User set minimum output yang acceptable
- ReentrancyGuard: Protect dari reentrancy attacks
4. Testing Contractsโ
4.1 Basic Token Testโ
Buat file test/CampusCoin.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/CampusCoin.sol";
contract CampusCoinTest is Test {
CampusCoin public campusCoin;
address public owner;
address public user1;
address public user2;
function setUp() public {
owner = address(this);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
campusCoin = new CampusCoin();
}
function test_InitialState() public view {
// Check basic properties
assertEq(campusCoin.name(), "Campus Coin");
assertEq(campusCoin.symbol(), "CAMP");
assertEq(campusCoin.decimals(), 18);
// Check initial supply
uint256 expectedInitial = 100_000 * 10**18;
assertEq(campusCoin.totalSupply(), expectedInitial);
assertEq(campusCoin.balanceOf(owner), expectedInitial);
}
function test_Mint() public {
uint256 mintAmount = 1000 * 10**18;
campusCoin.mint(user1, mintAmount);
assertEq(campusCoin.balanceOf(user1), mintAmount);
assertEq(campusCoin.totalSupply(), 100_000 * 10**18 + mintAmount);
}
function test_MintFailsWhenNotOwner() public {
vm.prank(user1);
// Use the new custom error format from OpenZeppelin v5
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user1));
campusCoin.mint(user2, 1000 * 10**18);
}
function test_MintFailsWhenExceedsMaxSupply() public {
uint256 excessAmount = campusCoin.MAX_SUPPLY() - campusCoin.totalSupply() + 1;
vm.expectRevert("Exceeds max supply");
campusCoin.mint(user1, excessAmount);
}
function test_Burn() public {
uint256 burnAmount = 1000 * 10**18;
campusCoin.burn(burnAmount);
assertEq(campusCoin.balanceOf(owner), 100_000 * 10**18 - burnAmount);
}
function test_RemainingSupply() public {
uint256 expected = campusCoin.MAX_SUPPLY() - campusCoin.totalSupply();
assertEq(campusCoin.remainingSupply(), expected);
// After minting
campusCoin.mint(user1, 1000 * 10**18);
assertEq(campusCoin.remainingSupply(), expected - 1000 * 10**18);
}
}
4.2 DEX Integration Testโ
Buat file test/SimpleDEX.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/CampusCoin.sol";
import "../src/MockUSDC.sol";
import "../src/SimpleDEX.sol";
contract SimpleDEXTest is Test {
CampusCoin public campusCoin;
MockUSDC public usdc;
SimpleDEX public dex;
address public owner;
address public alice;
address public bob;
uint256 public constant CAMP_AMOUNT = 1000 * 10**18; // 1000 CAMP
uint256 public constant USDC_AMOUNT = 2000 * 10**6; // 2000 USDC
function setUp() public {
owner = address(this);
alice = makeAddr("alice");
bob = makeAddr("bob");
// Deploy contracts
campusCoin = new CampusCoin();
usdc = new MockUSDC();
dex = new SimpleDEX(address(campusCoin), address(usdc));
// Setup balances
campusCoin.mint(alice, 10_000 * 10**18);
campusCoin.mint(bob, 5_000 * 10**18);
usdc.mint(alice, 20_000 * 10**6);
usdc.mint(bob, 10_000 * 10**6);
// Approve DEX
vm.prank(alice);
campusCoin.approve(address(dex), type(uint256).max);
vm.prank(alice);
usdc.approve(address(dex), type(uint256).max);
vm.prank(bob);
campusCoin.approve(address(dex), type(uint256).max);
vm.prank(bob);
usdc.approve(address(dex), type(uint256).max);
}
function test_AddLiquidity() public {
vm.prank(alice);
uint256 liquidity = dex.addLiquidity(CAMP_AMOUNT, USDC_AMOUNT);
// Check LP tokens minted
assertGt(liquidity, 0);
assertEq(dex.balanceOf(alice), liquidity);
// Check reserves updated
assertEq(dex.reserveA(), CAMP_AMOUNT);
assertEq(dex.reserveB(), USDC_AMOUNT);
// Check tokens transferred
assertEq(campusCoin.balanceOf(address(dex)), CAMP_AMOUNT);
assertEq(usdc.balanceOf(address(dex)), USDC_AMOUNT);
}
function test_SwapAforB() public {
// Add liquidity first
vm.prank(alice);
dex.addLiquidity(CAMP_AMOUNT, USDC_AMOUNT);
// Bob swaps CAMP for USDC
uint256 swapAmount = 100 * 10**18; // 100 CAMP
uint256 expectedOut = dex.getAmountOut(swapAmount, CAMP_AMOUNT, USDC_AMOUNT);
uint256 bobUsdcBefore = usdc.balanceOf(bob);
vm.prank(bob);
dex.swapAforB(swapAmount, expectedOut);
// Check USDC received
assertEq(usdc.balanceOf(bob), bobUsdcBefore + expectedOut);
// Check reserves updated
assertEq(dex.reserveA(), CAMP_AMOUNT + swapAmount);
assertEq(dex.reserveB(), USDC_AMOUNT - expectedOut);
}
function test_CompleteScenario() public {
console.log("=== Complete DEX Scenario ===");
// Alice adds liquidity
vm.prank(alice);
uint256 liquidity = dex.addLiquidity(CAMP_AMOUNT, USDC_AMOUNT);
console.log("Alice LP tokens:", liquidity);
// Bob swaps
uint256 swapAmount = 50 * 10**18;
uint256 expectedOut = dex.getAmountOut(swapAmount, CAMP_AMOUNT, USDC_AMOUNT);
vm.prank(bob);
dex.swapAforB(swapAmount, expectedOut);
console.log("Bob swapped 50 CAMP for", expectedOut / 10**6, "USDC");
// Alice removes liquidity
vm.prank(alice);
(uint256 campOut, uint256 usdcOut) = dex.removeLiquidity(liquidity / 2);
assertGt(campOut, 0);
assertGt(usdcOut, 0);
}
}
4.3 Jalankan Testsโ
# Compile contracts
forge build
# Run all tests
forge test
# Run with verbose output
forge test -vvv
# Run specific test
forge test --match-test test_AddLiquidity -vvv
# Run with gas report
forge test --gas-report
# Check coverage
forge coverage
Expected output:
[โ ] Compiling...
[โ ข] Compiling 10 files with Solc 0.8.30
[โ ] Solc 0.8.30 finished in 1.45s
Compiler run successful!
Ran 5 tests for test/SimpleDEX.t.sol:SimpleDEXTest
[PASS] test_AddLiquidity() (gas: 234567)
[PASS] test_SwapAforB() (gas: 156789)
[PASS] test_CompleteScenario() (gas: 567890)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 2.14s
5. Deployment Scriptโ
5.1 Create Deployment Scriptโ
Buat file script/DeployDEX.s.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Script, console} from "forge-std/Script.sol";
import {CampusCoin} from "../src/CampusCoin.sol";
import {MockUSDC} from "../src/MockUSDC.sol";
import {SimpleDEX} from "../src/SimpleDEX.sol";
contract DeployDEX is Script {
// Contract instances
CampusCoin public campusCoin;
MockUSDC public usdc;
SimpleDEX public dex;
function run() public returns (address, address, address) {
console.log("========================================");
console.log("Deploying Simple DEX to Lisk Sepolia...");
console.log("========================================");
console.log("");
// Get deployer info
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);
console.log("Deployer address:", deployer);
console.log("Network: Lisk Sepolia Testnet (Chain ID: 4202)");
// Check balance
uint256 balance = deployer.balance;
console.log("Deployer balance:", balance / 1e18, "ETH");
if (balance < 0.01 ether) {
console.log("");
console.log("WARNING: Low balance!");
console.log("Get test ETH from faucet:");
console.log("https://sepolia-faucet.lisk.com");
console.log("");
}
vm.startBroadcast(deployerPrivateKey);
// Step 1: Deploy CampusCoin
console.log("");
console.log("Step 1: Deploying CampusCoin...");
console.log("-----------------------------------");
campusCoin = new CampusCoin();
console.log("CampusCoin deployed at:", address(campusCoin));
// Step 2: Deploy MockUSDC
console.log("");
console.log("Step 2: Deploying MockUSDC...");
console.log("-------------------------------");
usdc = new MockUSDC();
console.log("MockUSDC deployed at:", address(usdc));
// Step 3: Deploy SimpleDEX
console.log("");
console.log("Step 3: Deploying SimpleDEX...");
console.log("--------------------------------");
dex = new SimpleDEX(address(campusCoin), address(usdc));
console.log("SimpleDEX deployed at:", address(dex));
vm.stopBroadcast();
// Step 4: Verification
console.log("");
console.log("Step 4: Deployment verification...");
console.log("------------------------------------");
_verifyDeployment();
// Step 5: Next steps
console.log("");
console.log("Step 5: Next steps...");
console.log("----------------------");
_printInstructions();
return (address(campusCoin), address(usdc), address(dex));
}
function _verifyDeployment() internal view {
console.log("CampusCoin:");
console.log(" Name :", campusCoin.name());
console.log(" Symbol :", campusCoin.symbol());
console.log(" Decimals :", campusCoin.decimals());
console.log(" Initial Supply:", campusCoin.totalSupply() / 10**18, "CAMP");
console.log("");
console.log("MockUSDC:");
console.log(" Name :", usdc.name());
console.log(" Symbol :", usdc.symbol());
console.log(" Decimals :", usdc.decimals());
console.log(" Initial Supply:", usdc.totalSupply() / 10**6, "USDC");
console.log("");
console.log("SimpleDEX:");
console.log(" LP Token Name :", dex.name());
console.log(" LP Token Symbol:", dex.symbol());
console.log(" Token A :", address(dex.tokenA()));
console.log(" Token B :", address(dex.tokenB()));
}
function _printInstructions() internal view {
console.log("DEPLOYED CONTRACT ADDRESSES:");
console.log(" CampusCoin :", address(campusCoin));
console.log(" MockUSDC :", address(usdc));
console.log(" SimpleDEX :", address(dex));
console.log("");
console.log("BLOCK EXPLORER:");
console.log(" CampusCoin :", "https://sepolia-blockscout.lisk.com/address/%s", address(campusCoin));
console.log(" MockUSDC :", "https://sepolia-blockscout.lisk.com/address/%s", address(usdc));
console.log(" SimpleDEX :", "https://sepolia-blockscout.lisk.com/address/%s", address(dex));
console.log("");
console.log("NEXT STEPS:");
console.log(" 1. To add initial liquidity, run:");
console.log(" forge script script/AddLiquidity.s.sol --rpc-url lisk_sepolia --broadcast --legacy");
console.log("");
console.log(" 2. Interact with your DEX:");
console.log(" - Add liquidity: dex.addLiquidity(campAmount, usdcAmount)");
console.log(" - Swap CAMP->USDC: dex.swapAforB(campAmount, minUsdcOut)");
console.log(" - Swap USDC->CAMP: dex.swapBforA(usdcAmount, minCampOut)");
console.log(" - Remove liquidity: dex.removeLiquidity(lpAmount)");
console.log("");
console.log("Save these addresses for later use!");
}
}
5.2 Create Add Liquidity Scriptโ
Buat file script/AddLiquidity.s.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Script, console} from "forge-std/Script.sol";
import {CampusCoin} from "../src/CampusCoin.sol";
import {MockUSDC} from "../src/MockUSDC.sol";
import {SimpleDEX} from "../src/SimpleDEX.sol";
contract AddLiquidity is Script {
// Existing contract addresses on Lisk Sepolia (Latest Deployment)
address constant CAMP_ADDRESS = 0x58cCF6ffF745C97Be8CA1ef1cE39346cb90d3ff7;
address constant USDC_ADDRESS = 0x0Eb09fF73E7c574263a635bb60eaa73dB155Ee69;
address constant DEX_ADDRESS = 0x56C3e0D38cbdFce27CC870F2dbaD0428f082E973;
// Liquidity amounts
uint256 constant CAMP_AMOUNT = 1000 * 10**18; // 1,000 CAMP
uint256 constant USDC_AMOUNT = 2000 * 10**6; // 2,000 USDC
function run() public {
console.log("Adding liquidity to existing DEX on Lisk Sepolia...");
console.log("");
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);
console.log("Deployer:", deployer);
console.log("DEX:", DEX_ADDRESS);
console.log("");
CampusCoin camp = CampusCoin(CAMP_ADDRESS);
MockUSDC usdc = MockUSDC(USDC_ADDRESS);
SimpleDEX dex = SimpleDEX(DEX_ADDRESS);
vm.startBroadcast(deployerPrivateKey);
// Check balances
uint256 campBalance = camp.balanceOf(deployer);
uint256 usdcBalance = usdc.balanceOf(deployer);
console.log("Current balances:");
console.log("CAMP:", campBalance / 10**18);
console.log("USDC:", usdcBalance / 10**6);
console.log("");
// Mint if needed
if (campBalance < CAMP_AMOUNT) {
console.log("Minting CAMP tokens...");
camp.mint(deployer, CAMP_AMOUNT + 5000 * 10**18);
}
if (usdcBalance < USDC_AMOUNT) {
console.log("Minting USDC tokens...");
usdc.mint(deployer, USDC_AMOUNT + 10000 * 10**6);
}
// Approve
console.log("Approving tokens...");
camp.approve(DEX_ADDRESS, type(uint256).max);
usdc.approve(DEX_ADDRESS, type(uint256).max);
// Add liquidity
console.log("Adding liquidity...");
uint256 liquidity = dex.addLiquidity(CAMP_AMOUNT, USDC_AMOUNT);
console.log("Success! LP tokens minted:", liquidity);
console.log("");
vm.stopBroadcast();
// Verify
(uint256 reserveA, uint256 reserveB, uint256 totalLP, uint256 price) = dex.getPoolInfo();
console.log("Pool Info:");
console.log("CAMP Reserve:", reserveA / 10**18);
console.log("USDC Reserve:", reserveB / 10**6);
console.log("Total LP:", totalLP);
console.log("Price:", price / 1e18, "USDC per CAMP");
}
}
6. Deploy ke Lisk Sepoliaโ
6.1 Get Test ETHโ
- Visit Lisk Sepolia Faucet: https://sepolia-faucet.lisk.com
- Paste your wallet address
- Request ETH
- Wait ~30 seconds untuk konfirmasi
Check balance:
cast balance YOUR_ADDRESS --rpc-url https://rpc.sepolia-api.lisk.com
6.2 Deploy Contractsโ
# Method 1: Using deployment script (RECOMMENDED)
forge script script/DeployDEX.s.sol \
--rpc-url https://rpc.sepolia-api.lisk.com \
--broadcast \
--legacy \
--skip-simulation
# Method 2: Deploy individually
forge create src/CampusCoin.sol:CampusCoin \
--rpc-url https://rpc.sepolia-api.lisk.com \
--private-key $PRIVATE_KEY
# Simpan address, lalu deploy MockUSDC
forge create src/MockUSDC.sol:MockUSDC \
--rpc-url https://rpc.sepolia-api.lisk.com \
--private-key $PRIVATE_KEY
# Deploy SimpleDEX dengan constructor args
forge create src/SimpleDEX.sol:SimpleDEX \
--constructor-args $CAMP_ADDRESS $USDC_ADDRESS \
--rpc-url https://rpc.sepolia-api.lisk.com \
--private-key $PRIVATE_KEY
Expected output:
========================================
Deploying Simple DEX to Lisk Sepolia...
========================================
Deployer address: 0x742d35Cc6635C0532925a3b8D40168675c8C44e7
Network: Lisk Sepolia Testnet (Chain ID: 4202)
Deployer balance: 0.5 ETH
Step 1: Deploying CampusCoin...
-----------------------------------
CampusCoin deployed at: 0x58cCF6ffF745C97Be8CA1ef1cE39346cb90d3ff7
Step 2: Deploying MockUSDC...
-------------------------------
MockUSDC deployed at: 0x0Eb09fF73E7c574263a635bb60eaa73dB155Ee69
Step 3: Deploying SimpleDEX...
--------------------------------
SimpleDEX deployed at: 0x56C3e0D38cbdFce27CC870F2dbaD0428f082E973
Step 4: Deployment verification...
------------------------------------
CampusCoin:
Name : Campus Coin
Symbol : CAMP
Decimals : 18
Initial Supply: 100000 CAMP
MockUSDC:
Name : Mock USDC
Symbol : USDC
Decimals : 6
Initial Supply: 1000000 USDC
SimpleDEX:
LP Token Name : SimpleDEX LP
LP Token Symbol: SDEX-LP
Token A : 0x58cCF6ffF745C97Be8CA1ef1cE39346cb90d3ff7
Token B : 0x0Eb09fF73E7c574263a635bb60eaa73dB155Ee69
Step 5: Next steps...
----------------------
DEPLOYED CONTRACT ADDRESSES:
CampusCoin : 0x58cCF6ffF745C97Be8CA1ef1cE39346cb90d3ff7
MockUSDC : 0x0Eb09fF73E7c574263a635bb60eaa73dB155Ee69
SimpleDEX : 0x56C3e0D38cbdFce27CC870F2dbaD0428f082E973
BLOCK EXPLORER:
CampusCoin : https://sepolia-blockscout.lisk.com/address/0x58cCF6ffF745C97Be8CA1ef1cE39346cb90d3ff7
MockUSDC : https://sepolia-blockscout.lisk.com/address/0x0Eb09fF73E7c574263a635bb60eaa73dB155Ee69
SimpleDEX : https://sepolia-blockscout.lisk.com/address/0x56C3e0D38cbdFce27CC870F2dbaD0428f082E973
NEXT STEPS:
1. To add initial liquidity, run:
forge script script/AddLiquidity.s.sol --rpc-url lisk_sepolia --broadcast --legacy
2. Interact with your DEX:
- Add liquidity: dex.addLiquidity(campAmount, usdcAmount)
- Swap CAMP->USDC: dex.swapAforB(campAmount, minUsdcOut)
- Swap USDC->CAMP: dex.swapBforA(usdcAmount, minCampOut)
- Remove liquidity: dex.removeLiquidity(lpAmount)
Save these addresses for later use!
6.3 Add Liquidity (Optional)โ
Setelah deployment, Anda bisa menambahkan initial liquidity menggunakan script yang sudah dibuat:
# Tambahkan liquidity ke DEX yang sudah di-deploy
forge script script/AddLiquidity.s.sol \
--rpc-url https://rpc.sepolia-api.lisk.com \
--broadcast \
--legacy
# Atau gunakan alias dari foundry.toml
forge script script/AddLiquidity.s.sol \
--rpc-url lisk_sepolia \
--broadcast \
--legacy
Expected output:
Adding liquidity to existing DEX on Lisk Sepolia...
Deployer: 0x742d35Cc6635C0532925a3b8D40168675c8C44e7
DEX: 0x56C3e0D38cbdFce27CC870F2dbaD0428f082E973
Current balances:
CAMP: 100000
USDC: 1000000
Minting CAMP tokens...
Minting USDC tokens...
Approving tokens...
Adding liquidity...
Success! LP tokens minted: 44721359549995793928
Pool Info:
CAMP Reserve: 1000
USDC Reserve: 2000
Total LP: 44721359549995794928
Price: 2 USDC per CAMP
6.4 Verify Contracts (Optional)โ
# Verify CampusCoin
forge verify-contract \
$CAMPUS_COIN_ADDRESS \
src/CampusCoin.sol:CampusCoin \
--chain 4202 \
--verifier blockscout \
--verifier-url https://sepolia-blockscout.lisk.com/api
# Verify MockUSDC
forge verify-contract \
$MOCK_USDC_ADDRESS \
src/MockUSDC.sol:MockUSDC \
--chain 4202 \
--verifier blockscout \
--verifier-url https://sepolia-blockscout.lisk.com/api
# Verify SimpleDEX
forge verify-contract \
$SIMPLE_DEX_ADDRESS \
src/SimpleDEX.sol:SimpleDEX \
--chain 4202 \
--verifier blockscout \
--verifier-url https://sepolia-blockscout.lisk.com/api \
--constructor-args $(cast abi-encode "constructor(address,address)" $CAMPUS_COIN_ADDRESS $MOCK_USDC_ADDRESS)
7. Interaksi dengan DEXโ
7.1 Setup Environmentโ
# Set contract addresses (ganti dengan hasil deployment Anda)
export CAMP_TOKEN=0x1234567890123456789012345678901234567890
export USDC_TOKEN=0x5678901234567890123456789012345678901234
export DEX_CONTRACT=0x9abc012345678901234567890123456789012345
export RPC_URL=https://rpc.sepolia-api.lisk.com
7.2 Read Functionsโ
# Check token balances
cast call $CAMP_TOKEN "balanceOf(address)(uint256)" YOUR_ADDRESS --rpc-url $RPC_URL
# Check DEX reserves
cast call $DEX_CONTRACT "getPoolInfo()(uint256,uint256,uint256,uint256)" --rpc-url $RPC_URL
# Get current price
cast call $DEX_CONTRACT "getPrice()(uint256)" --rpc-url $RPC_URL | cast to-dec
7.3 Write Functionsโ
Mint Tokensโ
# Mint CAMP (owner only)
cast send $CAMP_TOKEN \
"mint(address,uint256)" \
YOUR_ADDRESS \
1000000000000000000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
# Mint USDC (owner only)
cast send $USDC_TOKEN \
"mint(address,uint256)" \
YOUR_ADDRESS \
1000000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
Add Liquidityโ
# 1. Approve tokens
cast send $CAMP_TOKEN \
"approve(address,uint256)" \
$DEX_CONTRACT \
1000000000000000000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
cast send $USDC_TOKEN \
"approve(address,uint256)" \
$DEX_CONTRACT \
2000000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
# 2. Add liquidity: 100 CAMP + 200 USDC
cast send $DEX_CONTRACT \
"addLiquidity(uint256,uint256)" \
100000000000000000000 \
200000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
# 3. Check LP tokens received
cast call $DEX_CONTRACT \
"balanceOf(address)(uint256)" \
YOUR_ADDRESS \
--rpc-url $RPC_URL
Perform Swapsโ
# Swap 10 CAMP for USDC
# 1. Check expected output
cast call $DEX_CONTRACT \
"getAmountOut(uint256,uint256,uint256)(uint256)" \
10000000000000000000 \
1000000000000000000000 \
2000000000 \
--rpc-url $RPC_URL
# 2. Perform swap
cast send $DEX_CONTRACT \
"swapAforB(uint256,uint256)" \
10000000000000000000 \
19000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
8. Troubleshootingโ
Common Issuesโ
Issue 1: "Insufficient balance"โ
Problem: Tidak punya cukup test ETH
Solution:
# Check balance
cast balance YOUR_ADDRESS --rpc-url $RPC_URL
# Get from faucet
# Visit: https://sepolia-faucet.lisk.com
Issue 2: "Slippage too high"โ
Problem: Price berubah saat transaction pending
Solution:
# Increase slippage tolerance (lower minAmountOut)
# Atau check pool reserves dulu:
cast call $DEX_CONTRACT "getPoolInfo()(uint256,uint256,uint256,uint256)" --rpc-url $RPC_URL
Issue 3: "Insufficient liquidity"โ
Problem: Pool belum punya liquidity
Solution:
# Add liquidity terlebih dahulu
# Atau check apakah ada reserves:
cast call $DEX_CONTRACT "reserveA()(uint256)" --rpc-url $RPC_URL
cast call $DEX_CONTRACT "reserveB()(uint256)" --rpc-url $RPC_URL
Issue 4: Verification gagalโ
Problem: Contract verification error
Solution:
# Make sure compiler version match
forge --version
# Try manual verification di Blockscout UI:
# https://sepolia-blockscout.lisk.com/address/YOUR_CONTRACT/verify-via-flattened-code
๐ Kesimpulanโ
Selamat! Anda telah berhasil:
- โ Setup Foundry development environment
- โ Membuat 2 ERC-20 tokens (CAMP & USDC)
- โ Implementasi SimpleDEX dengan AMM formula (x ร y = k)
- โ Writing comprehensive tests
- โ Deploy ke Lisk Sepolia testnet
- โ Verifikasi contracts di Blockscout
- โ Interaksi dengan DEX via command line
Yang Telah Dibangun:
| Component | Description |
|---|---|
| CampusCoin | ERC-20 token dengan max supply & minting |
| MockUSDC | ERC-20 dengan 6 decimals (seperti USDC asli) |
| SimpleDEX | AMM DEX dengan liquidity pools & swap functions |
| LP Tokens | ERC-20 tokens untuk Liquidity Providers |
| Testing Suite | Unit & integration tests dengan Foundry |
| Deployment Scripts | Automated deployment dengan verification |
Gas Usage:
- Add Liquidity: ~150,000 gas
- Remove Liquidity: ~120,000 gas
- Swap A for B: ~100,000 gas
- Swap B for A: ~100,000 gas
Key Learnings:
- ๐ AMM formula: x ร y = k
- ๐ง Liquidity pools & LP tokens
- ๐ฑ Token swaps dengan slippage protection
- ๐ Security: ReentrancyGuard, access control
- ๐งช Testing dengan Foundry
- ๐ Deployment ke testnet
๐ Next Stepsโ
Improvements you can add:
- Price oracle integration
- Multi-hop swaps (A โ B โ C)
- Concentrated liquidity (like Uniswap V3)
- Frontend web interface
- Flash loan support
- Governance token & voting
Prepared by: Ethereum Jakarta x Lisk Last Updated: November 2025 Version: 1.0