Comprehensive guide to write ERC20 token presale smart contract on Ethereum blockchain using Solidity
Steven

Steven @steven228312

About: A critical and forward thinking fullstack developer && blockchain engineer

Location:
Mesquite, Texas
Joined:
Nov 8, 2024

Comprehensive guide to write ERC20 token presale smart contract on Ethereum blockchain using Solidity

Publish Date: Nov 9 '24
151 30

Introduction

This article will give you a comprehensive guide to build a presale contract that accepts ETH and major stablecoins step by step.

Key Features
  • Multiple payment options(ETH, USDT, USDC, DAI)
  • Early Investor bonus system
  • Staged token buying campaign
Prerequisites
  • Hardhat development environment
  • Openzeppelin contracts
  • Ethereum development experience
  • Basic understanding of ERC20 tokens
Token features
  • Type: ERC20
  • Name: Silver Phoenix
  • Symbol: SPX
  • Decimal: 18
  • Total Supply: 100 billion
Presale Features
  • Presale Supply: 10 billion (10%)
  • Presale Period: 30 days
  • Presale Stage: 4
  • Softcap: 500000 USDT
  • Hardcap: 1020000 USDT
  • Price and token amounts for each stage:
Stage Price Token Amount
1 0.00008 USDT 3 billion
2 0.00010 USDT 4 billion
3 0.00012 USDT 2 billion
4 0.00014 USDT 1 billion
  • Options for buying tokens: ETH, USDT, USDC, DAI
  • Claim time: After second public sale ends
  • Minimum amount for buying tokens: 100 USDT

Investors who bought tokens before softcap reached are listed on early investors and can get bonus tokens after presale ends if unsold tokens exist.

How to implement step by step

Key functions:
  • buyWithETH
  • buyWithStableCoin
  • Helper functions for calculating token amounts available with ETH or Stable coin and vice versa
  • Claim
  • Withdraw
  • Refund
  • Several helper functions like set and get functions
Buy SPX token with ETH
function buyWithETH() external payable whenNotPaused nonReentrant {
    require(
        block.timestamp >= startTime && block.timestamp <= endTime,
        "Invalid time for buying the token"
    );

    uint256 _estimatedTokenAmount = estimatedTokenAmountAvailableWithETH(
        msg.value
    );
    uint256 _tokensAvailable = tokensAvailable();

    require(
        _estimatedTokenAmount <= _tokensAvailable &&
            _estimatedTokenAmount > 0,
        "Invalid token amount to buy"
    );

    uint256 minUSDTOutput = (_estimatedTokenAmount * 90) / 100;
    // Swap ETH for USDT
    address[] memory path = new address[](2);
    path[0] = router.WETH();
    path[1] = USDT;

    uint256[] memory amounts = router.swapExactETHForTokens{
        value: msg.value
    }(minUSDTOutput, path, address(this), block.timestamp + 15 minutes);

    // Ensure the swap was successful
    require(amounts.length > 1, "Swap failed, no USDT received");
    uint256 _usdtAmount = amounts[1];

    // Calculate final token amount
    uint256 _tokenAmount = estimatedTokenAmountAvailableWithCoin(
        _usdtAmount,
        USDTInterface
    );

    //Update investor records
    _updateInvestorRecords(
        msg.sender,
        _tokenAmount,
        USDTInterface,
        _usdtAmount
    );

    //Update presale stats
    _updatePresaleStats(_tokenAmount, _usdtAmount, 6);

    emit TokensBought(
        msg.sender,
        _tokenAmount,
        _usdtAmount,
        block.timestamp
    );
}
Enter fullscreen mode Exit fullscreen mode

First, this function checks if presale is ongoing or not.
Next, it estimates how much tokens investor can buy with specific ETH amount and checks if this amount is availbale for purchase.
Next, it swaps ETH to USDT using Uniswap V2 Router for buying SPX tokens and returns USDT amount equivalent.
Next, it calculates how much tokens investor can buy with swapped USDT equivalent.
Next, it updates investor and investment status.

function _updateInvestorRecords(
    address investor_,
    uint256 tokenAmount_,
    IERC20 coin_,
    uint256 coinAmount_
) private {
    if (investorTokenBalance[investor_] == 0) {
        investors.push(investor_);
        if (fundsRaised < softcap && !earlyInvestorsMapping[investor_]) {
            earlyInvestorsMapping[investor_] = true;
            earlyInvestors.push(investor_);
        }
    }

    investorTokenBalance[investor_] += tokenAmount_;
    investments[investor_][address(coin_)] += coinAmount_;
}
Enter fullscreen mode Exit fullscreen mode

