In this tutorial, we will walk you through the process of creating a decentralized application (dApp) for ERC20 token swap. This guide is perfect for blockchain developers who want to learn how to build smart contracts in Solidity and integrate them with a Web3 frontend using Ethers.js. By the end of this tutorial, you will have a fully functional token swap dApp deployed on the Binance Smart Chain (BSC) testnet.
Step 1. The Foundation of ERC20 Token Swap – Smart Contracts & ABIs
What are smart contracts?
Smart contracts are self-executing agreements written in code, running on blockchain networks like Ethereum and Binance Smart Chain (BSC). These contracts automatically enforce terms and conditions without the need for intermediaries.
Smart contracts are:
- Immutable: Once deployed, they cannot be altered.
- Transparent: Their code and transaction history are visible on the blockchain.
- Automated: Execute specific actions when predefined conditions are met.
Example Use Case: Token Swaps
Imagine you want to exchange one cryptocurrency for another. A smart contract can automate the process, ensuring:
- The correct amount of tokens is transferred.
- The transaction occurs only if sufficient funds and approvals exist.
What Are ABIs (Application Binary Interfaces)?
An ABI is a crucial component when interacting with smart contracts. It acts as a bridge between the frontend of your application (like a website or mobile app) and the smart contract deployed on the blockchain.
Think of an ABI as a “menu” for your smart contract. It describes:
- Functions: What actions the contract can perform (e.g.,
buyTokens
,sellTokens
). - Inputs and Outputs: The parameters you need to provide and the data returned.
- Events: Notifications emitted by the contract during execution (e.g., a
Transfer
event).
Why Are ABIs Important?
- Decoding Blockchain Data: ABIs decode the raw data stored and retrieved from the blockchain, translating it into human-readable information.
- Frontend-Backend Communication: Without ABIs, your frontend cannot call functions or fetch data from the smart contract.
- Interoperability: ABIs enable compatibility with standard libraries like Ethers.js and Web3.js, simplifying the development process.
ERC20 Token Swap Smart Contract with Solidity
Head over to remix IDE and create a file named tokenSwap.sol or whatever name you wish to give it.
For building our ERC20 Token Swap, we are going to write an ERC20 token contract that allows trading/swapping using another ERC20 token, USDT in this case, which is a stablecoin deployed on Binance smart chain. You can access this USDT contract on this link https://testnet.bscscan.com/address/0x337610d27c682e347c9cd60bd4b3b107c9d34ddd
Our ERC20 Token Swap builds on following logic:
- EOA sends USDT to the contract
- ERC20 Token Swap Contract mints our tokens to the EOA after deduction of swap fees on 1:1 ratio thus completing buy action
- User sends our tokens to the smart contract, intending to sell back
- ERC20 Token Swap Contract burns the tokens and send USDT back to the EOA after deducting swap fees, thus completing sell action
Lets declare license, solidity version and also import necessary dependecies from openzepplin library.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
We are importing ERC20, SafeERC20, Ownable & ReentrancyGuard libraries from openzepplin. You can visit openzepplin to learn the importance and use case of these libraries.
Now we declare our contract and create state variables such as treasury to receive swapFees, and USDT contract address that is acceptable for trading with our own token.
contract TokenSwap is ERC20, ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
// State variables
address public treasury;
uint256 public immutable swapFees = 1; // 1% swapFees on buy and sell
IERC20 public immutable USDT = IERC20(0x337610d27c682E347C9cD60BD4b3b107C9d34dDd);
Next we will declare the events. In Solidity, events are special constructs that allow smart contracts to communicate with external applications or the Ethereum Virtual Machine (EVM) log system. They serve as logs or notifications that are emitted during the execution of a smart contract, providing important updates or information about what happened within the contract.
Key Features of Events
- EVM Logs: Events write data to the blockchain log, which is not directly accessible by smart contracts but can be read by external systems like a frontend or backend using Web3.js or Ethers.js.
- Efficient Data Tracking: Events are cost-efficient for storing data as they don’t consume as much gas as storing the same data in a contract’s storage.
- Indexed Parameters: Solidity allows specific event parameters to be indexed, making it easier to filter and search logs for specific data.
// Events
event TokensBought(address indexed _buyer, uint256 _amount, uint256 _receivedAmount);
event TokensSold(address indexed _buyer, uint256 _amount, uint256 _receivedAmount);
event SwapFeesUpdated(uint256 _fees);
event EmergencyWithdraw(address indexed token, uint256 amount);
Next we declare errors that inform users of the checks that we will put into our callable functions. Think of these as validtions performed to check the parameters sent to the function by the EOAs (Externally Owned Accounts).
// Custom errors for gas optimization
error InsufficientBalance(uint256 required, uint256 available);
error InvalidAmount();
error InsufficientLiquidity();
error TransferFailed();
error InvalidToken();
Now we write the construction along with the arguments for ERC20 & Ownable and set token Name, Symbol and Contract Owner as well as treasury to msg.sender.
constructor() ERC20 ("Arrnaya", unicode"ɑׁׅ֮ꭈׁׅꭈׁׅꪀׁׅɑׁׅ֮ᨮ꫶ׁׅ֮ɑׁׅ֮") Ownable(msg.sender) {
treasury = msg.sender;
}
It is time to write the core function of our contract that allow buying and selling of our tokens against USDT tokens.
function calculateOutputAmount(uint256 amount) public pure returns (uint256) {
return amount - ((amount * swapFees) / 100);
}
function buyTokens(uint256 amount) external nonReentrant returns (uint256) {
if (amount == 0) revert InvalidAmount();
// Cache balances
uint256 userBalance = USDT.balanceOf(msg.sender);
if (userBalance < amount) {
revert InsufficientBalance(amount, userBalance);
}
uint256 TokensToSend = calculateOutputAmount(amount);
uint256 TreasuryAmount = (amount * swapFees) / 100;
// Transfer tokens using SafeERC20
USDT.safeTransferFrom(msg.sender, address(this), amount);
USDT.safeTransfer(treasury, TreasuryAmount);
_mint(msg.sender, TokensToSend);
emit TokensBought(msg.sender, amount, TokensToSend);
return TokensToSend;
}
function sellTokens(uint256 amount) external nonReentrant returns (uint256) {
if (amount == 0) revert InvalidAmount();
// Cache balances
uint256 userTokenBalance = this.balanceOf(msg.sender);
if (userTokenBalance < amount) {
revert InsufficientBalance(amount, userTokenBalance);
}
uint256 UsdtAmount = calculateOutputAmount(amount);
uint256 contractUsdtBalance = USDT.balanceOf(address(this));
uint256 TreasuryAmount = (amount * swapFees) / 100;
if (contractUsdtBalance < UsdtAmount) {
revert InsufficientLiquidity();
}
_burn(msg.sender, amount);
// Transfer tokens using SafeERC20
USDT.safeTransfer(msg.sender, UsdtAmount);
USDT.safeTransfer(treasury, TreasuryAmount);
emit TokensSold(msg.sender, amount, UsdtAmount);
return UsdtAmount;
}
As a fallback measure, we also write a function to withdraw any other ERC20 tokens sent to this contract by mistake so that no tokens get stuck in this contrat, including our own tokens.
function withdrawERC20Tokens(address _token, uint256 amount) external onlyOwner {
if(IERC20(_token) == USDT) {
revert InvalidToken(); // Can't allow owner to drain Liquidity
}
IERC20(_token).safeTransfer(treasury, amount);
emit EmergencyWithdraw(_token, amount);
}
Finally we write a getter function to fetch USDT reserves of the contract.
function getContractBalances() external view returns (uint256 usdtBalance) {
return USDT.balanceOf(address(this));
}
Here is the complete smart contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TokenSwap is ERC20, ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
// State variables
address public treasury;
uint256 public immutable swapFees = 1; // 1% swapFees on buy and sell
IERC20 public immutable USDT = IERC20(0x337610d27c682E347C9cD60BD4b3b107C9d34dDd);
// Events
event TokensBought(address indexed _buyer, uint256 _amount, uint256 _receivedAmount);
event TokensSold(address indexed _buyer, uint256 _amount, uint256 _receivedAmount);
event SwapFeesUpdated(uint256 _fees);
event EmergencyWithdraw(address indexed token, uint256 amount);
// Custom errors for gas optimization
error InsufficientBalance(uint256 required, uint256 available);
error InvalidAmount();
error InsufficientLiquidity();
error TransferFailed();
error InvalidToken();
constructor() ERC20 ("Arrnaya", unicode"ɑׁׅ֮ꭈׁׅꭈׁׅꪀׁׅɑׁׅ֮ᨮ꫶ׁׅ֮ɑׁׅ֮") Ownable(msg.sender) {
treasury = msg.sender;
}
/**
* @dev Calculates the amount to be received after fees
* @param amount Input amount
* @return Output amount after fees
*/
function calculateOutputAmount(uint256 amount) public pure returns (uint256) {
return amount - ((amount * swapFees) / 100);
}
/**
* @dev Buy token
* @param amount Amount of USDT to swap
* @return Amount of Tokens received
*/
function buyTokens(uint256 amount) external nonReentrant returns (uint256) {
if (amount == 0) revert InvalidAmount();
// Cache balances
uint256 userBalance = USDT.balanceOf(msg.sender);
if (userBalance < amount) {
revert InsufficientBalance(amount, userBalance);
}
uint256 TokensToSend = calculateOutputAmount(amount);
uint256 TreasuryAmount = (amount * swapFees) / 100;
// Transfer tokens using SafeERC20
USDT.safeTransferFrom(msg.sender, address(this), amount);
USDT.safeTransfer(treasury, TreasuryAmount);
_mint(msg.sender, TokensToSend);
emit TokensBought(msg.sender, amount, TokensToSend);
return TokensToSend;
}
/**
* @dev Sell tokens
* @param amount Amount of TOKENS to swap
* @return Amount of USDT received
*/
function sellTokens(uint256 amount) external nonReentrant returns (uint256) {
if (amount == 0) revert InvalidAmount();
// Cache balances
uint256 userTokenBalance = this.balanceOf(msg.sender);
if (userTokenBalance < amount) {
revert InsufficientBalance(amount, userTokenBalance);
}
uint256 UsdtAmount = calculateOutputAmount(amount);
uint256 contractUsdtBalance = USDT.balanceOf(address(this));
uint256 TreasuryAmount = (amount * swapFees) / 100;
if (contractUsdtBalance < UsdtAmount) {
revert InsufficientLiquidity();
}
_burn(msg.sender, amount);
// Transfer tokens using SafeERC20
USDT.safeTransfer(msg.sender, UsdtAmount);
USDT.safeTransfer(treasury, TreasuryAmount);
emit TokensSold(msg.sender, amount, UsdtAmount);
return UsdtAmount;
}
/**
* @dev Emergency withdraw function for any ERC20 token
* @param _token Token address to withdraw
* @param amount Amount to withdraw
*/
function withdrawERC20Tokens(address _token, uint256 amount) external onlyOwner {
if(IERC20(_token) == USDT) {
revert InvalidToken(); // Can't allow owner to drain Liquidity
}
IERC20(_token).safeTransfer(treasury, amount);
emit EmergencyWithdraw(_token, amount);
}
/**
* @dev View function to get contract token balances
* @return usdtBalance USDT balance
*/
function getContractBalances() external view returns (uint256 usdtBalance) {
return USDT.balanceOf(address(this));
}
}
Step 2: Building the Frontend for our ERC20 Token Swap
Our frontend includes an interactive user interface (UI) for wallet connection, token swapping, and real-time balance updates. Let’s break it down:
HTML Structure (index.html)
Our structure includes section for “connect wallet”, “disconnection”, showing token and USDT balances of the user, swap section for token, and displaying balances of USDT reserve in the token contract for allowing sells. We will also integrate QR code generation for the token address, so users can copy it and add it to their wallets easily. In the part 2 you will find the links to the code for HTML & CSS that you can use.
This is it for the first part of building ERC20 Token Swap. as the post is getting long, we will continue with the rest in part-2 of this blog post.
Link to PART-2