DEV Community

Mark Santiago
Mark Santiago

Posted on

How to write ICO smart contract using Solidity and Hardhat

How to write ICO smart contract using Solidity and Hardhat

Introduction

This article will give you a knowledge of ERC20 token and how to write token smart contract and ICO 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.

Top comments (9)

Collapse
 
sebastian_robinson_64 profile image
Sebastian Robinson

Thanks.
Good article.

Collapse
 
jacksonmoridev0507 profile image
Jackson Mori

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

Collapse
 
arlo_oscar_d8a2de736e7c73 profile image
Arlo Oscar

Looks amazing.

Collapse
 
stevendev0822 profile image
Steven

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

Collapse
 
eugene_garrett_d1a47f08f6 profile image
eugene garrett

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

Collapse
 
dodger213 profile image
Mitsuru Kudo

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

Collapse
 
robert_angelo_484 profile image
Robert Angelo

Really impressive.
Thank you for sharing.

Collapse
 
ito_inoue_0718c4b43dff69b profile image
Ito Inoue

Good article.
Thank you

Collapse
 
steven0822 profile image
Steven

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