<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[CodeOps Blog]]></title><description><![CDATA[CodeOps Blog]]></description><link>https://buildwithdonnie.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 23 Sep 2026 08:09:36 GMT</lastBuildDate><atom:link href="https://buildwithdonnie.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I deployed a static site to S3 and automated it with GitHub Actions — here's what broke and how I fixed it.]]></title><description><![CDATA[Last week, I set up a static site on Amazon S3 (Simple Storage Service) for my portfolio page. After a series of manual updates, I decided to automate the process with GitHub Actions instead. It sound]]></description><link>https://buildwithdonnie.hashnode.dev/i-deployed-a-static-site-to-s3-and-automated-it-with-github-actions-here-s-what-broke-and-how-i-fixed-it</link><guid isPermaLink="true">https://buildwithdonnie.hashnode.dev/i-deployed-a-static-site-to-s3-and-automated-it-with-github-actions-here-s-what-broke-and-how-i-fixed-it</guid><category><![CDATA[AWS]]></category><category><![CDATA[GitHub Actions]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[S3]]></category><dc:creator><![CDATA[0xdonnie]]></dc:creator><pubDate>Sat, 19 Sep 2026 05:48:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/e040106d-4ec9-4dc1-abcb-dd87d57ba539.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last week, I set up a static site on Amazon S3 (Simple Storage Service) for my portfolio page. After a series of manual updates, I decided to automate the process with GitHub Actions instead. It sounded simple on paper: a bucket, a distribution, and a workflow file. In practice, it took me through IAM roles, OIDC trust policies, and two errors before it worked. Here's how it went.</p>
<h3>The simple plan</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/1f92fc59-dcdf-41c3-944a-834ae740be52.png" alt="" style="display:block;margin:0 auto" />

<p>Before touching <a href="https://docs.aws.amazon.com/whitepapers/latest/aws-overview/introduction.html">AWS</a>, I sketched out the whole process.</p>
<p>The flow was simple:</p>
<ol>
<li><p>Push an update from the local repo</p>
</li>
<li><p>GitHub Actions builds the site and pushes it to S3</p>
</li>
<li><p><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html">CloudFront's</a> cache gets invalidated, so it serves the new version</p>
</li>
<li><p>The user gets the update.</p>
</li>
</ol>
<h3>Building the boxes</h3>
<p><strong>Quick S3 bucket setup</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/872ee211-8ff4-4886-88b3-226b1873bcae.png" alt="" style="display:block;margin:0 auto" />

<p>The first step of this project is to create storage for the static files. Navigte to S3 &gt; buckets &gt; create buckets</p>
<p>I created my bucket with the following configurations:</p>
<ul>
<li><p>Bucket type: General purpose</p>
</li>
<li><p>Bucket namespace: Global namespace</p>
</li>
<li><p>Bucket name: myportfolio</p>
</li>
<li><p>Block public access: [ticked]</p>
</li>
<li><p>And the rest left at default settings</p>
</li>
</ul>
<p>Note: <em>The Amazon S3 bucket name must be globally unique across the AWS S3 namespace, not just unique within your AWS account or region.</em></p>
<p><strong>CloudFront distribution configuration</strong></p>
<p>To respond quickly and improve security, I set up CloudFront to handle content delivery and HTTPS.</p>
<p>For the origin, I pointed CloudFront at my S3 bucket, not the website endpoint. This setting enables OAC (origin access control), which lets CloudFront reach a private bucket without the bucket being public. I named the distribution and left the rest at their defaults</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/96cd0184-5d3b-4719-b5ae-23f819859f8b.png" alt="" style="display:block;margin:0 auto" />

<p>On the final create distribution page, CloudFront updated my S3 bucket's policy directly so it could restrict access to just this distribution. It required no manual copying, which was a nice surprise compared to what I expected going in.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/e127f956-ac37-4c6d-b01a-ceef4548b138.png" alt="" style="display:block;margin:0 auto" />

