DEV Community

Cover image for ๐Ÿช™ Day 13 of #30DaysOfSolidity โ€” Building a Token Sale (Sell Your ERC-20 for ETH with Foundry)
Saurav Kumar
Saurav Kumar

Posted on

๐Ÿช™ Day 13 of #30DaysOfSolidity โ€” Building a Token Sale (Sell Your ERC-20 for ETH with Foundry)

๐Ÿงฉ Overview

Welcome to Day 13 of my #30DaysOfSolidity journey!

Today, weโ€™ll build something that powers almost every token project โ€” a Token Sale Contract (or Pre-Sale Contract) where users can buy ERC-20 tokens with Ether.

Weโ€™ll use Foundry โ€” a blazing-fast framework for smart contract development.
By the end, youโ€™ll understand how to:

  • Sell your ERC-20 tokens for ETH ๐Ÿ’ฐ
  • Manage pricing, sales, and withdrawals
  • Deploy using Foundry

๐Ÿš€ What Weโ€™re Building

Weโ€™re creating two contracts:

  1. MyToken.sol โ€” ERC-20 token contract
  2. TokenSale.sol โ€” lets users buy tokens with ETH

The owner will:

  • Set a price (tokens per ETH)
  • Fund the sale contract with tokens
  • Withdraw ETH and unsold tokens

๐Ÿงฑ Project Structure

day-13-token-sale/ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ MyToken.sol โ”‚ โ””โ”€โ”€ TokenSale.sol โ”œโ”€โ”€ script/ โ”‚ โ””โ”€โ”€ Deploy.s.sol โ”œโ”€โ”€ test/ โ”‚ โ””โ”€โ”€ TokenSale.t.sol โ”œโ”€โ”€ foundry.toml โ””โ”€โ”€ README.md 
Enter fullscreen mode Exit fullscreen mode

โš™๏ธ Foundry Setup (Step-by-Step)

If you donโ€™t have Foundry yet, hereโ€™s how to set it up ๐Ÿ‘‡

1๏ธโƒฃ Install Foundry

curl -L https://foundry.paradigm.xyz | bash foundryup 
Enter fullscreen mode Exit fullscreen mode

2๏ธโƒฃ Create a new project

forge init day-13-token-sale cd day-13-token-sale 
Enter fullscreen mode Exit fullscreen mode

3๏ธโƒฃ Install OpenZeppelin (ERC-20 contracts)

forge install OpenZeppelin/openzeppelin-contracts 
Enter fullscreen mode Exit fullscreen mode

๐Ÿช™ Step 1 โ€” Create the Token

File: src/MyToken.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import "openzeppelin-contracts/contracts/access/Ownable.sol"; /// @title MyToken - Simple ERC20 Token contract MyToken is ERC20, Ownable { constructor(string memory name_, string memory symbol_, uint256 initialSupply) ERC20(name_, symbol_) { _mint(msg.sender, initialSupply * 10 ** decimals()); } function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } } 
Enter fullscreen mode Exit fullscreen mode

This is a simple ERC-20 token with an initial supply minted to the deployer.


๐Ÿ’ธ Step 2 โ€” Create the Token Sale Contract

