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());
}
}
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;
}
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);
}
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;
}
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;
}
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);
}
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;
}
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 (6)
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
Thanks.
Good article.
Thanks for your article.
Looks nice like always.
👍👍👍
Looks amazing.
Good article.
Thank you
I am new to blockchain, but this article gave me comprehensive guide to ERC20 token presale smart contract development