<p>Once the distribution finished deploying, I had a working <a href="http://cloudfront.net"><code>.cloudfront.net</code></a> URL, HTTPS by default, and now sitting in front of a bucket no one could reach directly.</p>
<h3>The trust setup</h3>
<p><strong>Setting trust between GitHub and AWS</strong></p>
<p>This is the first step for configuring the CI/CD pipeline. I used the OIDC provider type. Here, GitHub issues a short-lived identity token to a workflow while it's running. AWS is configured to trust the tokens issued by GitHub and check the token against a specific policy. If it matches, AWS sends back a temporary credential, which is valid for that one process.</p>
<p>To set this up;</p>
<ol>
<li><p>Register GitHub as a trusted identity provider</p>
</li>
<li><p>Create an IAM role that only my GitHub-specific repo can access.</p>
</li>
</ol>
<p>Head over to IAM &gt; Identity provider &gt; Add provider, and enter these details:</p>
<ul>
<li><p>Provider type: OIDC</p>
</li>
<li><p>Provider URL: <a href="https://token.actions.githubusercontent.com">https://token.actions.githubusercontent.com</a></p>
</li>
<li><p>Audience: <a href="http://sts.amazonaws.com">sts.amazonaws.com</a></p>
</li>
<li><p>Click Add provider</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/e4ce6513-870f-43c8-afbe-61a89eaadea5.png" alt="" style="display:block;margin:0 auto" />

<p>Proceed to create an IAM role. Head over to IAM &gt; Roles &gt; Create role</p>
<p>Set the trusted entity type to <code>Custom trust policy</code> and paste the following command in the box</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:ref:refs/heads/main"
        }
      }
    }
  ]
}
</code></pre>
<p>Edit fields like <code>YOUR_ACCOUNT_ID</code> and <code>YOUR_GITHUB_USERNAME/YOUR_REPO_NAME</code> to your personal AWS and GitHub information.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/1b6be33f-d3af-46b8-b8d8-b398c76c29e4.png" alt="" style="display:block;margin:0 auto" />

<p>Click Next to go to the add permissions page.</p>
<p>On this page, click Create inline policy, and add the policy below to the policy box.</p>
<pre><code class="language-json">{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket", "s3:DeleteObject"],
      "Resource": ["arn:aws:s3:::YOUR_BUCKET", "arn:aws:s3:::YOUR_BUCKET/*"]
    },
    {
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
    }
  ]
}
</code></pre>
<p>Edit the fields <code>YOUR_BUCKET</code> and <code>YOUR_ACCOUNT_ID</code> to your personal AWS information</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/3095ae48-f0d3-4926-b288-6a82eb407822.png" alt="" style="display:block;margin:0 auto" />

<p>Proceed to name, review, and create the role.</p>
<p>Once the role is created, copy its ARN. This address goes into our workflow YAML file as the <code>role-to-assume</code> value. It tells GitHub Actions the role to request the temporary credentials for.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/c9683053-6b16-4511-8d6f-b92d40d45014.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>The GitHub Actions Workflow</strong></h3>
<p>This is the last piece of the project. In simple terms, this file does five things in this order:</p>
<ul>
<li><p>It pulls in my latest code</p>
</li>
<li><p>Builds the site</p>
</li>
<li><p>Authenticates to AWS using the OIDC (OpenID Connect) setup from the previous step</p>
</li>
<li><p>Ships the built files to S3</p>
</li>
<li><p>Clears CloudFront's cache so the new version is sent to the user</p>
</li>
</ul>
<p>Proceed to create a .github/workflows/deploy.yml file and paste this code in.</p>
<pre><code class="language-yaml">name: Deploy static site
run-name: ${{ github.actor }} deployed "${{ github.event.head_commit.message }}"

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4
 
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
 
      - name: Install dependencies
        run: npm ci
 
      - name: Build site
        run: npm run build   # adjust if your build script has a different name
 
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::YOUR-AWS-ACCOUNT-ID:role/githubActionsPolicyDeploy   # replace with your role ARN
          aws-region: us-east-1   # adjust to your bucket's region
 
      - name: Sync site files to S3
        run: |
          # No separate build folder here — Tailwind just compiles output.css
          # in place, so we sync the whole project root, excluding anything
          # that isn't part of the actual deployed site.
          aws s3 sync . s3://YOUR-PROJECT-BUCKET\
            --delete \
            --exclude ".git/*" \
            --exclude ".github/*" \
            --exclude "node_modules/*" \
            --exclude "input.css" \
            --exclude "package.json" \
            --exclude "package-lock.json" \
            --exclude "tailwind.config.js" \
            --exclude "README.md" \
            --cache-control "public, max-age=31536000, immutable" \
            --exclude "index.html"
 
          # index.html should not be cached long, so browsers pick up new deploys quickly
          aws s3 cp ./index.html s3://login-form-frontend/index.html \
            --cache-control "public, max-age=0, must-revalidate"
 
      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id YOUR-DISTRIBUTION-ID\
            --paths "/*"
 
