2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00
2026-01-12 10:42:14 +07:00

🏗 Scaffold-ETH 2

Documentation | Website

🧪 An open-source, up-to-date toolkit for building decentralized applications (dapps) on the Ethereum blockchain. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts.

⚙️ Built using NextJS, RainbowKit, Hardhat, Wagmi, Viem, and Typescript.

  • Contract Hot Reload: Your frontend auto-adapts to your smart contract as you edit it.
  • 🪝 Custom hooks: Collection of React hooks wrapper around wagmi to simplify interactions with smart contracts with typescript autocompletion.
  • 🧱 Components: Collection of common web3 components to quickly build your frontend.
  • 🔥 Burner Wallet & Local Faucet: Quickly test your application with a burner wallet and local faucet.
  • 🔐 Integration with Wallet Providers: Connect to different wallet providers and interact with the Ethereum network.

Debug Contracts tab

Requirements

Before you begin, you need to install the following tools:

🚩 Challenge: 🏵 Token Vendor 🤖

readme

🤖 Smart contracts are kind of like "always on" vending machines that anyone can access. Let's make a decentralized, digital currency. Then, let's build an unstoppable vending machine that will buy and sell the currency. We'll learn about the "approve" pattern for ERC20s and how contract to contract interactions work.

🏵 Create YourToken.sol smart contract that inherits the ERC20 token standard from OpenZeppelin. Set your token to _mint() 1000 (* 10 ** 18) tokens to the msg.sender. Then create a Vendor.sol contract that sells your token using a payable buyTokens() function.

🎛 Edit the frontend that invites the user to input an amount of tokens they want to buy. We'll display a preview of the amount of ETH it will cost with a confirm button.

🔍 It will be important to verify your token's source code in the block explorer after you deploy. Supporters will want to be sure that it has a fixed supply and you can't just mint more.

🌟 The final deliverable is an app that lets users purchase your ERC20 token, transfer it, and sell it back to the vendor. Deploy your contracts on your public chain of choice and then yarn vercel your app to a public web server. Submit the url on SpeedrunEthereum.com!

💬 Meet other builders working on this challenge and get help in the Challenge Telegram!


Checkpoint 0: 📦 Environment 📚

Start your local network (a blockchain emulator in your computer):

yarn chain

in a second terminal window, 🛰 deploy your contract (locally):

yarn deploy

in a third terminal window, start your 📱 frontend:

yarn start

📱 Open http://localhost:3000 to see the app.

👩‍💻 Rerun yarn deploy --reset whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address.


⚠️ We have disabled AI in Cursor and VSCode and highly suggest that you do not enable it so you can focus on the challenge, do everything by yourself, and hence better understand and remember things. If you are using another IDE, please disable AI yourself.

🔧 If you are a vibe-coder and don't care about understanding the syntax of the code used and just want to understand the general takeaways, you can re-enable AI by:

  • Cursor: remove * from .cursorignore file
  • VSCode: set chat.disableAIFeatures to false in .vscode/settings.json file

Checkpoint 1: 🏵Your Token 💵

👩‍💻 Go to packages/hardhat/contracts/YourToken.sol look at how this contract is inheriting the ERC20 token standard from OpenZeppelin. This means that the YourToken contract obtains every method that is a part of the ERC20 standard and so it has all the default properties needed to be a used as a token on Ethereum.

In the constructor(), mint a fixed supply of 1000 tokens (with 18 decimals) to msg.sender (the deployer).

Implementing YourToken

  • Decide on the token name/symbol (already scaffolded for you as "Gold"/"GLD").
  • The OpenZeppelin ERC20 contract you are inheriting exposes a _mint function you can use to create new tokens.
  • Update the constructor to mint exactly 1000 tokens to msg.sender (the deployer).
🔎 Hint

ERC20 tokens typically use 18 decimals. Solidity has a convenient unit that matches that scaling:

  • 1000 ether is 1000 * 10**18

So minting 1000 tokens can be as simple as:

  • _mint(msg.sender, 1000 ether);
🎯 Solution
constructor() ERC20("Gold", "GLD") {
    _mint(msg.sender, 1000 ether);
}

