DEV Community

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

Posted on

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

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.

Top comments (29)

Collapse
 
btc415 profile image
LovelyBTC

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.

Collapse
 
james_takahashi_77908481e profile image
James Takahashi

Impressive article.
Thanks for your effort.
❀

Collapse
 
james_takahashi_77908481e profile image
James Takahashi

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!

Collapse
 
johnny_webster_27313b58e3 profile image
Johnny Webster

Great

Collapse
 
steven0822 profile image
Steven

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

Collapse
 
minato_876e167d9dcecec491 profile image
Minato

Great post.
Thx

Collapse
 
steven0822 profile image
Steven

πŸ‘πŸ‘Thank you

Collapse
 
ito_inoue_0718c4b43dff69b profile image
Ito Inoue

Nice article.
Thanks

Collapse
 
white_dream_bf5e3b9d832d4 profile image
White Dream

Great post
Thanks

Collapse
 
anders_nielsen_604d456759 profile image
anders nielsen

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