</code></pre>
<p>Update all fields in ALL CAPS with your AWS information.</p>
<p>Once the YAML file has been pasted;</p>
<ul>
<li><p>Commit and push to <code>main</code>.</p>
</li>
<li><p>Watch the Actions tab in your GitHub repo; each step shows live status (yellow for <code>running</code>, green for <code>passed</code>, red for <code>failed</code>).</p>
</li>
<li><p>On success, visit your CloudFront URL and hard-refresh to confirm the change is live.</p>
</li>
<li><p>You can make a small change, push again, and confirm it deploys automatically. This proves the pipeline works end to end.</p>
</li>
</ul>
<h3>The bugs</h3>
<p><strong>Default root was empty</strong></p>
<p>Once the distribution finished deploying, I opened the <code>.cloudfront.net</code> URL, expecting my homepage. Instead, I got this error message.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/a9ff70b2-fbbe-45f5-a2af-0ef9c940e171.png" alt="" style="display:block;margin:0 auto" />

<p>My first thought was permissions. Maybe the bucket policy CloudFront had written wasn't correct. I checked, and nothing about the setup looked wrong on paper.</p>
<p>The real problem came from CloudFront's general settings. When the client hits the URL, CloudFront needs to know which specific file to serve. Without the setting, CloudFront asks S3 for the root path itself, which is treated like a request to list the bucket's content. This request required a different permission (<code>s3:ListBucket</code>) than the one my policy actually granted (<code>s3:GetObject</code>). So instead of a clear "file not found," I got an "Access Denied" error.</p>
<p>The fix was quite simple. I set "Default root object" to index.html in the distribution's general settings.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/c34cf3c2-4be1-4019-8eb4-b446231647ed.png" alt="" style="display:block;margin:0 auto" />

<h3>Not authorized to perform <code>AssumeRoleWithWebIdentity</code> error</h3>
<p>After fixing the default root error, I pushed again, expecting things to work. Instead, I got a new error in the "Configure AWS credentials" step.</p>
<pre><code class="language-plaintext">Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/d04ab81b-bc5a-48ed-8d2c-2d72b3032a27.png" alt="" style="display:block;margin:0 auto" />

<p>From the error GitHub Actions returned, I figured the error had something to do with the IAM role's trust setup. I checked them one by one; the account ID matched, the audience matched, the branch name matched, and everything looked correct.</p>
<p>I compared the trust relationship against my repo's actual URL, letter by letter, and found it: one letter in my repo name was lowercase in the policy where it should have been uppercase. It turns out the condition matching GitHub's identity token is case-sensitive.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6806de0cdbc6c12df81c93af/61049429-2d1b-4bdc-bc3b-9ce52521eed2.png" alt="" style="display:block;margin:0 auto" />