🥅 Goals

  • ⚠️ Important: Your initial token supply was minted to the deployer. If the wallet you use in the frontend is a different address, you wont see a balance there yet.

    • Update FRONTEND_ADDRESS in packages/hardhat/deploy/01_deploy_vendor.ts and keep SEND_TOKENS_TO_VENDOR = false since we are not ready for that step.
    • Then run yarn deploy --reset to send the tokens to your frontend wallet so you can test in the UI.
  • Can you check the balanceOf() your frontend address in the Debug Contracts tab? (YourToken contract)

  • Can you transfer() your token to another account and check that account's balanceOf?

debugContractsYourToken

💬 Hint: Use an incognito window to create a new address and try sending to that new address. Can use the transfer() function in the Debug Contracts tab.

Testing your progress

🔍 Run:

yarn test --grep "Checkpoint1"

Checkpoint 2: ⚖️ Vendor 🤖

👩‍💻 Edit packages/hardhat/contracts/Vendor.sol and build a token vending machine with a payable buyTokens() function.

Step 1: Add a price constant

Use a price variable named tokensPerEth set to 100 (meaning 100 tokens per 1 ETH):

uint256 public constant tokensPerEth = 100;

Step 2: Add custom errors

Instead of require(condition, "message"), well use custom errors (theyre cheaper at runtime and easier to test).

In the error section of the contract, add these errors for the common failure cases:

error InvalidEthAmount();
error InsufficientVendorTokenBalance(uint256 available, uint256 required);

Step 3: Add an event

In the event section of the contract, add an event that the UI (and block explorers) can use to track purchases:

event BuyTokens(address indexed buyer, uint256 amountOfETH, uint256 amountOfTokens);

Implementing buyTokens()

The buyTokens() function should:

  • Reject a purchase with 0 ETH, reverting with InvalidEthAmount
  • Compute how many tokens the buyer should receive using msg.value and tokensPerEth
  • Make sure the Vendor has enough tokens to sell and if they don't revert with InsufficientVendorTokenBalance
  • Transfer tokens to the buyer
  • Emit the BuyTokens event
🔎 Hint

Decimals gotcha (important):

  • ETH is measured in wei (18 decimals)
  • ERC20 tokens in this challenge also use 18 decimals

If tokensPerEth = 100, then:

  • Sending 1 ether should yield 100 ether token units

A simple formula that works with 18-decimals tokens:

  • tokensToBuy = msg.value * tokensPerEth

Also, you can check how many tokens the vendor holds with:

  • yourToken.balanceOf(address(this))
🎯 Solution
function buyTokens() external payable {
    if (msg.value == 0) revert InvalidEthAmount();

    uint256 amountOfTokens = msg.value * tokensPerEth;
    uint256 vendorBalance = yourToken.balanceOf(address(this));
    if (vendorBalance < amountOfTokens) revert InsufficientVendorTokenBalance(vendorBalance, amountOfTokens);

    yourToken.transfer(msg.sender, amountOfTokens);
    emit BuyTokens(msg.sender, msg.value, amountOfTokens);
}

Try it out (frontend + deploy)

Edit packages/hardhat/deploy/01_deploy_vendor.ts to set SEND_TOKENS_TO_VENDOR to true. This will deploy the Vendor contract and automatically seed it with the tokens INSTEAD of sending the tokens to your FRONTEND_ADDRESS. It will also set your address as the owner of the Vendor contract but we will dig into that later...

🔎 Look in packages/nextjs/app/token-vendor/page.tsx and uncomment the Vendor Balances and Buy Tokens sections to display the Vendor ETH and Token balances as well as enable buying tokens from the frontend.

You can yarn deploy --reset to deploy your contract until you get it right.

TokenVendorBuy

🥅 Goals

  • Does the Vendor address start with a balanceOf 1000 in YourToken on the Debug Contracts tab?
  • Can you buy 10 tokens for 0.1 ETH?
  • Can you transfer tokens to a different account?

Testing your progress

🔍 Run:

yarn test --grep "Checkpoint2"

Checkpoint 3: 👑 Ownable + Withdraw 💸

Now that your Vendor can accept ETH via buyTokens(), lets protect the treasury.

Step 1: Inheriting Ownable (OpenZeppelin v5)

The OpenZeppelin Ownable contract adds special methods, modifiers and state variable for helping to secure certain methods. Notably the onlyOwner modifier can be used to guard a method so that only the owner can call it.

  • Notice how the vendor contract already imports Ownable from OpenZeppelin
  • See how it is inherited in the line defining the contract:
contract Vendor is Ownable ...
  • Lastly see how we define ownership in the constructor with Ownable(msg.sender), making the deployer of the contract the owner

