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
);
}
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_;
}
Next, it updates presale status.
function _updatePresaleStats(
uint256 tokenAmount_,
uint256 coinAmount_,
uint8 coinDecimals_
) private {
totalTokensSold += tokenAmount_;
fundsRaised += coinAmount_ / (10 ** (coinDecimals_ - 6));
}
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
);
}
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_);
}
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);
}
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;
}
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);
}
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);
}
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;
}
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
andinvestors
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 (23)
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.
Really impressive.
Thank you for sharing.
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.
Thank you so much for the helpful information!
Highly recommended.
Thanks again
Thank you very much.
This is the very basic but most essential part for solidity developers.
I will wait your next article.
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.
Thanks.
Good article.
Looks amazing.
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.
Wow.
I am new to blockchain, but this article gave me comprehensive guide to ERC20 token presale smart contract