Build and Deploy a Simple Sepolia Faucet with Solidity and Foundry


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:
Open Windows Powershell.
Run the command below:
wsl --install
Once the download is complete, head over to VS code
Click the button at the bottom left corner to open a remote connection and connect to WSL.
- Open the WSL terminal in VS code and type the command below to install foundry.
curl -L https://foundry.paradigm.xyz | bash
foundryup
- 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:
Open the
src
folder in theTestFaucet
folderCreate 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 namedMAX_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 ifmsg.sender
is whitelisted.onlyOwner
- To check ifmsg.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:Takes in input of address data type
Checks if the address has not been whitelisted.
Whitelist the address
Sends out a success message
removeFromWhitelist()
- This function can only be called by the owner of the contract. This function:Takes in input of address data type
Checks if the address has been whitelisted.
removes the address from whitelist
Sends out a success message
claim()
- This function can only be called by whitelisted addresses. This function:Checks if the last claim of the whitelisted address is less than 2 days
Checks if the faucet is empty, returns a faucet is empty message if its empty
Saves the time where the address last claimed.
Sends sepolia faucet to eligible address.
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:Checks if the faucet balance is empty
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:
An RPC endpoint
- A node provider like Infura, Alchemy, or Ankr. We will use Alchemy in this example.
An encrypted private key
This will be used to sign and send the deployment transaction.
For safe practice, we will encrypt the key.
A deploy script written in Solidity
Uses Foundry's
forge-std/Script.sol
This script will handle contract deployment logic
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
After the app is created, go to the app's Details page
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
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
Your private key
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.
Subscribe to my newsletter
Read articles from 0xdonnie directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
