Build Your Own Token Swap Contract in Solidity

Swap Tokens

In this tutorial, we’re gonna walk you through creating a dope token swap smart contract in Solidity. This bad boy lets users swap one ERC-20 token for another, all while collecting a little fee. Let’s get it!

Prerequisites

Before diving into the code, ensure you have the following tools installed:

  1. Node.js and npm: Node.js allows you to run JavaScript code outside the browser, and npm helps manage packages.

  2. Hardhat: A popular development environment for Ethereum, it helps compile and deploy smart contracts. Don’t stress; we’ll install it together.

Setting Up the Project

Alright, let’s kick things off by initializing our project. Open up your terminal and run the following commands:

mkdir token-swap
cd token-swap
npm init -y
npm install --save-dev hardhat
npx hardhat init
  1. OpenZeppelin Contracts: These are reusable, security-audited smart contracts for Ethereum. We'll use OpenZeppelin's ERC-20 token interface.

To install OpenZeppelin contracts, run:

npm install @openzeppelin/contracts

Swap Smart Contract Code

Let's now dive into the code of the token swap contract. We'll go through it step by step to explain its components.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Swap is Ownable {
    address public feeCollector; // Address to collect fees
    uint32 public feePercent; // Fee percentage (in basis points, e.g., 1000 = 10%)

    event TokensSwapped(
        address indexed user,
        address indexed tokenA,
        address indexed tokenB,
        uint256 amountIn,
        uint256 amountOut,
        uint256 fee
    );

Explanation:

  1. License Declaration: The SPDX-License-Identifier: MIT ensures the contract adheres to the MIT license, allowing others to use and modify it.Not sure about licenses? No sweat! You can totally leave it 'unlicensed' .

  2. Version Pragma: We’re rolling with Solidity version ^0.8.24. The caret (^) means that our contract can run on any compiler version that's 0.8.24 or higher.

  3. Imports:

    • IERC20.sol is the ERC-20 token standard interface from OpenZeppelin. It provides the necessary functions like balanceOf, transfer, and transferFrom.

    • Ownable.sol allows for ownership control, ensuring that only the contract owner can perform certain actions, such as updating the fee.

  4. State Variables:

    • feeCollector: This holds the address of the entity that will collect the swap fees.

    • feePercent: The fee percentage is expressed in basis points (100 basis points = 1%). For example, a feePercent of 1000 means a 10% fee on the swap.

  5. Event Declaration: The TokensSwapped event emits details whenever a swap occurs. It logs the user's address, tokens involved, and the amount swapped.

Constructor and Initial Setup

constructor(uint32 _feePercent) Ownable(msg.sender) {
        require(_feePercent <= 10000, "Fee too high"); 
        feeCollector = msg.sender;
        feePercent = _feePercent;
    }

Explanation:

  • The constructor initializes the contract with a fee percentage and sets the contract deployer as the fee collector.

  • Input validation: We gotta make sure the fee isn’t over 100%. Can’t be greedy, ya know?

Token Swap Logic

function swapTokens(
        address tokenA,
        address tokenB,
        uint256 amountIn
    ) external {
        require(amountIn > 0, "AmountIn must be greater than 0");

        // Calculate fee
        uint256 fee = (amountIn * feePercent) / 10000;
        uint256 amountOut = amountIn - fee;

        require(IERC20(tokenB).balanceOf(address(this)) >= amountOut, "Insufficient token for swap, Try lesser Amount");

        // Transfer tokenA from sender to this contract
        IERC20(tokenA).transferFrom(msg.sender, address(this), amountIn);

        // Send fee to fee collector
        if (fee > 0) {
            IERC20(tokenA).transfer(feeCollector, fee);
        }

        // Transfer tokenB to the receiver
        IERC20(tokenB).transfer(msg.sender, amountOut);

        // Emit event for transparency
        emit TokensSwapped(msg.sender, tokenA, tokenB, amountIn, amountOut, fee);
    }

Explanation:

  1. Input Validation: The function ensures that the amount being swapped is greater than zero.

  2. Fee Calculation:

    • The fee is calculated as a percentage of the input amount (amountIn). The fee is deducted, leaving amountOut, which will be sent to the user.
  3. Check Token Balance: Before proceeding with the swap, the contract checks if it has enough of tokenB to complete the swap. If the contract doesn't have enough tokens, the swap will fail with the message: "Insufficient token for swap, Try lesser Amount".

  4. Transfer tokenA: The transferFrom function is called to move tokenA from the user's wallet to the contract. For this to work, the user must have already approved this contract to spend their tokenA tokens.

  5. Transfer Fee: The contract sends the calculated fee to the feeCollector.

  6. Transfer tokenB: After deducting the fee, the contract transfers the remaining amountOut of tokenB to the user.

  7. Emit Event: Finally, the event TokensSwapped is emitted for transparency, logging the swap details.

Updating the Swap Fee

function updateFee(uint32 newFeePercent) public onlyOwner {
        require(msg.sender == feeCollector, "Not authorized");
        require(newFeePercent <= 10000, "Fee too high");
        feePercent = newFeePercent;
    }  
}

