How to write ERC20 Token Presale smart contract using Solidity and Hardhat
Mark Santiago

Mark Santiago @marksantiago02

About: A forward thinking full stack blockchain engineer

Location:
Angeles, Philippines
Joined:
Nov 20, 2024

How to write ERC20 Token Presale smart contract using Solidity and Hardhat

Publish Date: Nov 21 '24
160 24

Introduction

This article will give you a knowledge of ERC20 token and how to write ERC20 Token Presale smart contract using Solidity and Hardhat.

Theory

What is an ERC20 Token?

  • ERC-20 is a technical standard; it is used for all smart contracts on the Ethereum blockchain for token implementation and provides a list of rules that all Ethereum-based tokens must follow.
  • You can check all the ERC20 functions before moving ahead.

What is an Initial Coin Offering (ICO)?

  • An Initial Coin Offering (ICO) is a fundraising mechanism in the cryptocurrency industry, akin to an Initial Public Offering (IPO) in the traditional financial sector.

Development of Smart Contracts

ERC20 token contract

  • Token Specification
    • Token Name : MARK Token
    • Token Symbol : MRK
    • Token Decimal : 18
    • Total Supply : 100,000,000,000
    • Token Type : ERC20
  • Token Contract

We will use OpenZeppelin ERC20 contract to create our token and mint 100 billion tokens to the owner of the contract.

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MKT is ERC20 {
    uint256 private _totalSupply = 100_000_000_000;

    constructor() ERC20("MARK Token", "MKT") {
        _mint(msg.sender, _totalSupply * 10 ** decimals());
    }
}
Enter fullscreen mode Exit fullscreen mode

Token presale contract

  • Presale Specification

    • Presale Supply : 10 billion (10%)
    • Presale Period : 30 days
    • Softcap : 300000 USDT
    • Hardcap : 1000000 USDT
    • Buy Token with ETH and USDT
  • Key functions

    • Buy
    • Round management
    • Claim
    • Withdraw
  • Implementation

We are going to use Chainlink Oracle to get the latest price of USDT and ETH. Alternatively you can use Uniswap or PancakeSwap to get the price of USDT and ETH.

Buy MARK Token with ETH
  function buy_with_eth()
        external
        payable
        nonReentrant
        whenNotPaused
        canPurchase(_msgSender(), msg.value)
        returns (bool)
    {

        uint256 amount_in_usdt = (msg.value * get_eth_in_usdt()) / 1e30;
        require(
            round_list[current_round_index].usdt_round_raised + amount_in_usdt <
                round_list[current_round_index].usdt_round_cap,
            "BUY ERROR : Too much money already deposited."
        );

        uint256 amount_in_tokens = (amount_in_usdt *
            round_list[current_round_index].usdt_to_token_rate) * 1e3;

        users_list[_msgSender()].usdt_deposited += amount_in_usdt;
        users_list[_msgSender()].tokens_amount += amount_in_tokens;

        round_list[current_round_index].usdt_round_raised += amount_in_usdt;

        (bool sent,) = round_list[current_round_index].wallet.call{value: msg.value}("");
        require(sent, "Failed to send Ether");

        emit Deposit(_msgSender(), 1, amount_in_usdt, amount_in_tokens);

        return true;
    }
Enter fullscreen mode Exit fullscreen mode

First, the function has several checks through modifiers:

  • nonReentrant prevents reentrancy attacks
  • whenNotPaused ensures the contract isn't paused
  • canPurchase verifies the presale is active and valid purchase amount

Next, it calculates the USDT equivalent of sent ETH using Chainlink oracle price feeds

  function get_eth_in_usdt() internal view returns (uint256) {
        (, int256 price, , , ) = price_feed.latestRoundData();
        price = price * 1e10;
        return uint256(price);
    }
Enter fullscreen mode Exit fullscreen mode

And checks if the purchase amount is within the round cap.