Next, it updates presale status.

function _updatePresaleStats(
    uint256 tokenAmount_,
    uint256 coinAmount_,
    uint8 coinDecimals_
) private {
    totalTokensSold += tokenAmount_;
    fundsRaised += coinAmount_ / (10 ** (coinDecimals_ - 6));
}
Enter fullscreen mode Exit fullscreen mode

Last, it emits TokensBought event.

Buy SPX token with Stable Coins
function _buyWithCoin(
    IERC20 coin_,
    uint256 tokenAmount_
) internal checkSaleState(tokenAmount_) whenNotPaused nonReentrant {
    uint256 _coinAmount = estimatedCoinAmountForTokenAmount(
        tokenAmount_,
        coin_
    );
    uint8 _coinDecimals = getCoinDecimals(coin_);

    //Check allowances and balances
    require(
        _coinAmount <= coin_.allowance(msg.sender, address(this)),
        "Insufficient allowance"
    );
    require(
        _coinAmount <= coin_.balanceOf(msg.sender),
        "Insufficient balance."
    );

    //Send the coin to the contract
    SafeERC20.safeTransferFrom(
        coin_,
        msg.sender,
        address(this),
        _coinAmount
    );

    //Update the investor status
    _updateInvestorRecords(msg.sender, tokenAmount_, coin_, _coinAmount);

    // Update presale stats
    _updatePresaleStats(tokenAmount_, _coinAmount, _coinDecimals);

    emit TokensBought(
        msg.sender,
        tokenAmount_,
        _coinAmount,
        block.timestamp
    );
}
Enter fullscreen mode Exit fullscreen mode

First, this function checks if presale is ongoing, tokenAmount that investor wants to buy is available, and so on.(modifiers)
Next, it calculates how much coins are needed to buy those amount of tokens and checks if investor has sufficient balance and allowance.
Next, it transfers Stable coins to presale contract.
Then, it updates investor & investment status and presale status, finally, emits TokensBought event.

Each functions for buying token with specific stable coins can be written as follows:

function buyWithUSDT(uint256 tokenAmount_) external whenNotPaused {
    _buyWithCoin(USDTInterface, tokenAmount_);
}
Enter fullscreen mode Exit fullscreen mode
Helper function to calculate SPX token amount with ETH and vice versa
function estimatedTokenAmountAvailableWithETH(
    uint256 ethAmount_
) public view returns (uint256) {
    // Swap ETH for USDT
    address[] memory path = new address[](2);
    path[0] = router.WETH();
    path[1] = USDT;
    uint256[] memory amounts = router.getAmountsOut(ethAmount_, path);
    require(amounts.length > 1, "Invalid path");
    uint256 _usdtAmount = amounts[1];

    // Calculate token amount
    return
        estimatedTokenAmountAvailableWithCoin(_usdtAmount, USDTInterface);
}
Enter fullscreen mode Exit fullscreen mode

This function calculates how much tokens user can buy with specific eth amount using Uniswap V2 Router and estimatedTokenAmountAvailableWithCoin function.

Helper function to calculate SPX token amount with Stable Coin and vice versa
    function estimatedTokenAmountAvailableWithCoin(
        uint256 coinAmount_,
        IERC20 coin_
    ) public view returns (uint256) {
        uint256 tokenAmount = 0;
        uint256 remainingCoinAmount = coinAmount_;
        uint8 _coinDecimals = getCoinDecimals(coin_);

        for (uint8 i = 0; i < thresholds.length; i++) {
            // Get the current token price at the index
            uint256 _priceAtCurrentTier = getCurrentTokenPriceForIndex(i);
            uint256 _currentThreshold = thresholds[i];

            // Determine the number of tokens available at this tier
            uint256 numTokensAvailableAtTier = _currentThreshold >
                totalTokensSold
                ? _currentThreshold - totalTokensSold
                : 0;

            // Calculate the maximum number of tokens that can be bought with the remaining coin amount
            uint256 maxTokensAffordable = (remainingCoinAmount *
                (10 ** (18 - _coinDecimals + 6))) / _priceAtCurrentTier;

            // Determine how many tokens can actually be bought at this tier
            uint256 tokensToBuyAtTier = numTokensAvailableAtTier <
                maxTokensAffordable
                ? numTokensAvailableAtTier
                : maxTokensAffordable;

            // Update amounts
            tokenAmount += tokensToBuyAtTier;
            remainingCoinAmount -=
                (tokensToBuyAtTier * _priceAtCurrentTier) /
                (10 ** (18 - _coinDecimals + 6));

            // If there is no remaining coin amount, break out of the loop
            if (remainingCoinAmount == 0) {
                break;
            }
        }

        return tokenAmount;
    }
