Level Up Your Token Swap Game: Adding Native Swaps & Withdrawals 🌊💰

Hey there, crypto fam! If you’ve been following along, you already know how to build a basic token swap DApp.

If you haven’t checked out the first part yet, you can find it here.

Now, it’s time to level up! 🚀 Today, we're diving into how to swap native tokens (like CORE, ETH) for ERC-20 tokens (think USDT, USDC) and also how to withdraw those sweet, sweet tokens. So, let’s get started!

What’s on the Agenda? 📝

We’re going to:

  1. Add a function to swap native tokens for ERC-20 tokens.

  2. Implement functions to withdraw both ERC-20 tokens and native tokens from our contract.

  3. Provide the full code, along with a deployment script, and break it down for you!

The Swap Functionality: Native to ERC-20 💱

First up, let’s add a function to our smart contract that allows users to swap their native tokens (like ETH) for ERC-20 tokens. Here’s the magic code:

function swapNativeForToken(address tokenB) external payable {
    require(msg.value > 0, "Amount must be greater than 0");
    uint256 amountIn = msg.value;
    uint256 fee = (amountIn * feePercent) / 10000;
    uint256 amountOut = amountIn - fee;

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

    // Send fee to fee collector in native tokens
    if (fee > 0) {
        payable(feeCollector).transfer(fee);
    }

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

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

What’s Happening Here? 🤔

  1. Check Amount: We start by ensuring the user sends a positive amount of native tokens (like ETH).

  2. Calculate Fee: Next, we calculate the fee (in this case, 0.5%).

  3. Check Contract Balance: We check if our contract has enough of tokenB to swap.

  4. Transfer Fee: If there's a fee, we send it to the fee collector.

  5. Transfer Tokens: Finally, we transfer the tokenB to the user and log the swap event. Easy peasy! 🍋

Time to Withdraw Those Tokens! 💸

Now let’s add functions to allow the contract owner to withdraw both ERC-20 tokens and native tokens. Here’s the code for that:

Withdrawing ERC-20 Tokens:

function withdrawTokens(address token, uint256 amount) external onlyOwner {
    require(IERC20(token).balanceOf(address(this)) >= amount, "Insufficient token balance");
    IERC20(token).transfer(msg.sender, amount);
}

Withdrawing Native Tokens:

function withdrawNative(uint256 amount) external onlyOwner {
    require(address(this).balance >= amount, "Insufficient ETH balance");
    payable(msg.sender).transfer(amount);
}

Why These Functions Rock 🎉

  • ERC-20 Withdrawal: This function allows us (the owner) to withdraw ERC-20 tokens from our contract. We check that there are enough tokens and then send them to our wallet. Simple!

  • Native Withdrawal: Similar to the ERC-20 withdrawal, but this one’s for native tokens (like ETH). We ensure we have enough balance before transferring.

Full Smart Contract Code 🔥

Here’s the complete code, combining all the features we just discussed:

solidityCopy code// 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., 50 = 0.5%)

    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;
    }

    // Swap ERC20 tokens
    function swapTokens(
        address tokenA,
        address tokenB,
        uint256 amountIn
    ) external {
        require(amountIn > 0, "AmountIn must be greater than 0");
        uint256 fee = (amountIn * feePercent) / 10000;
        uint256 amountOut = amountIn - fee;

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

        // 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);
    }

    // Swap native token (ETH) for ERC20 token
    function swapNativeForToken(address tokenB) external payable {
        require(msg.value > 0, "Amount must be greater than 0");
        uint256 amountIn = msg.value;
        uint256 fee = (amountIn * feePercent) / 10000;
        uint256 amountOut = amountIn - fee;

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

        // Send fee to fee collector in native tokens
        if (fee > 0) {
            payable(feeCollector).transfer(fee);
        }

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

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

    // Withdraw ERC20 tokens from the contract
    function withdrawTokens(address token, uint256 amount) external onlyOwner {
        require(IERC20(token).balanceOf(address(this)) >= amount, "Insufficient token balance");
        IERC20(token).transfer(msg.sender, amount);
    }

    // Withdraw native tokens from the contract
    function withdrawNative(uint256 amount) external onlyOwner {
        require(address(this).balance >= amount, "Insufficient ETH balance");
        payable(msg.sender).transfer(amount);
    }

    // Updates the fee percentage
    function updateFee(uint32 newFeePercent) public onlyOwner {
        require(newFeePercent <= 10000, "Fee too high");
        feePercent = newFeePercent;
    }
}

Deploying the Contract 🚀

Alright, let’s get to the fun part—deploying our smart contract! Here’s the deployment script, broken down step-by-step:

const { ethers } = require("hardhat");
  • Imports: We start by importing the ethers library to interact with our Ethereum contracts.
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
  • Main Function: Our async function where the deployment logic happens.
  • Get Signer: We grab the deployer's account (the first one) to deploy the contract.
  • Logging: Outputs the deployer's address to confirm which account is being used.
const ERC20 = await ethers.getContractFactory("Token");
const usdt = await ERC20.deploy("usdt", "USDT");
const usdc = await ERC20.deploy("usdc", "USDC");
  • ERC20 Factory: Prepares to deploy the ERC-20 token contract (make sure you have a Token contract ready).
  • Deploy Tokens: Deploys instances of USDT and USDC. Each call will create a new token with the specified name and symbol.
const fee = 50; // Set the fee percentage (0.5%)
  • Setting Fee: We define our fee percentage here. This is how much we’ll charge for swaps.
const Swap = await ethers.getContractFactory("Swap");
const swap = await Swap.deploy(fee);
  • Swap Factory: Prepares to deploy the Swap contract.
  • Deploy Swap Contract: Deploys our swap contract with the defined fee.
console.log("Contracts deployed:");
console.log("Swap Contract:", await swap.getAddress());
console.log("USDT Token:", await usdt.getAddress());
console.log("USDC Token:", await usdc.getAddress());
  • Logging Deployed Contracts: Confirms that all contracts were deployed successfully by logging their addresses.
const swapContractAddr = await swap.getAddress();
  • Get Swap Address: Retrieves the address of the deployed swap contract to approve and transfer USDT and USDC reserves
await usdt.approve(swapContractAddr, ethers.parseEther("100"));
await usdt.transfer(swapContractAddr, ethers.parseEther("100"));
await usdc.approve(swapContractAddr, ethers.parseEther("100"));
await usdc.transfer(swapContractAddr, ethers.parseEther("100"));
  • Approvals and Transfer Tokens: Grants the swap contract permission to spend the maximum amount of our USDT and USDC tokens. This is crucial for enabling token swaps!

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });
  • Main Function Execution: Calls the main function, handles errors, and ensures the process exits cleanly.

Wrapping It Up 🎁

And there you have it! You now have a smart contract that can swap native tokens for ERC-20 tokens and allow for easy withdrawals. You’ve leveled up your DApp game and are ready to take on the crypto world. 🌍💥

Next up, let’s tackle the exciting part: deploying this contract to the blockchain! In the next part of our journey, we’ll walk through how to deploy your smart contract so you can start making those swaps in the wild.

Feel free to tweak the code, experiment with different fee structures, and get creative! Remember, the crypto space is all about innovation, so keep pushing those boundaries. 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