Build and Deploy a Simple Sepolia Faucet with Solidity and Foundry

0xdonnie0xdonnie
8 min read

Introduction

Imagine you have a group of friends who test Ethereum protocols. The group members get faucets manually, where the admin sends sepolia ETH to addresses, one at a time. This process is stressful and takes a lot of time. Luckily, there’s a way to solve this problem by building a faucet.

This article explains how to build a simple sepolia faucet, write scripts and deploy on sepolia. The audience is expected to have basic solidity knowledge, and WSL commands.

Setting Up the Project

Foundry does not work natively on Windows Powershell. Instead, install WSL (windows subsystem for Linux) with the following steps:

  1. Open Windows Powershell.

  2. Run the command below:

wsl --install
  1. Once the download is complete, head over to VS code

  2. Click the button at the bottom left corner to open a remote connection and connect to WSL.

  1. Open the WSL terminal in VS code and type the command below to install foundry.
curl -L https://foundry.paradigm.xyz | bash
foundryup
  1. Create and enter a folder using the command below:
mkdir TestFaucet
cd TestFaucet
forge init

The forge init command initializes a Foundry project in your folder, sets up the structure and configuration needed to write, test, and deploy smart contracts.

Your folder should look like this

Building the Faucet

To build the faucet, we will use the Solidity language. To get started:

  1. Open the src folder in the TestFaucet folder

  2. Create a file named Faucet.sol

Note: If your VS code is not familiar with the solidity language, head over to VS code extensions and download the “Solidity by Juan Blanco” extension.

In the Faucet.sol file, type the code below:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;
  • The first line is a license identifier, that tells tools and users which license the contract code is published. This is required by the Solidity compiler (as of Solidity 0.6.8+) to avoid warnings.

  • The second line indicates the contract is meant to be written in and compiled with Solidity 0.8.29.

We define a contract named Faucet

contract Faucet {

}

In the defined contract Faucet, initialize some state variables. State variables are parameters that are declared inside a contract but outside of functions. They are permanently stored on the blockchain. Initialize some state variables with the code below:

    address public owner;
    uint256 public constant MAX_AMOUNT = 0.2 ether;
    mapping(address => uint) public lastClaimedAt;
    mapping(address => bool) public whitelisted;

The code above:

  • Creates an owner variable with the data type of address.

  • Declares a constant 256-bit unsigned integer named MAX_AMOUNT set to 0.2 ether.

  • Creates a mapping named lastClaimedAt that maps addresses to uint values.

  • Creates a mapping named whitelisted that maps addresses to boolean values.

In the Faucet contract, initialize the following events below:

    event Whitelisted(address indexed user);
    event RemovedFromWhitelist(address indexed user);
    event Claimed(address indexed user, uint256 amount);

Events are log entries that are recorded on the blockchain. They are used for communication between your contract and external consumers, like user interfaces, in a cost-efficient way.

The code above creates an event for when a user is whitelisted, removed from the whitelist, and when a user has claimed faucet.

After creating an event, define ownership of the contract using a constructor:

constructor() {
        owner = msg.sender;
    }

Constructors in solidity are special functions that run only once, during deployment. In the code above, we set the owner of the contract to the address of the deployer msg.sender. This would help to provide security features on some functions in our contract.

Moving on from constructors, create needed modifiers.

    modifier onlyWhitelisted() {
        require(whitelisted[msg.sender], "Not whitelisted");
        _;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "only owner can call this function");
        _;
    }

Modifiers are special keywords in solidity, which is used to change the behaviour of functions. Modifiers are placed beside functions and used for access control, input validation, or reusable pre-checks before a function runs. In this code we use the following modifiers:

  • onlyWhitelisted - To check if msg.sender is whitelisted.

  • onlyOwner - To check if msg.sender is the owner of the contract.

Proceed to create the necessary functions

    function addToWhitelist(address user) external onlyOwner {
        require(!whitelisted[user], "Address already whitelisted");
        whitelisted[user] = true;
        emit Whitelisted(user);
    }

    function removeFromWhitelist(address user) external onlyOwner {
        require(whitelisted[user], "Address not whitelisted");
        whitelisted[user] = false;
        emit RemovedFromWhitelist(user);
    }

    function claim() external onlyWhitelisted {
        require(
            block.timestamp >= lastClaimedAt[msg.sender] + 2 days,
            "Claim cooldown: wait for 2 days"
        );

        uint payout = address(this).balance >= MAX_AMOUNT
            ? MAX_AMOUNT
            : address(this).balance;
        require(payout > 0, "Faucet is empty");
        lastClaimedAt[msg.sender] = block.timestamp;
        payable(msg.sender).transfer(payout);
        emit Claimed(msg.sender, payout);
    }

    function drainFaucet() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No faucet to drain");

        payable(owner).transfer(balance);
    }

