Creating a decentralized application (dApp) that enables users to buy and sell custom ERC20 tokens is a foundational skill in blockchain development. This guide walks you through building a token vendor dApp using the popular Scaffold-eth framework, integrating smart contracts, frontend interaction, and deployment to test networks. Whether you're new to Ethereum development or expanding your Web3 toolkit, this step-by-step tutorial delivers practical insights into real-world dApp architecture.
Setting Up the Development Environment
Before writing any code, we need to set up a local development environment. The Scaffold-eth project provides a robust foundation for full-stack Ethereum dApps using Hardhat, React, and The Graph.
Start by cloning the challenge repository:
git clone https://github.com/scaffold-eth/scaffold-eth-typescript-challenges.git challenge-2-token-vendor
cd challenge-2-token-vendor
git checkout challenge-2-token-vendor
yarn installThe project structure includes key directories:
hardhat-ts: Contains smart contracts and deployment scripts.vite-app-ts: Frontend interface built with React and Vite.subgraph: Configuration for indexing blockchain data with The Graph (optional for this guide).services: Local services like graph-node.
To run the app locally, launch three terminal sessions with these commands:
yarn chain– Starts a local Ethereum network via Hardhat.yarn deploy– Compiles and deploys contracts to the local chain.yarn start– Runs the React frontend at http://localhost:3000.
Use yarn deploy --reset to redeploy contracts during development.
👉 Discover how blockchain developers accelerate dApp creation with modern tooling.
Building an ERC20 Token Contract
At the heart of our dApp is a custom ERC20 token. ERC20 defines a standard interface for fungible tokens on Ethereum, ensuring compatibility across wallets, exchanges, and other smart contracts.
Understanding the ERC20 Standard
Key functions in the ERC20 standard include:
totalSupply()– Total number of tokens in circulation.balanceOf(address)– Balance of tokens for a given address.transfer(to, amount)– Send tokens directly.approve(spender, amount)– Allow another address to spend tokens.transferFrom(from, to, amount)– Transfer approved tokens from one account to another.
Events like Transfer and Approval enable off-chain tracking of token movements.
Leveraging OpenZeppelin for Secure Development
Rather than implementing ERC20 from scratch, we use OpenZeppelin Contracts, a library of audited, reusable smart contract components. It reduces bugs and security risks while accelerating development.
Install it via npm or import directly in your Solidity file.
Writing the Token Contract
Create YourToken.sol with the following code:
pragma solidity >=0.8.0 <0.9.0;
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
contract YourToken is ERC20 {
constructor() ERC20("Gold", "GLD") {
_mint(msg.sender, 1000 * 10 ** 18);
}
}This contract mints 1,000 GLD tokens (with 18 decimals) to the deployer. The _mint() function increases the total supply and assigns tokens securely.
Deploying and Testing the Token
Update the deployment script (00_deploy_your_token.ts) to optionally send tokens to a test wallet:
const yourToken = await ethers.getContract("YourToken", deployer);
await yourToken.transfer("0x1698...88", ethers.utils.parseEther("1000"));After deploying with yarn deploy --reset, verify balances using the frontend’s debug panel. You can also test transfers between accounts.
Remember to comment out the transfer line after testing to avoid unintended behavior in later steps.
Implementing the Token Vendor: Buy Functionality
Now that we have a token, let’s create a vendor contract that allows users to purchase GLD with ETH.
Core Features of the Vendor Contract
Our vendor will:
- Allow users to buy GLD at a fixed rate (e.g., 1 ETH = 100 GLD).
- Emit events when purchases occur.
- Let the owner withdraw ETH collected from sales.
We’ll use OpenZeppelin’s Ownable contract to restrict sensitive functions like withdrawals.
Writing the Vendor Contract
pragma solidity >=0.8.0 <0.9.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./YourToken.sol";
contract Vendor is Ownable {
YourToken public yourToken;
uint256 public tokensPerEth = 100;
event BuyTokens(address buyer, uint256 amountOfEth, uint256 amountOfTokens);
constructor(address tokenAddress) {
yourToken = YourToken(tokenAddress);
}
function buyTokens() external payable {
require(msg.value > 0, "Must send ETH");
uint256 amountOfTokens = msg.value * tokensPerEth;
require(yourToken.balanceOf(address(this)) >= amountOfTokens, "Not enough tokens");
require(yourToken.transfer(msg.sender, amountOfTokens), "Transfer failed");
emit BuyTokens(msg.sender, msg.value, amountOfTokens);
}
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No ETH to withdraw");
(bool sent, ) = msg.sender.call{value: balance}("");
require(sent, "Withdrawal failed");
}
}Deployment Script Setup
In 01_deploy_vendor.ts, ensure:
- The vendor receives all 1,000 GLD tokens.
- Ownership is transferred to your wallet for withdrawal control.
await yourToken.transfer(vendor.address, ethers.utils.parseEther("1000"));
await vendor.transferOwnership("0x1698...88"); // Your walletDeploy with yarn deploy --reset.
Verification Steps
- Check that the vendor contract holds 1,000 GLD via the debug UI.
- Use the “Buy Tokens” function with 0.1 ETH to receive 10 GLD.
- Confirm the vendor’s ETH balance increases.
- Use the owner account to call
withdraw()and empty the contract’s ETH balance.
👉 See how top developers streamline contract testing and deployment workflows.
Adding Sell Functionality: Token Buybacks
Allowing users to sell tokens back to the vendor enhances liquidity and user experience.
Why We Need approve() and transferFrom()
Smart contracts cannot receive tokens automatically when users send them. Instead, users must first approve the vendor to spend their tokens, then trigger a transferFrom call within the vendor contract.
This two-step process ensures user control and prevents unauthorized access.
Implementing sellTokens()
Add this function to the vendor contract:
event SellTokens(address seller, uint256 amountOfTokens, uint256 amountOfETH);
function sellTokens(uint256 amountToSell) external {
require(amountToSell > 0, "Sell amount must be positive");
require(yourToken.balanceOf(msg.sender) >= amountToSell, "Insufficient token balance");
uint256 ethToReturn = amountToSell / tokensPerEth;
require(address(this).balance >= ethToReturn, "Vendor has insufficient ETH");
require(yourToken.transferFrom(msg.sender, address(this), amountToSell), "Transfer failed");
(bool sent, ) = msg.sender.call{value: ethToReturn}("");
require(sent, "ETH transfer failed");
emit SellTokens(msg.sender, amountToSell, ethToReturn);
}Testing the Sell Flow
- Call
approve(address(vendor), 10)on the token contract. - Use the vendor’s
sellTokens(10)function. Verify:
- You receive ~0.1 ETH.
- The vendor receives 10 GLD.
- Events are emitted correctly.
If approval is missing, the transaction will revert — this is expected behavior.
Deploying to the Rinkeby Test Network
Once tested locally, deploy your dApp to a public testnet like Rinkeby for broader access.
Configuration Steps
- Update
hardhat.config.ts: SetdefaultNetworkto"rinkeby". - In
providersConfig.ts, changetargetNetworkInfoto point to Rinkeby. - Run
yarn accountto view your address; generate one if needed withyarn generate. - Fund your account using a faucet like faucet.paradigm.xyz.
- Deploy with
yarn deploy.
Successful deployment outputs contract addresses viewable on Rinkeby Etherscan.
Publishing the Frontend
Make your dApp publicly accessible by deploying the frontend.
Using Surge.sh:
yarn build
yarn surgeYou’ll be prompted to log in or create an account and choose a domain like my-token-vendor.surge.sh. Once published, anyone can interact with your live dApp.
Alternative platforms include IPFS, Netlify, or Vercel.
Verifying Contracts on Etherscan
Source code verification builds trust by proving your on-chain logic matches what users expect.
Steps to Verify
- Get an Etherscan API key from etherscan.io/myapikey.
- Add it to
package.jsonunder"etherscan-verify". - Add a root-level script:
"verify": "yarn workspace @scaffold-eth/hardhat etherscan-verify". - Run:
yarn verify --network rinkeby.
After verification, Etherscan displays a blue checkmark and allows anyone to read your contract source.
Final Submission and Learning Outcomes
Submit your deployed frontend URL and vendor contract address on speedrunethereum.com to complete the challenge.
Key Skills Learned
- ✅ Creating ERC20 tokens using OpenZeppelin
- ✅ Building secure vendor contracts with buy/sell logic
- ✅ Managing token approvals and transfers
- ✅ Deploying full-stack dApps on testnets
- ✅ Frontend publishing and contract verification
Frequently Asked Questions (FAQ)
Q: What is an ERC20 token?
A: ERC20 is a technical standard for fungible tokens on Ethereum. It defines methods like transfer, balanceOf, and events like Transfer, enabling interoperability across wallets and exchanges.
Q: Why do I need approve() before selling tokens?
A: Smart contracts can’t accept incoming token transfers directly. The user must first approve the vendor as a spender via approve(), allowing it to pull tokens using transferFrom().
Q: Can I customize the exchange rate in the vendor contract?
A: Yes! Modify the tokensPerEth variable or make it adjustable via an admin function for dynamic pricing models.
Q: Is it safe to use OpenZeppelin Contracts?
A: Absolutely. OpenZeppelin provides rigorously audited, community-vetted smart contract libraries widely used in production environments.
Q: How do I handle decimal precision in token calculations?
A: Most ERC20 tokens use 18 decimals. Use ethers.utils.parseEther("1") for accurate conversions from human-readable numbers to wei units.
Q: Can I deploy this dApp on mainnet?
A: Yes — after thorough testing on testnets, update configurations to connect to networks like Ethereum Mainnet or Polygon and re-deploy cautiously with real funds.
👉 Explore advanced tools for deploying and monitoring production-grade dApps today.