Next, it calculates token amount based on the USDT equivalent using the current round's exchange rate:

Next, it updates the states of the user and the round:

  • Records user's USDT deposit and token allocation
  • Updates the total USDT raised in current round
  • Transfers the ETH to the round's wallet address

Finally, it emits a Deposit event with purchase details and returns true for successful transaction.

Buy MARK token with USDT

Similar to the ETH purchase, we can define the buy function with USDT as follows. The only difference is that this handles direct USDT transfers instead of using price oracles for conversion.

 function buy_with_usdt(uint256 amount_)
        external
        nonReentrant
        whenNotPaused
        canPurchase(_msgSender(), amount_)
        returns (bool)
    {
        uint256 amount_in_usdt = amount_;
        require(
            round_list[current_round_index].usdt_round_raised + amount_in_usdt <
                round_list[current_round_index].usdt_round_cap,
            "BUY ERROR : Too much money already deposited."
        );

        uint256 allowance = usdt_interface.allowance(msg.sender, address(this));

        require(amount_ <= allowance, "BUY ERROR: Allowance is too small!");

        (bool success_receive, ) = address(usdt_interface).call(
            abi.encodeWithSignature(
                "transferFrom(address,address,uint256)",
                msg.sender,
                round_list[current_round_index].wallet,
                amount_in_usdt
            )
        );

        require(success_receive, "BUY ERROR: Transaction has failed!");

        uint256 amount_in_tokens = (amount_in_usdt *
            round_list[current_round_index].usdt_to_token_rate) * 1e3;

        users_list[_msgSender()].usdt_deposited += amount_in_usdt;
        users_list[_msgSender()].tokens_amount += amount_in_tokens;

        round_list[current_round_index].usdt_round_raised += amount_in_usdt;

        emit Deposit(_msgSender(), 3, amount_in_usdt, amount_in_tokens);

        return true;
    }
Enter fullscreen mode Exit fullscreen mode
Claim Token
function claim_tokens() external returns (bool) {
        require(presale_ended, "CLAIM ERROR : Presale has not ended!");
        require(
            users_list[_msgSender()].tokens_amount != 0,
            "CLAIM ERROR : User already claimed tokens!"
        );
        require(
            !users_list[_msgSender()].has_claimed,
            "CLAIM ERROR : User already claimed tokens"
        );

        uint256 tokens_to_claim = users_list[_msgSender()].tokens_amount;
        users_list[_msgSender()].tokens_amount = 0;
        users_list[_msgSender()].has_claimed = true;

        (bool success, ) = address(token_interface).call(
            abi.encodeWithSignature(
                "transfer(address,uint256)",
                msg.sender,
                tokens_to_claim
            )
        );
        require(success, "CLAIM ERROR : Couldn't transfer tokens to client!");

        return true;
    }
Enter fullscreen mode Exit fullscreen mode

This function

  • Checks if presale has ended
  • Verifies user has tokens to claim and hasn't claimed before
  • Retrieves and stores user's claimable token amount
  • Resets user's token balance to 0 and marks as claimed
  • Transfers tokens to user using the token contract interface
  • Returns true on successful claim
Withdraw Token
 function withdrawToken(address tokenContract, uint256 amount) external onlyOwner {
        IERC20(tokenContract).transfer(_msgSender(), amount);
    }
Enter fullscreen mode Exit fullscreen mode

This function

  • Is restricted to contract owner only through onlyOwner modifier
  • Allows owner to withdraw any ERC20 token from the contract
  • Takes token contract address and amount as parameters
  • Transfers specified amount to the owner's address
Round Management