File: src/TokenSale.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "openzeppelin-contracts/contracts/access/Ownable.sol"; /// @title TokenSale - Sell tokens for ETH contract TokenSale is Ownable { IERC20 public token; uint256 public tokensPerEth; bool public saleActive; event TokensPurchased(address indexed buyer, uint256 ethSpent, uint256 tokensBought); event PriceUpdated(uint256 oldPrice, uint256 newPrice); event SaleToggled(bool active); event EtherWithdrawn(address indexed to, uint256 amount); event TokensWithdrawn(address indexed to, uint256 amount); constructor(address tokenAddress, uint256 _tokensPerEth) { require(tokenAddress != address(0), "Invalid token address"); token = IERC20(tokenAddress); tokensPerEth = _tokensPerEth; saleActive = true; } function buyTokens() public payable { require(saleActive, "Sale not active"); require(msg.value > 0, "Send ETH to buy tokens"); uint256 tokensToBuy = (msg.value * tokensPerEth) / 1 ether; require(tokensToBuy > 0, "Not enough ETH for 1 token"); require(token.balanceOf(address(this)) >= tokensToBuy, "Not enough tokens"); token.transfer(msg.sender, tokensToBuy); emit TokensPurchased(msg.sender, msg.value, tokensToBuy); } function setPrice(uint256 _tokensPerEth) external onlyOwner { require(_tokensPerEth > 0, "Invalid price"); emit PriceUpdated(tokensPerEth, _tokensPerEth); tokensPerEth = _tokensPerEth; } function toggleSale(bool _active) external onlyOwner { saleActive = _active; emit SaleToggled(_active); } function withdrawEther(address payable to) external onlyOwner { uint256 amount = address(this).balance; require(amount > 0, "No Ether"); (bool sent, ) = to.call{value: amount}(""); require(sent, "Transfer failed"); emit EtherWithdrawn(to, amount); } function withdrawTokens(address to) external onlyOwner { uint256 amount = token.balanceOf(address(this)); require(amount > 0, "No tokens"); token.transfer(to, amount); emit TokensWithdrawn(to, amount); } receive() external payable { buyTokens(); } } 
Enter fullscreen mode Exit fullscreen mode

โš™๏ธ Step 3 โ€” Deploy with Foundry

File: script/Deploy.s.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "forge-std/Script.sol"; import "../src/MyToken.sol"; import "../src/TokenSale.sol"; contract Deploy is Script { function run() external { vm.startBroadcast(); // Deploy ERC20 Token MyToken token = new MyToken("MyToken", "MTK", 1_000_000); // Deploy Token Sale uint256 price = 1000 * 10 ** 18; // 1000 tokens per 1 ETH TokenSale sale = new TokenSale(address(token), price); // Transfer tokens to sale contract token.transfer(address(sale), 100_000 * 10 ** 18); vm.stopBroadcast(); } } 
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช Step 4 โ€” Build and Deploy

Compile

forge build 
Enter fullscreen mode Exit fullscreen mode

Run a Local Node

anvil 
Enter fullscreen mode Exit fullscreen mode

Deploy the Contracts

forge script script/Deploy.s.sol:Deploy --rpc-url http://127.0.0.1:8545 --private-key <YOUR_PRIVATE_KEY> --broadcast 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ฐ Step 5 โ€” Interact with Your Contract

  • Buyers can call buyTokens() and send ETH.
  • Tokens are automatically transferred to their wallets.
  • Owner can:

    • Change price (setPrice)
    • Pause sale (toggleSale)
    • Withdraw ETH (withdrawEther)
    • Withdraw unsold tokens (withdrawTokens)

๐Ÿงฎ Example Calculation

If you set tokensPerEth = 1000 * 10^18:

ETH Sent Tokens Received
1 ETH 1000 Tokens
0.5 ETH 500 Tokens
0.1 ETH 100 Tokens

๐Ÿง  What Youโ€™ll Learn

โœ… ERC-20 token creation
โœ… Handling Ether in contracts
โœ… Token pricing & conversion
โœ… Secure withdrawal patterns
โœ… Deployment using Foundry


๐Ÿ” Security Tips

  • Fund the sale contract before making it public.
  • Use onlyOwner modifiers to secure functions.
  • Validate ETH amounts to avoid reentrancy or precision issues.
  • Consider whitelisting buyers for real-world sales.

๐Ÿ“Š Future Improvements

  • Add cap limits per user
  • Integrate vesting & timelocks
  • Add USDT or stablecoin support
  • Build a React frontend for users to interact with your sale

๐Ÿงพ Conclusion

You just created a Token Sale DApp using Foundry and Solidity โ€” the foundation of many Web3 projects like ICOs, presales, and launchpads.

Every token economy begins here: a simple smart contract that turns Ether into tokens.

Keep building! ๐Ÿš€


๐Ÿงก Follow the Journey

Iโ€™m documenting #30DaysOfSolidity โ€” from basics to advanced DeFi & Web3 projects.

๐Ÿ‘‰ Follow me on Dev.to
๐Ÿ‘‰ Connect on LinkedIn
๐Ÿ‘‰ Read all previous days

Top comments (0)