Enter fullscreen mode Exit fullscreen mode

This function ensures:

  • Accurate token calculations across different price tiers
  • Proper decimal handling for different stablecoins
  • Maximum token availability limits per tier
  • Efficient use of remaining purchase amount

The implementation supports the presale's tiered pricing structure while maintaining precision in token calculations.

Claim function
function claim(address investor_) external nonReentrant {
    require(
        block.timestamp > claimTime && claimTime > 0,
        "It's not claiming time yet."
    );

    require(
        fundsRaised >= softcap,
        "Can not claim as softcap not reached. Instead you can be refunded."
    );

    uint256 _tokenAmountforUser = getTokenAmountForInvestor(investor_);
    uint256 _bonusTokenAmount = getBonusTokenAmount();

    if (isEarlyInvestors(investor_))
        _tokenAmountforUser += _bonusTokenAmount;
    require(_tokenAmountforUser > 0, "No tokens claim.");
    investorTokenBalance[investor_] = 0;
    earlyInvestorsMapping[investor_] = false;

    SafeERC20.safeTransfer(token, investor_, _tokenAmountforUser);
    emit TokensClaimed(investor_, _tokenAmountforUser);
}
Enter fullscreen mode Exit fullscreen mode

This function

  • checks claim time and softcap requirements
  • calculates total tokens including bonuses
  • resets investor balances and early investor status
  • uses SafeERC20 for token transfers
  • emits TokensClaimed event
Withdraw function
function withdraw() external onlyOwner nonReentrant {
    require(
        block.timestamp > endTime,
        "Cannot withdraw because presale is still in progress."
    );

    require(wallet != address(0), "Wallet not set");

    require(
        fundsRaised > softcap,
        "Can not withdraw as softcap not reached."
    );

    uint256 _usdtBalance = USDTInterface.balanceOf(address(this));
    uint256 _usdcBalance = USDCInterface.balanceOf(address(this));
    uint256 _daiBalance = DAIInterface.balanceOf(address(this));

    require(
        _usdtBalance > 0 && _usdcBalance > 0 && _daiBalance > 0,
        "No funds to withdraw"
    );

    if (_usdtBalance > 0)
        SafeERC20.safeTransfer(USDTInterface, wallet, _usdtBalance);
    if (_usdcBalance > 0)
        SafeERC20.safeTransfer(USDCInterface, wallet, _usdcBalance);
    if (_daiBalance > 0)
        SafeERC20.safeTransfer(DAIInterface, wallet, _daiBalance);
}
Enter fullscreen mode Exit fullscreen mode

This function

  • validates if predefined multisig wallet address is set
  • ensures presale is already ended
  • verifies sufficient funds exist
  • uses SafeERC20 for transfers
Refund function
function refund() external onlyOwner nonReentrant {
    require(
        block.timestamp > endTime,
        "Cannot refund because presale is still in progress."
    );
    require(fundsRaised < softcap, "Softcap reached, refund not available");

    // refund all funds to investors
    for (uint256 i = 0; i < investors.length; i++) {
        address investor = investors[i];

        //Refund USDT
        uint256 _usdtAmount = investments[investor][address(USDTInterface)];
        if (_usdtAmount > 0) {
            investments[investor][address(USDTInterface)] = 0;
            SafeERC20.safeTransfer(USDTInterface, investor, _usdtAmount);
            emit FundsRefunded(investor, _usdtAmount, block.timestamp);
        }

        //Refund USDC
        uint256 _usdcAmount = investments[investor][address(USDCInterface)];
        if (_usdcAmount > 0) {
            investments[investor][address(USDCInterface)] = 0;
            SafeERC20.safeTransfer(USDCInterface, investor, _usdcAmount);
            emit FundsRefunded(investor, _usdcAmount, block.timestamp);
        }

        //Refund DAI
        uint256 _daiAmount = investments[investor][address(DAIInterface)];
        if (_daiAmount > 0) {
            investments[investor][address(DAIInterface)] = 0;
            SafeERC20.safeTransfer(DAIInterface, investor, _daiAmount);
            emit FundsRefunded(investor, _daiAmount, block.timestamp);
        }
    }

    fundsRaised = 0;
    delete investors;
}
Enter fullscreen mode Exit fullscreen mode