<p>I fixed the error, pushed again, and this time, the credentials step passed.</p>
<h3>Small projects, real lessons</h3>
<p>Setting up cloud resources is, in itself, real progress in cloud engineering. Here, I set up a static frontend with a CI/CD pipeline, ran into real errors along the way, and worked through each one.</p>
<p>From here, you can take it further by setting up a real domain, with an ACM certificate attached to CloudFront via Route 53. It's a natural next step on top of everything in place.</p>
]]></content:encoded></item><item><title><![CDATA[Build and Deploy a Simple Sepolia Faucet with Solidity and Foundry]]></title><description><![CDATA[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...]]></description><link>https://buildwithdonnie.hashnode.dev/build-and-deploy-a-simple-sepolia-faucet-with-solidity-and-foundry</link><guid isPermaLink="true">https://buildwithdonnie.hashnode.dev/build-and-deploy-a-simple-sepolia-faucet-with-solidity-and-foundry</guid><category><![CDATA[Solidity]]></category><category><![CDATA[foundry]]></category><category><![CDATA[Smart Contracts]]></category><dc:creator><![CDATA[0xdonnie]]></dc:creator><pubDate>Fri, 09 May 2025 15:55:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746805987020/ee68eda7-d8ff-4fc2-8fac-a77d8d9dfacc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>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.</p>
<p>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.</p>
<h2 id="heading-setting-up-the-project"><strong>Setting Up the Project</strong></h2>
<p>Foundry does not work natively on Windows Powershell. Instead, install WSL (windows subsystem for Linux) with the following steps:</p>
<ol>
<li><p>Open Windows Powershell.</p>
</li>
<li><p>Run the command below:</p>
</li>
</ol>
<pre><code class="lang-powershell">wsl -<span class="hljs-literal">-install</span>
</code></pre>
<ol start="3">
<li><p>Once the download is complete, head over to VS code</p>
</li>
<li><p>Click the button at the bottom left corner to open a remote connection and connect to WSL.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746728715139/411c6cff-e4e8-43b0-82a9-e565765b2b11.jpeg" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Open the WSL terminal in VS code and type the command below to install foundry.</li>
</ol>
<pre><code class="lang-powershell"><span class="hljs-built_in">curl</span> <span class="hljs-literal">-L</span> https://foundry.paradigm.xyz | bash
foundryup
</code></pre>
<ol start="6">
<li>Create and enter a folder using the command below:</li>
</ol>
<pre><code class="lang-powershell">mkdir TestFaucet
<span class="hljs-built_in">cd</span> TestFaucet
forge init
</code></pre>
<p>The <code>forge init</code> command initializes a Foundry project in your folder, sets up the structure and configuration needed to write, test, and deploy smart contracts.</p>
<p>Your folder should look like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746614132066/d8f00e99-252e-4859-b95d-26b97735965f.png" alt /></p>
<h2 id="heading-building-the-faucet"><strong>Building the Faucet</strong></h2>
<p>To build the faucet, we will use the Solidity language. To get started:</p>
<ol>
<li><p>Open the <code>src</code> folder in the <code>TestFaucet</code> folder</p>
</li>
<li><p>Create a file named Faucet.sol</p>
</li>
</ol>
<p><em>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.</em></p>
<p>In the <code>Faucet.sol</code> file, type the code below:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.29;</span>
</code></pre>
<ul>
<li><p>The first line is a <strong>license identifier</strong>, 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.</p>
</li>
<li><p>The second line indicates the contract is meant to be written in and compiled with Solidity 0.8.29.</p>
</li>
</ul>
<p>We define a contract named <code>Faucet</code></p>
<pre><code class="lang-solidity"><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Faucet</span> </span>{

}
</code></pre>
<p>In the defined contract <code>Faucet</code>, 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:</p>
<pre><code class="lang-solidity">    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> owner;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> MAX_AMOUNT <span class="hljs-operator">=</span> <span class="hljs-number">0</span><span class="hljs-number">.2</span> <span class="hljs-literal">ether</span>;
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> <span class="hljs-keyword">uint</span>) <span class="hljs-keyword">public</span> lastClaimedAt;
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> <span class="hljs-keyword">bool</span>) <span class="hljs-keyword">public</span> whitelisted;
</code></pre>
<p>The code above:</p>
<ul>
<li><p>Creates an <code>owner</code> variable with the data type of address.</p>
</li>
<li><p>Declares a <code>constant</code> 256-bit unsigned integer named <code>MAX_AMOUNT</code> set to 0.2 ether.</p>
</li>
<li><p>Creates a <code>mapping</code> named lastClaimedAt that maps addresses to uint values.</p>
</li>
<li><p>Creates a <code>mapping</code> named whitelisted that maps addresses to boolean values.</p>
</li>
</ul>
<p>In the <code>Faucet</code> contract, initialize the following events below:</p>
<pre><code class="lang-solidity">    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Whitelisted</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> user</span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">RemovedFromWhitelist</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> user</span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Claimed</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> user, <span class="hljs-keyword">uint256</span> amount</span>)</span>;
</code></pre>
<p><strong>Events</strong> are <strong>log entries</strong> that are recorded on the blockchain. They are used for <strong>communication between your contract and external consumers</strong>, like user interfaces, in a <strong>cost-efficient</strong> way.</p>
<p>The code above creates an event for when a user is whitelisted, removed from the whitelist, and when a user has claimed faucet.</p>
<p>After creating an event, define ownership of the contract using a constructor:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        owner <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
    }