We are using the onlyOwner modifier to protect the withdraw method so that only the owner can withdraw the contract's ETH balance.

Step 2: Add a custom error for failed transfers

First add an error you will need in the method.

error EthTransferFailed(address to, uint256 amount);

Implementing withdraw()

Your withdraw() function should:

  • Be restricted to the owner (onlyOwner)
  • Send all ETH in the Vendor to the owner
  • Revert with EthTransferFailed if the ETH transfer fails
🔎 Hint

Avoid transfer() (it can unexpectedly fail due to gas changes and is no longer supported in the latest Solidity versions). Prefer call:

  • (bool ok,) = owner().call{value: amount}("");

If ok is false, revert.

🎯 Solution
function withdraw() external onlyOwner {
    uint256 amount = address(this).balance;
    (bool success,) = owner().call{value: amount}("");
    if (!success) revert EthTransferFailed(owner(), amount);
}

Try it out

Deploy the updated contract with yarn deploy --reset and then go test it out by depositing ETH and withdrawing. You can do this from the Debug Contracts tab.

🥅 Goals

  • Is your frontend address the owner of the Vendor?
  • Can your address successfully withdraw all the ETH in the Vendor?

Testing your progress

🔍 Run:

yarn test --grep "Checkpoint3"

Checkpoint 4: 🤔 Vendor Buyback 🤯

👩‍🏫 The hardest part of this challenge is to build your Vendor in such a way so that it can buy the tokens back.

🧐 The reason why this is hard is the approve() pattern in ERC20s.

😕 First, the user has to call approve() on the YourToken contract, approving the Vendor contract address to take some amount of tokens.

🤨 Then, the user makes a second transaction to the Vendor contract to sellTokens(uint256 amount).

🤓 The Vendor should call yourToken.transferFrom(msg.sender, address(this), theAmount) and if the user has approved the Vendor correctly, tokens should transfer to the Vendor and ETH should be sent to the user.

🤔 But why do we need the approve method?

The crux of the issue is this: if smart contracts can move tokens out of your wallet, how do you make sure that only the smart contract you want to take tokens is the one thats allowed to do it?

Heres the simple mental model:

  • approve(spender, amount) = “I allow this contract to spend up to X of my tokens.”

    • It does not move tokens.
    • It writes an allowance into the token contract: allowance[you][spender] = amount.
  • transferFrom(from, to, amount) = “Use that permission to pull tokens.”

    • The Vendor contract calls this during sellTokens(...).
    • The token contract checks the allowance and only lets the transfer happen if its big enough.

What this unlocks: safe, pull-based token interactions where a contract can perform an action (swap, buyback, marketplace purchase, subscription, etc.) and pull exactly the tokens it needs, without having blanket access to your wallet. You can also limit risk by approving only the exact amount (or revoke later by approving 0).

Luckily, wallet UX is improving fast. With proposals like EIP-7702 now being enabled on Ethereum, a wallet can let you sign one “sell” action that executes a small bundle of steps atomically (e.g. approve + sellTokens / transferFrom) in a single transaction, instead of making you click through two separate user actions. The underlying ERC-20 allowance model still exists; youre just authorizing a smarter, batched execution path. This only needs to be adopted by wallets and frontends for users to reap the benefits.

Step 1: Add custom errors + event

error InvalidTokenAmount();
error InsufficientVendorEthBalance(uint256 available, uint256 required);

event SellTokens(address indexed seller, uint256 amountOfTokens, uint256 amountOfETH);

Implementing sellTokens(uint256 amount)

Your sellTokens(amount) should:

  • Reject amount == 0 with InvalidTokenAmount
  • Pull tokens from the user with transferFrom (requires prior approve call by user)
  • Compute the ETH to return using the inverse of your pricing
  • Ensure the Vendor has enough ETH liquidity and if not, revert with InsufficientVendorEthBalance
  • Send ETH back to the user
  • Emit a SellTokens event
🔎 Hint

If tokensPerEth = 100, then the inverse conversion is:

  • ethToReturn = amount / tokensPerEth
🎯 Solution
function sellTokens(uint256 amount) external {
    if (amount == 0) revert InvalidTokenAmount();

    uint256 amountOfETH = amount / tokensPerEth;
    uint256 vendorEthBalance = address(this).balance;
    if (vendorEthBalance < amountOfETH) revert InsufficientVendorEthBalance(vendorEthBalance, amountOfETH);

    yourToken.transferFrom(msg.sender, address(this), amount);

    (bool success,) = msg.sender.call{value: amountOfETH}("");
    if (!success) revert EthTransferFailed(msg.sender, amountOfETH);

    emit SellTokens(msg.sender, amount, amountOfETH);
}