Explanation:

  1. Access Control: Only the contract owner (via Ownable) can update the fee percentage.

  2. Fee Validation: We check that the new fee doesn’t exceed 100%. Gotta keep it in check!

  3. Set New Fee: The feePercent is updated with the new value.

Complete Code

Here’s the full contract again for reference:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Swap is Ownable {
    address public feeCollector; // Address to collect fees
    uint32 public feePercent; // Fee percentage (in basis points, e.g., 10 = 10%)

    event TokensSwapped(
        address indexed user,
        address indexed tokenA,
        address indexed tokenB,
        uint256 amountIn,
        uint256 amountOut,
        uint256 fee
    );

    constructor(uint32 _feePercent) Ownable(msg.sender) {
        require(_feePercent <= 10000, "Fee too high");
        feeCollector = msg.sender;
        feePercent = _feePercent;
    }

    function swapTokens(
        address tokenA,
        address tokenB,
        uint256 amountIn
    ) external {
        require(amountIn > 0, "AmountIn must be greater than 0");

        // Calculate fee
        uint256 fee = (amountIn * feePercent) / 100;
        uint256 amountOut = amountIn - fee;

        require(IERC20(tokenB).balanceOf(address(this)) >= amountOut, "Insufficient token for swap, Try lesser Amount");

        // Transfer tokenA from sender to this contract
        IERC20(tokenA).transferFrom(msg.sender, address(this), amountIn);

        // Send fee to fee collector
        if (fee > 0) {
            IERC20(tokenA).transfer(feeCollector, fee);
        }

        // Transfer tokenB to the receiver
        IERC20(tokenB).transfer(msg.sender, amountOut);

        // Emit event for transparency
        emit TokensSwapped(msg.sender, tokenA, tokenB, amountIn, amountOut, fee);
    }

    function updateFee(uint32 newFeePercent) public onlyOwner {
        require(msg.sender == feeCollector, "Not authorized");
        require(newFeePercent <= 10000, "Fee too high");
        feePercent = newFeePercent;
    }  
}

Pro Tip 💡

Before you dive into that swap, just a quick reminder: make sure your swap contract has some liquidity! No one likes a dry pool, right? So, load it up with enough tokens to handle the swaps you want to initiate.

Let’s keep things flowing! 💧💰

Alright! Now that you’ve got the swap function down, how about taking it a step further? Try creating a new function to update the fee collector's address. Just a heads up: you'll want to use that onlyOwner modifier to keep things secure—check out how it’s done in the updateFee function for some inspo!

Let’s see what you can come up with! 🚀

Wrapping It Up 🎉

And there you have it! We’ve just built a simple token swap contract in Solidity. This contract lets users swap one ERC-20 token for another while adding a fee to the mix. We also covered how to set it up to collect fees and how to tweak the fee percentage using OpenZeppelin's Ownable contract.

Ready to take it up a notch? You can enhance this basic swap contract by integrating Chainlink's oracle price feeds for real-time pricing or by connecting it with Uniswap V3 for more dynamic trading options. Let’s level up!

Happy coding! ✌️

0
Subscribe to my newsletter

Read articles from Akshaya Gangatharan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Akshaya Gangatharan
Akshaya Gangatharan