There are four main functions for this Faucet:

  • addToWhitelist() - This function can only be called by the owner of the contract. This function:

    1. Takes in input of address data type

    2. Checks if the address has not been whitelisted.

    3. Whitelist the address

    4. Sends out a success message

  • removeFromWhitelist() - This function can only be called by the owner of the contract. This function:

    1. Takes in input of address data type

    2. Checks if the address has been whitelisted.

    3. removes the address from whitelist

    4. Sends out a success message

  • claim() - This function can only be called by whitelisted addresses. This function:

    1. Checks if the last claim of the whitelisted address is less than 2 days

    2. Checks if the faucet is empty, returns a faucet is empty message if its empty

    3. Saves the time where the address last claimed.

    4. Sends sepolia faucet to eligible address.

    5. Emits a success message which could be used off-chain.

  • drainFaucet() - This function can only be called by the owner of the contract. This function:

    1. Checks if the faucet balance is empty

    2. Sends all the faucet balance to the owner if its not empty

Lastly, to enable the contract receive ETH, we set a special fallback function in the contract

receive() external payable {}

Deploy the Faucet to Sepolia

To deploy our contract to sepolia, we need the following:

  1. An RPC endpoint

    • A node provider like Infura, Alchemy, or Ankr. We will use Alchemy in this example.
  2. An encrypted private key

    • This will be used to sign and send the deployment transaction.

    • For safe practice, we will encrypt the key.

  3. A deploy script written in Solidity

    • Uses Foundry's forge-std/Script.sol

    • This script will handle contract deployment logic

  4. Sepolia ETH

    • The private key must control an account with sepolia ETH.

To create an RPC endpoint;

1. Sign Up / Log In to Alchemy

Go to Alchemy website

  • Click Sign Up (or Log In if you already have an account)

2. Create a New App

  • Once logged in, go to your Alchemy dashboard

  • Click “+ Create App”

  • Fill in the form:

    • Name: e.g., SepoliaTestApp

    • Description: e.g., Create a Faucet

    • UseCase: Other project

  • Click Next

  • Click Ethereum

  • Click Next

  • Click “Create App”

3. Get the RPC Endpoint

  1. After the app is created, go to the app's Details page

  2. Under “API Key”, you'll see the HTTPS endpoint

    • Switch the dropdown from mainnet to sepolia

    • It looks like:

        https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
      
  3. Copy this URL — it's your RPC endpoint

4. Use in Foundry

In foundry.toml, save your endpoints :

[rpc_endpoints]
sepolia = "https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY"

Private key encryption is the best practice when working in web3 development. Saving your keys in the .env file is very unsafe and is risky especially when pushing your code to github. To encrypt your private key in foundry;

Run this command in your WSL terminal

cast wallet import defaultKey --interactive //The name of your private key is set to "defaultKey"

The terminal prompts you to enter the following

  1. Your private key

  2. A new password

Note: As you type your private key and password, the terminal does not display any characters. This is a security feature to protect sensitive information. Please proceed.

Once this has been completed, the terminal logs a success message

Deployment Script

Writing a deploy script automates the deployment of your smart contract to a blockchain using your wallet and an RPC provider. The deployment code is below:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.29;

import {Script} from "forge-std/Script.sol";
import {ApexFaucet} from "../src/ApexFaucet.sol";

contract DeployApexFaucet is Script {
    function run() external returns (ApexFaucet) {
        vm.startBroadcast();
        ApexFaucet apexFaucet = new ApexFaucet();
        vm.stopBroadcast();
        return apexFaucet;
    }
}

The code above basically starts a broadcast to the rpc node to deploy a new instance of your contract on sepolia.

To deploy the contract using the script, run the command below;

forge script script/DeployApexFaucet.s.sol:DeployApexFaucet --rpc-url sepolia --private-key defaultKey --broadcast

The command above;

  • Compiles the script script/DeployApexFaucet.s.sol

  • Connects to sepolia using the --rpc-url sepolia

  • Signs the transaction with our --private-key defaultKey

  • Uses the --broadcast to send transactions on the sepolia network

Once your contract has been deployed, proceed to fund the contract using the contract address

Conclusion

In this article, we learnt how to build and deploy a faucet contract to the sepolia testnet. We covered the entire process from setting up the Foundry development environment to writing the smart contract, testing its functionality, and deploying it to the Sepolia testnet.

After building this project, you can take a step further to build a frontend, where users interact to claim faucet, log error messages like “not eligible” for ineligible participants, and also perform gas optimization techniques to reduce the amount of gas used.

11
Subscribe to my newsletter

Read articles from 0xdonnie directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

0xdonnie
0xdonnie