This function

  • loops through all investors
  • checks and refunds each stable coin separately
  • Resets investment records to zero
  • Emits FundsRefunded events
  • Clears global state(fundsRaised and investors array)

Conclusion

This SPX token presale smart contract demonstrates a robust and versatile implementation that effectively handles multiple payment methods including ETH, USDT, USDC, and DAI.
This implementation serves as an excellent template for future presale contracts, offering a balance of security, functionality, and user accessibility.
It's architecture ensures fair distribution while protecting both investor and project owner interests through its well-structured validation and distribution mechanisms.

Comments 30 total

  • LovelyBTC
    LovelyBTCNov 9, 2024

    This SPX token presale smart contract stands out as an excellent example of professional DeFi development, incorporating multiple key features that make it highly valuable for developers and entrepreneurs.
    This implementation sets a high standard for presale contract development and documentation in the DeFi space.
    I highly recommend.
    Thank you.

  • Robert Angelo
    Robert AngeloNov 9, 2024

    Really impressive.
    Thank you for sharing.

  • Mitsuru Kudo
    Mitsuru KudoNov 9, 2024

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

  • Ichikawa Hiroshi
    Ichikawa HiroshiNov 9, 2024

    This article is incredibly insightful and packed with valuable information in ERC20 token presale smart contract development! It has provided me with practical strategies that I can apply directly to my work. Highly recommend it to anyone looking to enhance their knowledge and skills in Blockchain development.

  • Arlo Oscar
    Arlo OscarNov 12, 2024

    Looks amazing.

  • eugene garrett
    eugene garrettNov 12, 2024

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

  • Jackson Mori
    Jackson MoriNov 12, 2024

    Thanks for your effort.
    I think this article is good for beginners but also for a professional to review their work and audit smart contract.

  • Jason
    JasonNov 12, 2024

    Thank you very much.
    This is the very basic but most essential part for solidity developers.
    I will wait your next article.

  • Jin William
    Jin WilliamNov 12, 2024

    I am new to here, but it feels like I am with senior developers.
    It arises me new intereste in blockchain technology.
    Thank you Steven.

  • Sebastian Robinson
    Sebastian RobinsonNov 12, 2024

    Thanks.
    Good article.

  • Oleksii Kasian
    Oleksii KasianNov 12, 2024

    Great. Thanks

  • Sebastian Robinson
    Sebastian RobinsonNov 12, 2024

    Comprehensive and detailed guide for smart contract.
    Thanks

  • Creative
    CreativeNov 12, 2024

    Wow, what an incredibly detailed and informative guide!
    This will undoubtedly be a valuable resource for developers looking to navigate the Ethereum blockchain. Thank you for sharing your expertise!

  • Creative
    CreativeNov 14, 2024

    Impressive article.
    Thanks for your effort.

  • Handy Apps
    Handy AppsNov 14, 2024

    This article gave me clear understanding of ERC20 token presale smart contract.
    Thanks for your article.

  • Elon
    ElonNov 14, 2024

    👍👍Thank you

  • Ares Colin
    Ares ColinNov 15, 2024

    Thanks for your article.

  • Brave
    BraveNov 15, 2024

    This guide provides a foundational understanding of creating an ERC20 token presale smart contract using Solidity. You can expand it further by adding features like whitelist functionality, dynamic pricing, or vesting schedules depending on your project requirements.

  • anders nielsen
    anders nielsenNov 15, 2024

    This article gave me clear understanding of writing presale smart contract.
    Thanks

  • Johnny Webster
    Johnny WebsterNov 15, 2024

    Great

  • Minato Shaikh
    Minato ShaikhNov 15, 2024

    This article is really impressive.
    Thanks for your effort
    🎈🎉

  • Elon
    ElonNov 16, 2024

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

  • Ito Inoue
    Ito InoueNov 16, 2024

    Nice article.
    Thanks

  • Minato
    MinatoNov 24, 2024

    Great post.
    Thx

  • White Dream
    White DreamNov 24, 2024

    Great post
    Thanks

  • James Takahashi
    James TakahashiDec 1, 2024

    Good idea.

  • ForgetMeNot
    ForgetMeNotDec 10, 2024

    Thanks for sharing the article

  • Shiro Ennosuke
    Shiro EnnosukeDec 16, 2024

    Thanks for sharing great article

  • Pavlo Posternak
    Pavlo PosternakDec 16, 2024

    I guess its great article for not only begineers, but also professionals.

  • Michael Liang
    Michael LiangMay 24, 2025

    Great post

Add comment