This content originally appeared on DEV Community and was authored by Saurav Kumar
๐งฉ 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:
-
MyToken.sol
โ ERC-20 token contract -
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
โ๏ธ 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
2๏ธโฃ Create a new project
forge init day-13-token-sale
cd day-13-token-sale
3๏ธโฃ Install OpenZeppelin (ERC-20 contracts)
forge install OpenZeppelin/openzeppelin-contracts
๐ช 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);
}
}
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();
}
}
โ๏ธ 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();
}
}
๐งช Step 4 โ Build and Deploy
Compile
forge build
Run a Local Node
anvil
Deploy the Contracts
forge script script/Deploy.s.sol:Deploy --rpc-url http://127.0.0.1:8545 --private-key <YOUR_PRIVATE_KEY> --broadcast
๐ฐ 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
)
- Change price (
๐งฎ 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
This content originally appeared on DEV Community and was authored by Saurav Kumar

Saurav Kumar | Sciencx (2025-10-13T16:06:55+00:00) ๐ช Day 13 of #30DaysOfSolidity โ Building a Token Sale (Sell Your ERC-20 for ETH with Foundry). Retrieved from https://www.scien.cx/2025/10/13/%f0%9f%aa%99-day-13-of-30daysofsolidity-building-a-token-sale-sell-your-erc-20-for-eth-with-foundry-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.