We also need to define functions to manage the rounds:

  function start_next_round(
        address payable wallet_,
        uint256 usdt_to_token_rate_,
        uint256 usdt_round_cap_
    ) external onlyOwner {
        current_round_index = current_round_index + 1;

        round_list.push(
            Round(wallet_, usdt_to_token_rate_, 0, usdt_round_cap_ * (10**6))
        );
    }

  function set_current_round(
        address payable wallet_,
        uint256 usdt_to_token_rate_,
        uint256 usdt_round_cap_
    ) external onlyOwner {
        round_list[current_round_index].wallet = wallet_;
        round_list[current_round_index]
            .usdt_to_token_rate = usdt_to_token_rate_;
        round_list[current_round_index].usdt_round_cap = usdt_round_cap_ * (10**6);
    }

  function get_current_round()
        external
        view
        returns (
            address,
            uint256,
            uint256,
            uint256
        )
    {
        return (
            round_list[current_round_index].wallet,
            round_list[current_round_index].usdt_to_token_rate,
            round_list[current_round_index].usdt_round_raised,
            round_list[current_round_index].usdt_round_cap
        );
    }

  function get_current_raised() external view returns (uint256) {
        return round_list[current_round_index].usdt_round_raised;
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

This ERC20 token contract and Presale contract is a comprehensive and secure solution for conducting token presales. It provides features for managing presale rounds, depositing USDT, claiming tokens, withdrawing tokens, and managing rounds. The contract is designed to be flexible and customizable for different presale scenarios.

Comments 24 total

  • Arlo Oscar
    Arlo OscarNov 21, 2024

    Looks amazing.

  • eugene garrett
    eugene garrettNov 21, 2024

    I am new to blockchain, but this article gave me comprehensive guide to ERC20 token presale smart contract development

  • Ito Inoue
    Ito InoueNov 21, 2024

    Good article.
    Thank you

  • Jackson Mori
    Jackson MoriNov 21, 2024

    Thanks for your article.
    Looks nice like always.
    👍👍👍

  • Sebastian Robinson
    Sebastian RobinsonNov 21, 2024

    Thanks.
    Good article.

  • Steven
    StevenNov 21, 2024

    Thanks.
    I also have some experience in ICO smart contract.
    I guess we can collaborate with each other.
    Anyway, your article is impressive
    Thanks again

  • Mitsuru Kudo
    Mitsuru KudoNov 21, 2024

    Thank you so much for the helpful information!
    Highly recommended.
    Thanks again

  • Elon
    ElonNov 21, 2024

    Thanks for sharing the article.
    I am new to blockchain but it's really easy to understand.

  • Robert Angelo
    Robert AngeloNov 21, 2024

    Really impressive.
    Thank you for sharing.

  • LovelyBTC
    LovelyBTCNov 23, 2024

    Thanks for your article

  • Minato
    MinatoNov 24, 2024

    Great post.
    Thx

  • James Takahashi
    James TakahashiNov 28, 2024

    It allows me to understand how to write smart contract for ICO.
    Thanks for you article.

  • White Dream
    White DreamDec 8, 2024

    Great article

  • Zane Massey
    Zane MasseyDec 8, 2024

    Thanks for sharing the article

  • Tony Allen
    Tony AllenDec 8, 2024

    Thank you.
    Good article

  • Minato Shaikh
    Minato ShaikhDec 8, 2024

    Nice article.
    🌹🌹

  • Sky Room
    Sky RoomDec 8, 2024

    Its really impressive

  • ForgetMeNot
    ForgetMeNotDec 10, 2024

    Good article.
    Thank you

  • Jason
    JasonDec 14, 2024

    Great.
    Thx

  • Jin William
    Jin WilliamDec 14, 2024

    Interesting

  • super
    superDec 16, 2024

    Thanks for sharing great article

  • Kennedy Antonio
    Kennedy AntonioFeb 13, 2025

    Really helpful

  • Wiljan Santiago
    Wiljan SantiagoFeb 13, 2025

    Great work.
    Really helpful for those who try to hold ERC20 token presale.
    Thanks for valuable article

  • Andrew Costure
    Andrew CostureJul 26, 2025

    Awesome.
    Thank you for your posting.
    It was very useful for us.

Add comment