Try it out

🔁 Redeploy (yarn deploy --reset) and try out your new function!

🔨 Use the Debug Contracts tab to call the approve and sellTokens() at first but then...

🔍 Look in the packages/nextjs/app/token-vendor/page.tsx for the extra approve/sell UI to uncomment and then go to packages/nextjs/app/events/page.tsx and uncomment the SellTokens Events section to update the Events tab on the frontend.

VendorBuyBack

🥅 Goal

  • Can you sell tokens back to the vendor?
  • Do you receive the right amount of ETH for the tokens?
  • Do you see SellTokens events in the Events tab now?

Events

⚔️ Side Quests

  • Should we disable the owner withdraw to keep liquidity in the Vendor?
  • Would people be more interested in your token if they knew there wasn't a way to drain the ETH backing?

Testing your progress

🔍 Run:

yarn test --grep "Checkpoint4"

Checkpoint 5: 💾 Deploy your contracts! 🛰

📡 Edit the defaultNetwork in hardhat.config.ts to match the name of one of testnets from the networks object. We recommend to use "sepolia" or "optimismSepolia"

🔐 You will need to generate a deployer address using yarn generate This creates a mnemonic and saves it locally.

👩‍🚀 Use yarn account to view your deployer account balances.

You will need to send ETH to your deployer address with your wallet, or get it from a public faucet of your chosen network. You can also request ETH by sending a message with your new deployer address and preferred network in the challenge Telegram. People are usually more than willing to share.

🚀 Run yarn deploy to deploy your smart contract to a public network (selected in hardhat.config.ts)

💬 Hint: Instead of editing hardhat.config.ts you can just add a network flag to the deploy command like this: yarn deploy --network sepolia or yarn deploy --network optimismSepolia


Checkpoint 6: 🚢 Ship your frontend! 🚁

✏️ Edit your frontend config in packages/nextjs/scaffold.config.ts to change the targetNetwork to chains.sepolia (or chains.optimismSepolia if you deployed to OP Sepolia)

💻 View your frontend at http://localhost:3000 and verify you see the correct network.

📡 When you are ready to ship the frontend app...

📦 Run yarn vercel to package up your frontend and deploy.

You might need to log in to Vercel first by running yarn vercel:login. Once you log in (email, GitHub, etc), the default options should work.

If you want to redeploy to the same production URL you can run yarn vercel --prod. If you omit the --prod flag it will deploy it to a preview/test URL.

Follow the steps to deploy to Vercel. It'll give you a public URL.

🦊 Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default 🔥 burner wallets are only available on hardhat . You can enable them on every chain by setting onlyLocalBurnerWallet: false in your frontend config (scaffold.config.ts in packages/nextjs/)

Configuration of Third-Party Services for Production-Grade Apps.

By default, 🏗 Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services. This is great to complete your Speedrun Ethereum.

For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at:

  • 🔷ALCHEMY_API_KEY variable in packages/hardhat/.env and packages/nextjs/.env.local. You can create API keys from the Alchemy dashboard.

  • 📃ETHERSCAN_API_KEY variable in packages/hardhat/.env with your generated API key. You can get your key here.

💬 Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing.


Checkpoint 7: 📜 Contract Verification

Run the yarn verify --network your_network command to verify your contracts on etherscan 🛰

👀 You may see an address for both YourToken and Vendor. You will want the Vendor address.

👉 Search this address on Sepolia Etherscan (or Optimism Sepolia Etherscan if you deployed to OP Sepolia) to get the URL you submit to 🏃‍♀️SpeedrunEthereum.com.


🏃 Head to your next challenge here.

💬 Problems, questions, comments on the stack? Post them to the 🏗 scaffold-eth developers chat

Documentation

Visit our docs to learn how to start building with Scaffold-ETH 2.

To know more about its features, check out our website.

Contributing to Scaffold-ETH 2

We welcome contributions to Scaffold-ETH 2!

Please see CONTRIBUTING.MD for more information and guidelines for contributing to Scaffold-ETH 2.

Description
No description provided
Readme 1.5 MiB
Languages
TypeScript 97.2%
CSS 2%
JavaScript 0.8%