</code></pre>
<p>Constructors in solidity are special functions that run only once, during deployment. In the code above, we set the <code>owner</code> of the contract to the address of the deployer <code>msg.sender</code>. This would help to provide security features on some functions in our contract.</p>
<p>Moving on from constructors, create needed modifiers.</p>
<pre><code class="lang-solidity">    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">onlyWhitelisted</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(whitelisted[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>], <span class="hljs-string">"Not whitelisted"</span>);
        <span class="hljs-keyword">_</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">onlyOwner</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner, <span class="hljs-string">"only owner can call this function"</span>);
        <span class="hljs-keyword">_</span>;
    }
</code></pre>
<p>Modifiers are special keywords in solidity, which is used to change the behaviour of functions. Modifiers are placed beside functions and used for <strong>access control,</strong> <strong>input validation</strong>, or <strong>reusable pre-checks</strong> before a function runs. In this code we use the following modifiers:</p>
<ul>
<li><p><code>onlyWhitelisted</code> - To check if <code>msg.sender</code> is whitelisted.</p>
</li>
<li><p><code>onlyOwner</code> - To check if <code>msg.sender</code> is the owner of the contract.</p>
</li>
</ul>
<p>Proceed to create the necessary functions</p>
<pre><code class="lang-solidity">    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addToWhitelist</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>whitelisted[user], <span class="hljs-string">"Address already whitelisted"</span>);
        whitelisted[user] <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        <span class="hljs-keyword">emit</span> Whitelisted(user);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">removeFromWhitelist</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-built_in">require</span>(whitelisted[user], <span class="hljs-string">"Address not whitelisted"</span>);
        whitelisted[user] <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
        <span class="hljs-keyword">emit</span> RemovedFromWhitelist(user);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claim</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyWhitelisted</span> </span>{
        <span class="hljs-built_in">require</span>(
            <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> lastClaimedAt[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">+</span> <span class="hljs-number">2</span> <span class="hljs-literal">days</span>,
            <span class="hljs-string">"Claim cooldown: wait for 2 days"</span>
        );

        <span class="hljs-keyword">uint</span> payout <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> MAX_AMOUNT
            ? MAX_AMOUNT
            : <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>;
        <span class="hljs-built_in">require</span>(payout <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>, <span class="hljs-string">"Faucet is empty"</span>);
        lastClaimedAt[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>;
        <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">transfer</span>(payout);
        <span class="hljs-keyword">emit</span> Claimed(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, payout);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">drainFaucet</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>;
        <span class="hljs-built_in">require</span>(balance <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>, <span class="hljs-string">"No faucet to drain"</span>);

        <span class="hljs-keyword">payable</span>(owner).<span class="hljs-built_in">transfer</span>(balance);
    }
</code></pre>
<p>There are four main functions for this Faucet:</p>
<ul>
<li><p><code>addToWhitelist()</code> - This function can only be called by the owner of the contract. This function:</p>
<ol>
<li><p>Takes in input of address data type</p>
</li>
<li><p>Checks if the address has not been whitelisted.</p>
</li>
<li><p>Whitelist the address</p>
</li>
<li><p>Sends out a success message</p>
</li>
</ol>
</li>
<li><p><code>removeFromWhitelist()</code> - This function can only be called by the owner of the contract. This function:</p>
<ol>
<li><p>Takes in input of address data type</p>
</li>
<li><p>Checks if the address has been whitelisted.</p>
</li>
<li><p>removes the address from whitelist</p>
</li>
<li><p>Sends out a success message</p>
</li>
</ol>
</li>
<li><p><code>claim()</code> - This function can only be called by whitelisted addresses. This function:</p>
<ol>
<li><p>Checks if the last claim of the whitelisted address is less than 2 days</p>
</li>
<li><p>Checks if the faucet is empty, returns a faucet is empty message if its empty</p>
</li>
<li><p>Saves the time where the address last claimed.</p>
</li>
<li><p>Sends sepolia faucet to eligible address.</p>
</li>
<li><p>Emits a success message which could be used off-chain.</p>
</li>
</ol>
</li>
<li><p><code>drainFaucet()</code> - This function can only be called by the owner of the contract. This function:</p>
<ol>
<li><p>Checks if the faucet balance is empty</p>
</li>
<li><p>Sends all the faucet balance to the owner if its not empty</p>
</li>
</ol>
</li>
</ul>
<p>Lastly, to enable the contract receive ETH, we set a special fallback function in the contract</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">receive</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{}
</code></pre>
<h2 id="heading-deploy-the-faucet-to-sepolia"><strong>Deploy the Faucet to Sepolia</strong></h2>
<p>To deploy our contract to sepolia, we need the following:</p>
<ol>
<li><p><strong>An RPC endpoint</strong></p>
<ul>
<li>A node provider like <strong>Infura</strong>, <strong>Alchemy</strong>, or <strong>Ankr.</strong> We will use Alchemy in this example.</li>
</ul>
</li>
<li><p><strong>An encrypted private key</strong></p>
<ul>
<li><p>This will be used to sign and send the deployment transaction.</p>
</li>
<li><p>For safe practice, we will encrypt the key.</p>
</li>
</ul>
</li>
<li><p><strong>A deploy script written in Solidity</strong></p>
<ul>
<li><p>Uses Foundry's <code>forge-std/Script.sol</code></p>
</li>
<li><p>This script will handle contract deployment logic</p>
</li>
</ul>
</li>
<li><p><strong>Sepolia ETH</strong></p>
<ul>
<li>The private key must control an account with sepolia ETH.</li>
</ul>
</li>
</ol>
<p>To create an RPC endpoint;</p>
<h4 id="heading-1-sign-up-log-in-to-alchemy">1. <strong>Sign Up / Log In to Alchemy</strong></h4>
<p>Go to <a target="_blank" href="https://alchemy.com">Alchemy website</a></p>
<ul>
<li>Click <strong>Sign Up</strong> (or <strong>Log In</strong> if you already have an account)</li>
</ul>
<h4 id="heading-2-create-a-new-app">2. <strong>Create a New App</strong></h4>
<ul>
<li><p>Once logged in, go to your Alchemy dashboard</p>
</li>
<li><p>Click <strong>“+ Create App”</strong></p>
</li>
<li><p>Fill in the form:</p>
<ul>
<li><p><strong>Name:</strong> e.g., <code>SepoliaTestApp</code></p>
</li>
<li><p><strong>Description:</strong> e.g., Create a Faucet</p>
</li>
<li><p><strong>UseCase:</strong> Other project</p>
</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746868898031/e069b3ba-3601-48f4-8007-5496a564f16c.jpeg" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Click <strong>Next</strong></p>
</li>
<li><p>Click <strong>Ethereum</strong></p>
</li>
<li><p>Click <strong>Next</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746869000341/2188e147-d870-4be9-989d-869616bbe870.jpeg" alt class="image--center mx-auto" /></p>
<ul>
<li>Click <strong>“Create App”</strong></li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746869062316/c8e4404f-a465-4983-b68a-1846223cf8cf.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>3. Get the RPC Endpoint</strong></p>
<ol>
<li><p>After the app is created, go to the app's <strong>Details page</strong></p>
</li>
<li><p>Under <strong>“API Key”</strong>, you'll see the <strong>HTTPS endpoint</strong></p>
<ul>
<li><p>Switch the dropdown from mainnet to sepolia</p>
</li>
<li><p>It looks like:</p>
<pre><code class="lang-solidity">  https:<span class="hljs-comment">//eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY</span>
</code></pre>
</li>
</ul>
</li>
<li><p><strong>Copy this URL</strong> — it's your RPC endpoint</p>
</li>
</ol>
<h4 id="heading-4-use-in-foundry">4. <strong>Use in Foundry</strong></h4>
<p>In <code>foundry.toml</code>, save your endpoints :</p>
<pre><code class="lang-solidity">[rpc_endpoints]
sepolia <span class="hljs-operator">=</span> <span class="hljs-string">"https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY"</span>
</code></pre>
<p>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;</p>
<p>Run this command in your WSL terminal</p>
<pre><code class="lang-powershell">cast wallet import defaultKey -<span class="hljs-literal">-interactive</span> //The name of your private key is <span class="hljs-built_in">set</span> to <span class="hljs-string">"defaultKey"</span>
</code></pre>
<p>The terminal prompts you to enter the following</p>
<ol>
<li><p>Your private key</p>
</li>
<li><p>A new password</p>
</li>
</ol>
<p><em>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.</em></p>
<p>Once this has been completed, the terminal logs a success message</p>
<h3 id="heading-deployment-script"><strong>Deployment Script</strong></h3>
<p>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:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: UNLICENSED</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.29;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Script</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Script.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">ApexFaucet</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"../src/ApexFaucet.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">DeployApexFaucet</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Script</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params">ApexFaucet</span>) </span>{
        vm.startBroadcast();
        ApexFaucet apexFaucet <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ApexFaucet();
        vm.stopBroadcast();
        <span class="hljs-keyword">return</span> apexFaucet;
    }
}
</code></pre>
<p>The code above basically starts a broadcast to the rpc node to deploy a new instance of your contract on sepolia.</p>
<p>To deploy the contract using the script, run the command below;</p>
<pre><code class="lang-solidity">forge script script<span class="hljs-operator">/</span>DeployApexFaucet.s.sol:DeployApexFaucet <span class="hljs-operator">-</span><span class="hljs-operator">-</span>rpc<span class="hljs-operator">-</span>url sepolia <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-keyword">private</span><span class="hljs-operator">-</span>key defaultKey <span class="hljs-operator">-</span><span class="hljs-operator">-</span>broadcast
</code></pre>
<p>The command above;</p>
<ul>
<li><p>Compiles the script <code>script/DeployApexFaucet.s.sol</code></p>
</li>
<li><p>Connects to sepolia using the <code>--rpc-url sepolia</code></p>
</li>
<li><p>Signs the transaction with our <code>--private-key defaultKey</code></p>
</li>
<li><p>Uses the <code>--broadcast</code> to send transactions on the sepolia network</p>
</li>
</ul>
<p>Once your contract has been deployed, proceed to fund the contract using the contract address</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>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.</p>
<p>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.</p>
]]></content:encoded></item></channel></rss>