<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Obinna Duru</title>
    <description>The latest articles on DEV Community by Obinna Duru (@binnadev).</description>
    <link>https://dev.to/binnadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3596254%2Fe41e7fc6-92fd-4450-8764-75345f856471.jpg</url>
      <title>DEV Community: Obinna Duru</title>
      <link>https://dev.to/binnadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/binnadev"/>
    <language>en</language>
    <item>
      <title>Integrating Trust: A Developer's Guide to the Resume Protocol</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Sun, 21 Dec 2025 23:59:07 +0000</pubDate>
      <link>https://dev.to/binnadev/integrating-trust-a-developers-guide-to-the-resume-protocol-1f72</link>
      <guid>https://dev.to/binnadev/integrating-trust-a-developers-guide-to-the-resume-protocol-1f72</guid>
      <description>&lt;p&gt;In my previous &lt;a href="https://dev.to/binnadev/your-career-onchain-building-a-resume-protocol-with-purpose-and-trust-3p67"&gt;article&lt;/a&gt;,  I introduced the &lt;strong&gt;Resume Protocol&lt;/strong&gt;: a system designed to make professional reputation verifiable, soulbound, and owned by you.&lt;/p&gt;

&lt;p&gt;But a protocol is only as useful as the tools we build to interact with it.&lt;/p&gt;

&lt;p&gt;To bridge the gap between complex smart contracts and everyday utility, I built the &lt;strong&gt;Resume Integrator&lt;/strong&gt;. This isn't just a script; it is a reference implementation designed to demonstrate &lt;strong&gt;reliability&lt;/strong&gt; and &lt;strong&gt;excellence&lt;/strong&gt; in Web3 engineering.&lt;/p&gt;

&lt;p&gt;Whether you are building a freelance marketplace or a university certification portal, the challenge remains the same: linking rich off-chain evidence (PDFs, images) with on-chain truth (Immutable Ledgers).&lt;/p&gt;

&lt;p&gt;In this guide, I will walk you through the thoughtful architectural decisions behind this integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Engineering Challenge&lt;/strong&gt;&lt;br&gt;
Integrating a blockchain protocol requires us to reconcile two different realities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Evidence (Off-chain):&lt;/strong&gt; The detailed descriptions, design portfolios, and certificates. These are heavy, and storing them on-chain is inefficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Truth (On-chain):&lt;/strong&gt; The cryptographic proof of ownership and endorsement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My goal with the Resume Integrator was to stitch these together seamlessly, creating a system that is robust and user-centric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture&lt;/strong&gt;&lt;br&gt;
I believe that good code tells a story. Here is the visual narrative of how an endorsement travels from a local environment to the blockchain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5ceil3csr5m4tvsrfed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5ceil3csr5m4tvsrfed.png" alt=" " width="697" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Structuring Data with Intent (The Metadata)&lt;/strong&gt;&lt;br&gt;
Clarity is the first step toward reliability. If we upload unstructured data, we create noise. To ensure our endorsements are interoperable with the wider Ethereum ecosystem: wallets, explorers, and marketplaces, we strictly adhere to the &lt;strong&gt;ERC-721 Metadata Standard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I enforced this using strict TypeScript interfaces. We don't guess the shape of our data; we define it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From src/types.ts

export interface Attribute {
  trait_type: string;
  value: string | number | Date;
}

/**
 * Standard ERC-721 Metadata Schema
 * We use strict typing to ensure every credential we mint 
 * is readable by standard wallets.
 */
export interface CredentialMetadata {
  name: string;
  description: string;
  image: string;
  attributes: Attribute[];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: The Storage Layer (Pinata SDK)&lt;/strong&gt;&lt;br&gt;
For our "Evidence" layer, we need permanence. If I rely on a centralized server to host my resume data, my reputation is rented, not owned. That is a risk I am not willing to take.&lt;/p&gt;

&lt;p&gt;We use &lt;strong&gt;IPFS&lt;/strong&gt; (InterPlanetary File System) via the &lt;strong&gt;Pinata SDK&lt;/strong&gt;. I chose Pinata because it offers the reliability of a managed service without compromising the decentralized nature of content addressing.&lt;/p&gt;

&lt;p&gt;Here is the "Two-Step" pattern I implemented to ensure data integrity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Upload the visual proof first.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed that proof's URI into the metadata.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From src/storage.ts

/**
 * Creates NFT-compatible metadata for a credential
 * and uploads it to IPFS via Pinata.
 *
 * This function optionally uploads an image first,
 * then embeds its IPFS URL into the metadata JSON.
 * @param input Credential metadata fields
 *
 * @returns A public IPFS gateway URL pointing to the metadata JSON
 */
export async function createCredentialMetadata(
  input: CredentialMetadataInput
): Promise&amp;lt;string&amp;gt; {
  console.log("Authenticating with Pinata...");
  await pinata.testAuthentication();
  console.log("Pinata authentication successful");

  // Will store the IPFS URL of the uploaded image (if any)
  let image = "";

  // If an image path is provided, upload the image to IPFS first
  if (input.imagePath &amp;amp;&amp;amp; fs.existsSync(input.imagePath)) {
    console.log(`Uploading image: ${input.imagePath}`);
    // Read the image file from disk into a buffer
    const buffer = fs.readFileSync(input.imagePath);

    // Convert the buffer into a File object (Node 18+ compatible)
    const file = new File([buffer], "credential.png", {
      type: "image/png",
    });

    // Upload the image to Pinata's public IPFS network
    const upload = await pinata.upload.public.file(file);

    // Construct a gateway-accessible URL using the returned CID
    image = `https://${CONFIG.PINATA_GATEWAY}/ipfs/${upload.cid}`;
    console.log(`   Image URL: ${image}`);
  } else if (input.imagePath) {
    console.warn(
      `Warning: Image path provided but file not found: ${input.imagePath}`
    );
  }

  // Construct ERC-721 compatible metadata JSON
  // This structure is widely supported by NFT platforms
  const metadata: CredentialMetadata = {
    name: input.skillName,
    description: input.description,
    image,
    attributes: [
      { trait_type: "Recipient", value: input.recipientName },
      { trait_type: "Endorser", value: input.issuerName },
      {
        trait_type: "Date",
        value: new Date(input.endorsementDate.toISOString().split("T")[0]!),
      },
      { trait_type: "Token Standard", value: "Soulbound (SBT)" },
    ],
  };

  // Upload the metadata JSON to IPFS
  console.log("Uploading metadata JSON...");
  const result = await pinata.upload.public.json(metadata);

  // Return a public gateway URL pointing to the metadata
  // This URL can be used directly as a tokenURI on-chain
  return `https://${CONFIG.PINATA_GATEWAY}/ipfs/${result.cid}`;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 3: The Issuance Layer (Viem)&lt;/strong&gt;&lt;br&gt;
With our data secured, we move to the "Truth" layer. We need to instruct the smart contract to mint a Soulbound Token that points to our metadata.&lt;/p&gt;

&lt;p&gt;I chose &lt;strong&gt;Viem&lt;/strong&gt; for this task. It is lightweight, type-safe, and aligns with my preference for precision over bloat.&lt;/p&gt;

&lt;p&gt;The most critical engineering decision here is &lt;strong&gt;Waiting for Confirmation&lt;/strong&gt;. In blockchain systems, broadcasting a transaction is not enough; we must ensure it is finalized. This prevents UI glitches and ensures the user knows exactly when their reputation is secured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From src/contract.ts

/**
 * Mint a new endorsement onchain
 */
export async function mintEndorsement(
  recipient: string,
  skill: string,
  dataURI: string
) {
  if (!CONFIG.CONTRACT_ADDRESS)
    throw new Error("Contract Address not set in .env");

  console.log(`Minting endorsement for ${skill}...`);

  const hash = await walletClient.writeContract({
    address: CONFIG.CONTRACT_ADDRESS,
    abi: CONTRACT_ABI,
    functionName: "endorsePeer",
    args: [recipient, skill, dataURI],
  });

  console.log(`   Tx Sent: ${hash}`);

  // Wait for confirmation
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log(`Confirmed in block ${receipt.blockNumber}`);

  return hash;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Verification (The Read)&lt;/strong&gt;&lt;br&gt;
A protocol is useless if we cannot retrieve the data efficiently.&lt;/p&gt;

&lt;p&gt;Querying a blockchain state variable by variable is slow and expensive. Instead, we use &lt;strong&gt;Event Logs&lt;/strong&gt;. By listening to the &lt;code&gt;EndorsementMinted&lt;/code&gt; event, we can reconstruct a user's entire professional history in a single, efficient query. This is thoughtful engineering that respects both the network and the user's time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// From src/contract.ts

/**
 * Find all endorsements for a specific user
 */
export async function getEndorsementsFor(userAddress: string) {
  if (!CONFIG.CONTRACT_ADDRESS)
    throw new Error("Contract Address not set in .env");

  console.log(`Querying endorsements for ${userAddress}...`);

  const logs = await publicClient.getLogs({
    address: CONFIG.CONTRACT_ADDRESS,
    event: parseAbiItem(
      "event EndorsementMinted(uint256 indexed tokenId, address indexed issuer, address indexed recipient, bytes32 skillId, string skill, uint8 status)"
    ),
    args: {
      recipient: userAddress as Hex,
    },
    fromBlock: "earliest",
  });

  return logs.map((log) =&amp;gt; ({
    tokenId: log.args.tokenId,
    skill: log.args.skill,
    issuer: log.args.issuer,
    status: log.args.status === 1 ? "Active" : "Pending",
  }));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
The &lt;strong&gt;Resume Integrator&lt;/strong&gt; is more than a codebase. It is a blueprint for building with purpose.&lt;/p&gt;

&lt;p&gt;By separating our concerns: using IPFS for heavy data and the Blockchain for trust, we create a system that is efficient, immutable, and scalable. By enforcing strict types and waiting for confirmations, we ensure reliability for our users.&lt;/p&gt;

&lt;p&gt;The Resume Protocol is the foundation. This Integrator is the bridge. Now, it is up to you to build the interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repositories:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Protocol (Smart Contracts):&lt;/strong&gt; &lt;a href="https://github.com/obinnafranklinduru/nft-resume-protocol" rel="noopener noreferrer"&gt;github.com/obinnafranklinduru/nft-resume-protocol&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Integrator (Sample Client):&lt;/strong&gt; &lt;a href="https://github.com/obinnafranklinduru/resume-integrator" rel="noopener noreferrer"&gt;github.com/obinnafranklinduru/resume-integrator&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build something you can trust with clarity, purpose, and excellence.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>web3</category>
      <category>typescript</category>
      <category>ipfs</category>
    </item>
    <item>
      <title>Your Career, Onchain: Building a Resume Protocol with Purpose and Trust</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Sun, 21 Dec 2025 14:57:58 +0000</pubDate>
      <link>https://dev.to/binnadev/your-career-onchain-building-a-resume-protocol-with-purpose-and-trust-3p67</link>
      <guid>https://dev.to/binnadev/your-career-onchain-building-a-resume-protocol-with-purpose-and-trust-3p67</guid>
      <description>&lt;p&gt;In the traditional world, a resume is just a PDF. It is a claim, not a proof. Anyone can write "Expert in Solidity" on a document, and verifying that claim requires trust, phone calls, and manual friction.&lt;/p&gt;

&lt;p&gt;As a smart contract engineer, I look at systems through the lens of &lt;strong&gt;reliability&lt;/strong&gt;. I asked myself: Why is our professional reputation: one of our most valuable assets, stored on fragile, centralized servers?&lt;/p&gt;

&lt;p&gt;I wanted to solve this by building the &lt;strong&gt;Resume Protocol&lt;/strong&gt;: a decentralized registry for peer-to-peer professional endorsements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhwhwshrob1yxd5rlt71c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhwhwshrob1yxd5rlt71c.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't just about putting data on a blockchain. It is about engineering a system where trust is cryptographic, ownership is absolute, and the design is thoughtful. Here is how I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem: Trust is Fragile&lt;/strong&gt;&lt;br&gt;
We currently rely on platforms like LinkedIn to host our professional identities. While useful, these platforms have structural weaknesses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fabrication:&lt;/strong&gt; Claims are self-reported and often unverified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralization:&lt;/strong&gt; Your endorsements live on a company's database. If they change their API or ban your account, your reputation vanishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lack of Ownership:&lt;/strong&gt; You rent your profile; you do not own it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My mission was to engineer a solution where reliability is baked into the code. I wanted a system that was &lt;strong&gt;Verifiable&lt;/strong&gt; (traceable to a real address), &lt;strong&gt;Soulbound&lt;/strong&gt; (non-transferable), and &lt;strong&gt;Consensual&lt;/strong&gt; (you control what appears on your profile).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Soulbound Tokens (SBTs)&lt;/strong&gt;&lt;br&gt;
To engineer this, I utilized &lt;strong&gt;Soulbound Tokens (SBTs)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In technical terms, this is a modified ERC-721 token. Unlike a standard NFT, which is designed to be traded or sold, an SBT is bound to an identity. Think of it like a university degree or a Nobel Prize, it belongs to you, and you cannot sell it to someone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional Resume vs. Onchain Protocol&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Traditional Resume&lt;/th&gt;
&lt;th&gt;Resume Protocol (Onchain)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;Self-claimed&lt;/td&gt;
&lt;td&gt;Peer-verified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;Hosted by platforms&lt;/td&gt;
&lt;td&gt;Owned by YOU (Soulbound)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust&lt;/td&gt;
&lt;td&gt;Hard to verify&lt;/td&gt;
&lt;td&gt;Cryptographically verifiable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transferability&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Non-transferable (Identity-bound)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By deploying this on &lt;strong&gt;&lt;a href="https://basescan.org/" rel="noopener noreferrer"&gt;Base&lt;/a&gt;&lt;/strong&gt;, we leverage the security of Ethereum with the accessibility required for a global onchain economy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture of Trust&lt;/strong&gt;&lt;br&gt;
Excellence in engineering means choosing clarity over complexity. The protocol works on a simple but rigorous state machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Analogy:&lt;/strong&gt; Imagine a Digital Award Ceremony.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Issuer&lt;/strong&gt; (your manager) decides to give you an award.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Protocol&lt;/strong&gt; (the stage) checks if they are allowed to give awards right now (Rate Limiting).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Award&lt;/strong&gt; (the Token) is presented to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Status:&lt;/strong&gt; It starts as "Pending" until you walk up and accept it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The "Consent" Pattern&lt;/strong&gt;&lt;br&gt;
One of the most deliberate design choices I made was the &lt;strong&gt;Consent Mechanism&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In many onchain systems, if someone sends you a token, it just appears in your wallet. For a professional resume, this is a vulnerability. You do not want spam or malicious endorsements clogging your reputation.&lt;/p&gt;

&lt;p&gt;Therefore, the protocol enforces a two-step lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pending:&lt;/strong&gt; An issuer sends an endorsement. It sits in a "limbo" state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active:&lt;/strong&gt; You, the recipient, must explicitly sign a transaction to &lt;code&gt;acceptEndorsement&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This puts the user in control. It is a small detail, but it reflects a thoughtful approach to user safety.&lt;/p&gt;

&lt;p&gt;This diagram shows how a user interacts with the system to send an endorsement.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz88onpthvvgnwtmdtwio.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz88onpthvvgnwtmdtwio.png" alt=" " width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Look at the Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's look at the heart of the endorsement logic. I wrote this in Solidity using &lt;strong&gt;Foundry&lt;/strong&gt; for rigorous testing.&lt;/p&gt;

&lt;p&gt;Notice the specific checks. Every line represents a decision to prioritize security and reliability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    /**
     * @notice Peer-to-Peer Endorsement with Anti-Spam checks.
     * @dev Enforces Rate Limits. Mints as PENDING (requires acceptance).
     */
    function endorsePeer(address to, string calldata skill, string calldata dataURI) external {
        // 1. Rate Limit Check
        if (block.timestamp &amp;lt; lastEndorsementTimestamp[msg.sender] + RATE_LIMIT_COOLDOWN) {
            revert RateLimitExceeded();
        }

        // 2. Mint as Pending
        _mintEndorsement(msg.sender, to, skill, dataURI, Status.Pending);

        // 3. Update Timestamp (Checks-Effects-Interactions)
        lastEndorsementTimestamp[msg.sender] = uint64(block.timestamp);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Making it "Soulbound"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To ensure the token behaves as a reputation marker and not a financial asset, we override the transfer logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        address from = _ownerOf(tokenId);
        if (from != address(0) &amp;amp;&amp;amp; to != address(0)) {
            revert SoulboundNonTransferable();
        }
        return super._update(to, tokenId, auth);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Engineering Excellence: Testing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smart contracts handle real value and in this case, real reputations. Therefore, "it works on my machine" is not enough.&lt;br&gt;
I used Foundry to subject this protocol to extensive verification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit Tests:&lt;/strong&gt; Verifying every state transition (Pending -&amp;gt; Active).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzz Testing:&lt;/strong&gt; I threw thousands of random inputs at the contract to ensure it handles edge cases gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invariant Testing:&lt;/strong&gt; Ensuring that no matter what happens, a user can never transfer their reputation token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzla31nvwfbv8hh8q2u9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzla31nvwfbv8hh8q2u9d.png" alt=" " width="800" height="154"&gt;&lt;/a&gt;&lt;br&gt;
This rigor is what separates "code" from "engineering."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building for the Future&lt;/strong&gt;&lt;br&gt;
I built the Resume Protocol because I believe the future of work is onchain. We are moving toward a world where your history, your skills, and your reputation are portable assets that you own.&lt;/p&gt;

&lt;p&gt;This project is open-source. It is an invitation to collaborate. If you are a developer, a designer, or a builder who cares about &lt;strong&gt;clarity, purpose,&lt;/strong&gt; and &lt;strong&gt;excellence,&lt;/strong&gt; I invite you to review the code and contribute.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/obinnafranklinduru/nft-resume-protocol" rel="noopener noreferrer"&gt;github.com/obinnafranklinduru/nft-resume-protocol&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am Obinna Duru, a Smart Contract Engineer dedicated to building reliable, secure, and efficient onchain systems.&lt;/p&gt;

&lt;p&gt;Let's connect and build something you can trust..&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://linkedin.com/in/obinna-franklin-duru" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://x.com/BinnaDev" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://binnadev.vercel.app" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📚 &lt;strong&gt;Beginner's Glossary&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SBT (Soulbound Token):&lt;/strong&gt; A non-transferable NFT. Once it's in your wallet, it's yours forever (unless revoked).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Contract:&lt;/strong&gt; A digital program stored on the blockchain that runs automatically when specific conditions are met.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limit:&lt;/strong&gt; A security feature that prevents users from spamming the network by forcing them to wait between actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foundry:&lt;/strong&gt; A blazing fast toolkit for Ethereum application development written in Rust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onchain:&lt;/strong&gt; Anything that lives directly on the blockchain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;"Smart contracts handle real value, so I build with reliability, thoughtfulness, and excellence."&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>solidity</category>
      <category>beginners</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Engineering Trust: My Journey Building a Decentralized Stablecoin</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 01 Dec 2025 16:48:35 +0000</pubDate>
      <link>https://dev.to/binnadev/engineering-trust-my-journey-building-a-decentralized-stablecoin-a9p</link>
      <guid>https://dev.to/binnadev/engineering-trust-my-journey-building-a-decentralized-stablecoin-a9p</guid>
      <description>&lt;p&gt;"Smart contracts handle real value."&lt;/p&gt;

&lt;p&gt;This simple truth drives everything I do as an engineer. When we write code for the blockchain, we aren't just pushing pixels or serving data; we are building financial structures that people need to rely on. There is no customer support hotline in DeFi. If the math is wrong, the trust is broken.&lt;/p&gt;

&lt;p&gt;That responsibility is why I decided to tackle my &lt;strong&gt;milestone project:&lt;/strong&gt; a decentralized, &lt;strong&gt;&lt;a href="https://github.com/obinnafranklinduru/crypto-backed-stablecoin" rel="noopener noreferrer"&gt;crypto-backed stablecoin&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Inspired by the &lt;strong&gt;&lt;a href="https://updraft.cyfrin.io/courses" rel="noopener noreferrer"&gt;Cyfrin Updraft curriculum&lt;/a&gt;&lt;/strong&gt; and the guidance of &lt;strong&gt;&lt;a href="https://x.com/PatrickAlphaC" rel="noopener noreferrer"&gt;Patrick Collins&lt;/a&gt;&lt;/strong&gt; (huge shoutout to the legend himself), I wanted to go beyond just copying a tutorial. I wanted to engineer a system that embodies my core values: &lt;strong&gt;Reliability, Thoughtfulness, and Excellence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article is my &lt;strong&gt;engineering journal&lt;/strong&gt;. Whether you are a total beginner or a Solidity vet, I want to take you under the hood of how a digital dollar is actually built, the security patterns that keep it safe, and the lessons I learned along the way.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The "What": 3 Big Words, 1 Simple Concept&lt;/strong&gt;&lt;br&gt;
When I started, the technical definition of this project terrified me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An Exogenous, Crypto-Collateralized, Algorithmic Stablecoin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It sounds complicated, but let's strip away the jargon.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exogenous:&lt;/strong&gt; It is backed by assets outside the protocol (like Ethereum and Bitcoin), not by its own token. This prevents the "death spiral" we saw with Terra/Luna.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crypto-Collateralized:&lt;/strong&gt; You can't just print money. You have to deposit crypto assets (Collateral) first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithmic:&lt;/strong&gt; There is no central bank. A smart contract does the math to decide if you are rich enough to mint more money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3lz4zis39d8197ws1b8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3lz4zis39d8197ws1b8.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The Architecture: The Cash, The Brain, and The Eyes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the first decisions I made was to separate concerns. Monolithic code is dangerous code. Instead, I built a modular system relying on three distinct pillars.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76cpwch8thxsqkvt6834.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76cpwch8thxsqkvt6834.png" alt=" " width="432" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Cash: &lt;code&gt;DecentralizedStableCoin.sol&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Think of this contract as the physical paper bills.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is an &lt;strong&gt;ERC20 token&lt;/strong&gt; (like USDC or DAI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Detail:&lt;/strong&gt; It is "owned" by the Engine. I wrote it so that no one, not even me, can mint tokens manually. Only the logic of the Engine can trigger the printer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. The Brain: &lt;code&gt;DSCEngine.sol&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
This is where the magic (and the math) happens. &lt;br&gt;
This contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Holds the user's collateral.&lt;/li&gt;
&lt;li&gt;Tracks everyone's debt.&lt;/li&gt;
&lt;li&gt;Calculates the "Health Factor" (more on this soon).&lt;/li&gt;
&lt;li&gt;Enforces the rules of the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. The Eyes: &lt;code&gt;OracleLib.sol&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Blockchains are isolated; they don't know that the price of Bitcoin changed 5 minutes ago. We need "Oracles" specifically &lt;strong&gt;Chainlink Data Feeds&lt;/strong&gt; to bridge that gap. I wrapped these feeds in a custom library called &lt;code&gt;OracleLib&lt;/code&gt; to add extra safety checks. If the Oracle goes silent (stale data), my system pauses to prevent catastrophe.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The Mechanics: How to Print Digital Money&lt;/strong&gt;&lt;br&gt;
Let's walk through a scenario. Imagine you want to borrow $100 of my stablecoin (let's call it &lt;strong&gt;BUSC&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Bank Vault" Rule (Over-Collateralization)&lt;/strong&gt;&lt;br&gt;
In traditional banking, they trust your credit score. In DeFi, we trust your assets. To borrow &lt;strong&gt;$100 of BUSC&lt;/strong&gt;, the system forces you to deposit &lt;strong&gt;$200 worth of ETH&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwiqddkag5enyzsbptcys.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwiqddkag5enyzsbptcys.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why the 200% requirement? Because crypto is volatile. If ETH crashes by 50% tomorrow, the protocol needs to ensure there is still enough value in the vault to back the stablecoin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Health Factor ❤️&lt;/strong&gt;&lt;br&gt;
This is the heartbeat of the protocol. I implemented a function called &lt;code&gt;_healthFactor()&lt;/code&gt; that runs every time a user tries to do anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health Factor = (Collateral Value X Liquidation Threshold) / Total Debt&lt;/strong&gt;&lt;br&gt;
​&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health Factor &amp;gt; 1:&lt;/strong&gt; You are safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Factor = 1:&lt;/strong&gt; You are on the edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Factor &amp;lt; 1: DANGER.&lt;/strong&gt; You are insolvent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a user tries to mint enough BUSC to drop their health factor below 1, the transaction simply &lt;code&gt;reverts&lt;/code&gt; (fails). The code refuses to let them make a bad financial decision.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The "Necessary Evil": Liquidation&lt;/strong&gt;&lt;br&gt;
This was the hardest concept for me to wrap my head around initially, but it is the most crucial for reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the price of ETH crashes?&lt;/strong&gt; If &lt;code&gt;User A&lt;/code&gt; has $100 of debt and their collateral drops to $90, the system is broken. The stablecoin is no longer stable.&lt;/p&gt;

&lt;p&gt;To prevent this, we have Liquidators.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63nza3ort8mvevfdimdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63nza3ort8mvevfdimdc.png" alt=" " width="616" height="939"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Think of Liquidators as the protocol's cleanup crew.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They monitor the blockchain for insolvent users.&lt;/li&gt;
&lt;li&gt;They pay off the bad debt (burning their own BUSC).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Incentive:&lt;/strong&gt; The protocol rewards them with the user's collateral plus a 10% bonus.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It sounds harsh, but it's fair. It ensures that bad debt is wiped out by the free market, keeping the protocol solvent for everyone else.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Security Patterns: Engineering for Excellence&lt;/strong&gt;&lt;br&gt;
Building this wasn't just about making it work; it was about making it &lt;strong&gt;secure&lt;/strong&gt;. Here are two specific patterns I used to protect user funds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Pull-Over-Push Pattern&lt;/strong&gt;&lt;br&gt;
In early smart contracts, if the protocol owed you money, it would automatically "push" it to your wallet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Risk:&lt;/strong&gt; If your wallet was a malicious contract, it could reject the transfer, causing the entire protocol to freeze (Denial of Service).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My Solution:&lt;/strong&gt; I used the "Pull" pattern. The protocol updates your balance, but you have to initiate the withdrawal. This shifts the risk away from the protocol and puts control in the user's hands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Staleness Checks on Oracles&lt;/strong&gt;&lt;br&gt;
What if Chainlink goes down? What if the price of ETH freezes at $2,000 while the real world crashes to $500? If I blindly trusted the Oracle, users could drain the vault.&lt;/p&gt;

&lt;p&gt;I implemented a check in &lt;code&gt;OracleLib&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

/**
 * @title OracleLib
 * @author Obinna Franklin Duru
 * @notice This library is used to check the Chainlink Oracle for stale data.
 * If a price is stale, functions will revert, rendering the DSCEngine unusable - this is by design.
 * We want the DSCEngine to freeze if prices are not accurate.
 *
 * If the Chainlink network explodes and you have a lot of money locked in the protocol... too bad.
 */
library OracleLib {
    error OracleLib__StalePrice();

    uint256 private constant TIMEOUT = 3 hours; // 3 * 60 * 60 = 10800 seconds

    function staleCheckLatestRoundData(AggregatorV3Interface priceFeed)
        public
        view
        returns (uint80, int256, uint256, uint256, uint80)
    {
        (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =
            priceFeed.latestRoundData();

        if (updatedAt == 0 || answeredInRound &amp;lt; roundId) {
            revert OracleLib__StalePrice();
        }

        uint256 secondsSince = block.timestamp - updatedAt;
        if (secondsSince &amp;gt; TIMEOUT) {
            revert OracleLib__StalePrice();
        }

        return (roundId, answer, startedAt, updatedAt, answeredInRound);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;thoughtfulness&lt;/strong&gt; in code. I'd rather have the protocol stop working temporarily than function incorrectly.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Testing: The "Aha!" Moment&lt;/strong&gt;&lt;br&gt;
I didn't just write unit tests. I learned &lt;strong&gt;Stateful&lt;/strong&gt; Invariant Fuzzing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard tests check:&lt;/strong&gt; "Does 1 + 1 = 2?" &lt;br&gt;
&lt;strong&gt;Fuzz tests scream:&lt;/strong&gt; "What happens if I send random numbers, empty data, and massive values all at once?"&lt;/p&gt;

&lt;p&gt;I wrote a test called &lt;code&gt;invariant_protocolMustHaveMoreValueThanTotalSupply&lt;/code&gt;. It runs thousands of random scenarios: deposits, crashes, liquidations and constantly checks one golden rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The value in the vault MUST always be greater than the total supply of stablecoins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ojo9fh1w3jwxcr2iduk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ojo9fh1w3jwxcr2iduk.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watching those tests pass was the moment I realized: This system is actually robust.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;br&gt;
This project was more than just a repo on my GitHub. It was a masterclass in risk management, system design, and the ethos of Web3.&lt;/p&gt;

&lt;p&gt;We are building the future of finance. That future demands &lt;strong&gt;reliability:&lt;/strong&gt; systems that don't break when the market panics. It demands &lt;strong&gt;thoughtfulness:&lt;/strong&gt; code that anticipates failure. And it demands &lt;strong&gt;excellence:&lt;/strong&gt; refusing to settle for "good enough."&lt;/p&gt;

&lt;p&gt;I am excited to take these lessons into my next project. If you want to see the code, break it, or build on top of it, check out the repo below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/obinnafranklinduru/crypto-backed-stablecoin" rel="noopener noreferrer"&gt;Link to GitHub Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's build something you can trust.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;📚 The DeFi Glossary (For the Curious)&lt;/strong&gt;&lt;br&gt;
If you are new to Web3, some of these terms might feel like alien language. Here is a cheat sheet I wish I had when I started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart Contract:&lt;/strong&gt; A digital agreement that lives on the blockchain. It executes automatically, no middlemen, no "I'll pay you Tuesday."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stablecoin:&lt;/strong&gt; A cryptocurrency designed to stay at a fixed value (usually $1.00), avoiding the wild price swings of Bitcoin or Ethereum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collateral:&lt;/strong&gt; Assets (like ETH or BTC) that you lock up to secure a loan. Think of it like pawning a watch to get cash, but you get the watch back when you repay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peg:&lt;/strong&gt; The target price of the stablecoin (e.g., "Pegged to $1.00").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minting:&lt;/strong&gt; The act of creating new tokens. In this protocol, you "mint" stablecoins when you lock up collateral.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burning:&lt;/strong&gt; The opposite of minting. It permanently destroys tokens, removing them from circulation (usually when you repay a debt).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liquidator:&lt;/strong&gt; A user (usually a bot) who monitors the system for risky loans. They pay off bad debt to keep the system safe and earn a profit for doing so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solvency:&lt;/strong&gt; Being able to pay what you owe. If the protocol has $100 of assets and $90 of debt, it is &lt;strong&gt;solvent&lt;/strong&gt;. If it has $100 of debt and $90 of assets, it is &lt;strong&gt;insolvent&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle:&lt;/strong&gt; A service (like Chainlink) that fetches real-world data (like the price of Gold or ETH) and puts it on the blockchain for smart contracts to read.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>web3</category>
      <category>stablecoin</category>
      <category>opensource</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Monad is Fast. Your Code Should Be Reliable. (A New Foundry Starter Kit)</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Fri, 28 Nov 2025 13:11:06 +0000</pubDate>
      <link>https://dev.to/binnadev/monad-is-fast-your-code-should-be-reliable-a-new-foundry-starter-kit-28h1</link>
      <guid>https://dev.to/binnadev/monad-is-fast-your-code-should-be-reliable-a-new-foundry-starter-kit-28h1</guid>
      <description>&lt;p&gt;A thoughtful, hardened Foundry starter kit for building secure applications on Monad's 10,000 TPS network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed Requires Stability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monad is changing the conversation. With 10,000 TPS, 1-second finality, and parallel execution, it isn't just "another EVM chain", it is a high-performance engine for the next generation of onchain applications.&lt;/p&gt;

&lt;p&gt;But in high-performance environments, &lt;strong&gt;reliability&lt;/strong&gt; is everything.&lt;/p&gt;

&lt;p&gt;When we build at speed, the cracks in our foundation show up faster. A default configuration might work for a hackathon, but does it communicate trust? Is it thoughtful enough to handle the nuances of &lt;code&gt;via_ir&lt;/code&gt; optimization or the rigor of property-based fuzz testing?&lt;/p&gt;

&lt;p&gt;I built the &lt;a href="https://github.com/obinnafranklinduru/monad-foundry-starter" rel="noopener noreferrer"&gt;Monad Foundry Starter Kit&lt;/a&gt; to answer that question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwyeysg8bvzkdwfrlyrf0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwyeysg8bvzkdwfrlyrf0.png" alt=" " width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not just a template. It is a standard for &lt;strong&gt;Reliable, Thoughtful,&lt;/strong&gt; and &lt;strong&gt;Excellent&lt;/strong&gt; engineering on Monad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Another Starter Kit?&lt;/strong&gt;&lt;br&gt;
The Monad team has done an incredible job with their &lt;a href="https://github.com/monad-developers/foundry-monad" rel="noopener noreferrer"&gt;official resources&lt;/a&gt;, providing a great entry point for developers. My goal isn't to replace that, but to &lt;strong&gt;harden&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;I wanted to build an environment that feels like a professional workshop where the tools are sharp, the safety protocols are in place, and the workflow is designed for craftsmanship, not just speed.&lt;/p&gt;

&lt;p&gt;Here is how this kit brings &lt;strong&gt;Reliability&lt;/strong&gt; and &lt;strong&gt;Thoughtfulness&lt;/strong&gt; to your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Optimized for Monad's Engine (&lt;code&gt;via_ir&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
Monad's execution environment is highly optimized. To match that, your Solidity code should be too.&lt;/p&gt;

&lt;p&gt;In this kit, the &lt;code&gt;foundry.toml&lt;/code&gt; comes pre-configured with &lt;code&gt;via_ir = true&lt;/code&gt; and the &lt;code&gt;cancun&lt;/code&gt; EVM version. This ensures your contracts take full advantage of the Intermediate Representation (IR) optimization pipeline, which is crucial for gas efficiency and complex logic on a high-throughput chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[profile.default]
src = "src"
out = "out"
test = "test"
script = "script"
libs = ["lib"]

# Compiler Settings
solc_version = "0.8.24"
evm_version = "cancun"
optimizer = true
optimizer_runs = 200
via_ir = true 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Thoughtful Detail:&lt;/strong&gt; Many devs forget to enable this until deployment day. We make it the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A Makefile That Speaks "Developer"&lt;/strong&gt;&lt;br&gt;
Complex Foundry commands are powerful, but they are also prone to typos. I've abstracted the complexity into a robust &lt;code&gt;Makefile&lt;/code&gt; that handles the heavy lifting reliably.&lt;/p&gt;

&lt;p&gt;Instead of remembering long flags for verification or deployment, you execute clear, intent-based commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make build          # Compiles with optimization
make test           # Runs Unit + Integration tests
make test-gas       # Generates a gas report
make deploy-testnet # Deploys reliably to Monad Testnet
make verify-testnet # Verifies on Blockvision/Sourcify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Reliable Detail:&lt;/strong&gt; The &lt;code&gt;deploy&lt;/code&gt; commands include safety checks and clear separation between local (Anvil) and network (Monad) environments, preventing accidental mainnet deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Testing: Beyond "It Works"&lt;/strong&gt;&lt;br&gt;
"It works on my machine" isn't enough for decentralized finance. This kit establishes a testing culture based on &lt;code&gt;Negative Testing&lt;/code&gt; and &lt;code&gt;Fuzzing&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;- Unit Tests:&lt;/strong&gt; We don't just test that a user can update a value; we explicitly test that unauthorized users revert.&lt;br&gt;
&lt;strong&gt;- Fuzz Tests:&lt;/strong&gt; Included by default (&lt;code&gt;MonadGreeterFuzz.t.sol&lt;/code&gt;), running 10,000 randomized scenarios to catch edge cases you might miss.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4u7br9j92nnpttagl2xk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4u7br9j92nnpttagl2xk.png" alt=" " width="454" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Excellent Detail:&lt;/strong&gt; The kit includes a pre-built CI workflow (&lt;code&gt;ci.yml&lt;/code&gt;) that enforces these tests on every Pull Request. You can't merge broken code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting Started&lt;/strong&gt;&lt;br&gt;
Onboarding to Monad should be seamless. Here is how you can start building with clarity today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Clone the Standard&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/obinnafranklinduru/monad-foundry-starter
cd /monad-foundry-starter
make install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Secure Configuration&lt;br&gt;
We use a thoughtful &lt;code&gt;.gitignore&lt;/code&gt; strategy to keep your secrets safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp .env.example .env
# Add your MONAD_TESTNET_RPC and PRIVATE_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Build &amp;amp; Deploy&lt;/strong&gt;&lt;br&gt;
Experience the flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make build
make test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you are ready to go onchain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make deploy-testnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Vision&lt;/strong&gt;&lt;br&gt;
We are part of a movement. Monad provides the speed; we provide the stability.&lt;/p&gt;

&lt;p&gt;I believe that every line of code we write is a promise to the user. By starting with a foundation that values &lt;strong&gt;Reliability, Thoughtfulness,&lt;/strong&gt; and &lt;strong&gt;Excellence,&lt;/strong&gt; we aren't just deploying contracts-we are building an ecosystem that people can trust.&lt;/p&gt;

&lt;p&gt;If you are a developer looking to build on &lt;a href="https://www.monad.xyz" rel="noopener noreferrer"&gt;Monad&lt;/a&gt; with precision, this kit is for you.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/obinnafranklinduru/monad-foundry-starter" rel="noopener noreferrer"&gt;https://github.com/obinnafranklinduru/monad-foundry-starter&lt;/a&gt;&lt;br&gt;
Connect: &lt;a href="https://x.com/BinnaDev" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's build something excellent.&lt;/p&gt;

&lt;p&gt;Obinna Duru (&lt;a href="https://binnadev.vercel.app" rel="noopener noreferrer"&gt;BinnaDev&lt;/a&gt;) is a Smart Contract Engineer dedicated to building reliable, secure, and efficient onchain systems.&lt;/p&gt;

</description>
      <category>monad</category>
      <category>foundry</category>
      <category>solidity</category>
      <category>web3</category>
    </item>
    <item>
      <title>Building a Reusable, Multi-Event Soulbound NFT Contract in Solidity</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Fri, 14 Nov 2025 10:46:22 +0000</pubDate>
      <link>https://dev.to/binnadev/building-a-reusable-multi-event-soulbound-nft-contract-in-solidity-1hmm</link>
      <guid>https://dev.to/binnadev/building-a-reusable-multi-event-soulbound-nft-contract-in-solidity-1hmm</guid>
      <description>&lt;p&gt;As onchain engineers, our work handles real value. This demands reliability, thoughtfulness, and a security-first mindset. We aren't just building innovative systems; we're building systems people can trust.&lt;/p&gt;

&lt;p&gt;I engineered a production-ready &lt;code&gt;EventCertificate&lt;/code&gt; contract for issuing soulbound (non-transferable) attendance certificates. The primary goal wasn't just to mint an NFT; it was to build a &lt;strong&gt;scalable, secure, and reusable&lt;/strong&gt; onchain framework that could serve hundreds of future events from a single deployed contract.&lt;/p&gt;

&lt;p&gt;Deploying a new contract for every event is gas-intensive, a maintenance nightmare, and fragments a project's onchain identity. I want to share the architecture of my solution, focusing on the design patterns that make it robust and efficient.&lt;/p&gt;

&lt;p&gt;You can find the full project here, including the backend relayer and frontend DApp:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/obinnafranklinduru/event-cert" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://base.blockscout.com/address/0x423F09321A5aA584f2b06F7FD987f8718f3caA6a" rel="noopener noreferrer"&gt;Deployed Contract (Base)&lt;/a&gt;&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://event-cert.vercel.app/" rel="noopener noreferrer"&gt;Frontend DApp&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The High-Level System Architecture&lt;/strong&gt;&lt;br&gt;
Before diving into the contract, here's the end-to-end flow. It starts with the off-chain organizer preparing the metadata and Merkle tree, and ends with the on-chain contract verifying the relayer's mint transaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjg115z7cic5571t6wqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjg115z7cic5571t6wqp.png" alt="High-Level System Architecture" width="800" height="347"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;If the image appears blurred, you can view it clearly &lt;a href="https://raw.githubusercontent.com/obinnafranklinduru/event-cert/refs/heads/main/assets/system_flow.png" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The "Campaign" Model: A Multi-Tenant Architecture&lt;/strong&gt;&lt;br&gt;
Instead of a one-and-done contract, I implemented a "Campaign" model. This allows a single &lt;code&gt;EventCertificate&lt;/code&gt; instance to manage an unlimited number of distinct minting events.&lt;/p&gt;

&lt;p&gt;The core of this is the &lt;code&gt;MintingCampaign&lt;/code&gt; struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct MintingCampaign {
    bytes32 merkleRoot;  // Unique whitelist for this event
    uint256 startTime;   // When minting can begin
    uint256 endTime;     // When minting ends
    uint256 maxMints;    // Supply cap for this campaign
    bool isActive;       // Admin-controlled toggle
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mapping, &lt;code&gt;mapping(uint256 =&amp;gt; MintingCampaign) public campaigns;&lt;/code&gt;, stores each campaign by a unique &lt;code&gt;campaignId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is this better?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; The owner can launch new events simply by calling &lt;code&gt;createCampaign()&lt;/code&gt;, a simple storage-writing transaction. No re-deployment needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency:&lt;/strong&gt; All events share the same core logic, which is far more gas-efficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clarity:&lt;/strong&gt; It provides a single, trusted onchain source for all of an organization's events, rather than a dozen different contract addresses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. The Trusted Relayer &amp;amp; Merkle Whitelisting&lt;/strong&gt;&lt;br&gt;
Security and user experience were non-negotiable. This system needed to be both secure against bots and provide a gasless minting experience for attendees. The solution was a combination of Merkle Proofs and a trusted relayer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merkle Proofs for Scalable Whitelists&lt;/strong&gt;&lt;br&gt;
We can't store 10,000 addresses in an onchain array. The &lt;code&gt;merkleRoot&lt;/code&gt; in the &lt;code&gt;MintingCampaign&lt;/code&gt; struct is the key. Off-chain, we generate a Merkle tree from the list of attendee addresses. To mint, a user must provide a valid &lt;code&gt;merkleProof&lt;/code&gt; for their address.&lt;/p&gt;

&lt;p&gt;The contract verifies this with a single, efficient check using &lt;strong&gt;&lt;a href="https://github.com/OpenZeppelin/openzeppelin-contracts" rel="noopener noreferrer"&gt;OpenZeppelin's library&lt;/a&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes32 leaf = keccak256(abi.encodePacked(attendee));
if (!MerkleProof.verify(merkleProof, campaign.merkleRoot, leaf)) {
    revert InvalidProof();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Trusted Relayer Pattern&lt;/strong&gt;&lt;br&gt;
The &lt;strong&gt;mint&lt;/strong&gt; function has a critical modifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function mint(
    address attendee,
    uint256 campaignId,
    bytes32[] calldata merkleProof
) external whenNotPaused {
    if (msg.sender != relayer) revert NotAuthorizedRelayer();
    // ... all other logic
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only a trusted &lt;code&gt;relayer&lt;/code&gt; address (set in the constructor and updatable by the owner) can call &lt;code&gt;mint()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This design is powerful:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gasless Experience:&lt;/strong&gt; Attendees simply sign a message (or interact with a frontend). The relayer (a backend service) takes their proof, constructs the transaction, and pays the gas. For the user, the mint is free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Gate:&lt;/strong&gt; It serves as a backend-level defense. We can add API rate-limiting or other checks before the relayer even tries to submit the transaction, protecting the contract from DDoS or spam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof Validation:&lt;/strong&gt; The relayer's backend can pre-verify the Merkle proof before spending gas on a transaction that might fail, saving money and network congestion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Enforcing "Soulbound" with Clarity&lt;/strong&gt;&lt;br&gt;
A certificate of achievement is meaningless if it can be sold. These NFTs must be non-transferable.&lt;/p&gt;

&lt;p&gt;While ERC-4973 is a common standard, for this use case, a simple and clear override of the OpenZeppelin ERC721 &lt;code&gt;_update&lt;/code&gt; function is the most direct and reliable approach. &lt;code&gt;_update&lt;/code&gt; is the internal function called by &lt;code&gt;_transfer&lt;/code&gt;, &lt;code&gt;_mint&lt;/code&gt;, and &lt;code&gt;_burn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We only want to allow minting (where &lt;code&gt;from&lt;/code&gt; is &lt;code&gt;address(0)&lt;/code&gt;) and burning (where &lt;code&gt;to&lt;/code&gt; is &lt;code&gt;address(0)&lt;/code&gt;). Any other combination is a transfer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// @notice Soulbound and Metadata Logic
function _update(
    address to,
    uint256 tokenId,
    address auth
) internal override returns (address) {
    address from = _ownerOf(tokenId);

    // Revert if 'from' AND 'to' are not address(0).
    // This allows mints (from == 0) and burns (to == 0),
    // but blocks all transfers (from != 0 &amp;amp;&amp;amp; to != 0).
    if (from != address(0) &amp;amp;&amp;amp; to != address(0)) {
        revert NonTransferable();
    }

    return super._update(to, tokenId, auth);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is simple, clear, and effectively makes the tokens soulbound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Deterministic Metadata Linked to the Owner&lt;/strong&gt;&lt;br&gt;
This is a subtle but important piece of thoughtful design. Most NFTs link metadata to the &lt;code&gt;tokenId&lt;/code&gt;. But for this system, the certificate is personalized to the attendee.&lt;/p&gt;

&lt;p&gt;What if we needed to burn and re-issue a certificate? The &lt;code&gt;tokenId&lt;/code&gt; would change, but the attendee's address would not.&lt;/p&gt;

&lt;p&gt;Therefore, the &lt;code&gt;tokenURI&lt;/code&gt; function is designed to be deterministic based on the owner's address, not the &lt;code&gt;tokenId&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function tokenURI(uint256 tokenId)
    public view override returns (string memory)
{
    address ownerAddr = _ownerOf(tokenId);
    if (ownerAddr == address(0)) revert NonExistentToken();

    // 1. Find which campaign this token belongs to
    uint256 campaignId = tokenToCampaignId[tokenId];

    // 2. Get the specific baseURI for THAT campaign
    string memory baseURI = campaignBaseURI[campaignId];
    if (bytes(baseURI).length == 0) revert NonExistentToken();

    // 3. Convert owner's address to a string
    string memory addrStr = _toAsciiString(ownerAddr);

    // 4. Concatenate: baseURI + 0xaddress.json
    return string.concat(baseURI, addrStr, ".json");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The off-chain script (shown in the system flow) generates personalized JSON metadata named by the user's address (e.g., &lt;code&gt;0x....json&lt;/code&gt;). The contract's &lt;code&gt;tokenURI&lt;/code&gt; function simply rebuilds this path. This is a robust, reliable way to link personalized metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Full Smart Contract Code&lt;/strong&gt;&lt;br&gt;
For complete reference, here is the full implementation of &lt;code&gt;EventCertificate.sol&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

/// @title EventCertificate
/// @author Obinna Franklin Duru
/// @notice A reusable, pausable, and soulbound ERC721 certificate contract for multiple events.
/// @dev Manages minting "campaigns" with unique whitelists, timelines, and mint limits.
/// A trusted relayer facilitates gasless minting for whitelisted participants.
contract EventCertificate is ERC721, Ownable2Step, Pausable {
    // --- Custom Errors ---
    error NotAuthorizedRelayer();
    error AlreadyMinted();
    error InvalidProof();
    error NonTransferable();
    error NonExistentToken();
    error ZeroAddress();
    error CampaignNotActive();
    error CampaignDoesNotExist();
    error InvalidCampaignTimes();
    error CampaignMustStartInFuture();
    error EmptyMerkleRoot();
    error MintingWindowNotOpen();
    error ProofTooLong();
    error InvalidInput();
    error CannotModifyStartedCampaign();
    error MintLimitReached();
    error CampaignDurationTooLong();
    error CampaignExpired();
    error CampaignHasMints();

    // --- Constants ---
    uint256 private constant MAX_PROOF_DEPTH = 500;
    uint256 private constant MAX_CAMPAIGN_DURATION = 365 days;

    // --- Structs ---
    /// @notice Holds all parameters for a single minting event.
    struct MintingCampaign {
        bytes32 merkleRoot;
        uint256 startTime;
        uint256 endTime;
        uint256 maxMints;
        bool isActive;
    }

    // --- State Variables ---
    address public relayer;
    mapping(uint256 =&amp;gt; MintingCampaign) public campaigns;
    mapping(uint256 =&amp;gt; mapping(address =&amp;gt; bool)) public hasMintedInCampaign;
    mapping(uint256 =&amp;gt; uint256) public campaignMintCount;
    mapping(uint256 =&amp;gt; uint256) public tokenToCampaignId;
    mapping(uint256 =&amp;gt; string) public campaignBaseURI;
    uint256 private _nextTokenId = 1;
    uint256 private _nextCampaignId = 1;

    // --- Events ---
    event CertificateMinted(address indexed attendee, uint256 indexed tokenId, uint256 indexed campaignId);
    event CampaignCreated(
        uint256 indexed campaignId, bytes32 merkleRoot, uint256 startTime, uint256 endTime, uint256 maxMints
    );
    event CampaignUpdated(uint256 indexed campaignId, bytes32 newMerkleRoot, uint256 newStartTime, uint256 newEndTime);
    event CampaignActiveStatusChanged(uint256 indexed campaignId, bool isActive);
    event CampaignDeleted(uint256 indexed campaignId);
    event RelayerUpdated(address newRelayer);
    event CampaignBaseURIUpdated(uint256 indexed campaignId, string newBaseURI);

    // --- Constructor ---
    /// @notice Initializes the contract with core, immutable parameters.
    /// @param name_ The name of the ERC721 token collection.
    /// @param symbol_ The symbol of the ERC721 token collection.
    /// @param relayer_ The trusted address that will pay gas fees for minting.
    constructor(string memory name_, string memory symbol_, address relayer_)
        ERC721(name_, symbol_)
        Ownable(msg.sender)
    {
        if (bytes(name_).length == 0 || bytes(symbol_).length == 0) {
            revert InvalidInput();
        }
        if (relayer_ == address(0)) revert ZeroAddress();

        relayer = relayer_;
    }

    // --- Minting Function (Relayer Only) ---
    /// @notice Mints a soulbound certificate to an attendee for a specific campaign.
    /// @dev Checks campaign status, time windows, mint limits, and Merkle proof validity.
    /// @param attendee The address that will receive the certificate NFT.
    /// @param campaignId The ID of the campaign the user is minting for.
    /// @param merkleProof An array of bytes32 hashes forming the Merkle proof.
    function mint(address attendee, uint256 campaignId, bytes32[] calldata merkleProof) external whenNotPaused {
        if (attendee == address(0)) revert ZeroAddress();
        if (msg.sender != relayer) revert NotAuthorizedRelayer();
        if (merkleProof.length &amp;gt; MAX_PROOF_DEPTH) revert ProofTooLong();

        MintingCampaign storage campaign = campaigns[campaignId];

        if (campaign.merkleRoot == bytes32(0)) revert CampaignDoesNotExist();
        if (!campaign.isActive) revert CampaignNotActive();
        if (block.timestamp &amp;lt; campaign.startTime || block.timestamp &amp;gt; campaign.endTime) {
            revert MintingWindowNotOpen();
        }
        if (hasMintedInCampaign[campaignId][attendee]) revert AlreadyMinted();
        if (campaignMintCount[campaignId] &amp;gt;= campaign.maxMints) revert MintLimitReached();

        bytes32 leaf = keccak256(abi.encodePacked(attendee));
        if (!MerkleProof.verify(merkleProof, campaign.merkleRoot, leaf)) {
            revert InvalidProof();
        }

        uint256 tokenId = _nextTokenId;
        hasMintedInCampaign[campaignId][attendee] = true;
        campaignMintCount[campaignId]++;
        tokenToCampaignId[tokenId] = campaignId;

        unchecked {
            _nextTokenId++;
        }

        emit CertificateMinted(attendee, tokenId, campaignId);
        _safeMint(attendee, tokenId);
    }

    // --- Admin Functions ---

    /// @notice Creates a new minting campaign. Campaigns are inactive by default.
    /// @param merkleRoot The whitelist Merkle root.
    /// @param startTime The Unix timestamp for when minting begins.
    /// @param endTime The Unix timestamp for when minting ends.
    /// @param maxMints The maximum number of NFTs that can be minted for this campaign.
    function createCampaign(
        bytes32 merkleRoot,
        uint256 startTime,
        uint256 endTime,
        uint256 maxMints,
        string calldata baseURI_
    ) external onlyOwner {
        if (bytes(baseURI_).length == 0) revert InvalidInput();
        if (startTime &amp;lt; block.timestamp) revert CampaignMustStartInFuture();
        if (startTime &amp;gt;= endTime) revert InvalidCampaignTimes();
        if (endTime - startTime &amp;gt; MAX_CAMPAIGN_DURATION) revert CampaignDurationTooLong();
        if (merkleRoot == bytes32(0)) revert EmptyMerkleRoot();

        uint256 campaignId = _nextCampaignId;
        campaigns[campaignId] = MintingCampaign({
            merkleRoot: merkleRoot,
            startTime: startTime,
            endTime: endTime,
            maxMints: maxMints,
            isActive: false
        });

        campaignBaseURI[campaignId] = baseURI_;

        unchecked {
            _nextCampaignId++;
        }
        emit CampaignCreated(campaignId, merkleRoot, startTime, endTime, maxMints);
    }

    /// @notice Updates the parameters of a campaign BEFORE it has started.
    /// @param campaignId The ID of the campaign to update.
    /// @param newMerkleRoot The new whitelist Merkle root.
    /// @param newStartTime The new start time.
    /// @param newEndTime The new end time.
    function updateCampaignBeforeStart(
        uint256 campaignId,
        bytes32 newMerkleRoot,
        uint256 newStartTime,
        uint256 newEndTime
    ) external onlyOwner {
        MintingCampaign storage campaign = campaigns[campaignId];
        if (campaign.merkleRoot == bytes32(0)) revert CampaignDoesNotExist();
        if (block.timestamp &amp;gt;= campaign.startTime) revert CannotModifyStartedCampaign();

        if (newStartTime &amp;lt; block.timestamp) revert CampaignMustStartInFuture();
        if (newStartTime &amp;gt;= newEndTime) revert InvalidCampaignTimes();
        if (newEndTime - newStartTime &amp;gt; MAX_CAMPAIGN_DURATION) revert CampaignDurationTooLong();
        if (newMerkleRoot == bytes32(0)) revert EmptyMerkleRoot();

        campaign.merkleRoot = newMerkleRoot;
        campaign.startTime = newStartTime;
        campaign.endTime = newEndTime;

        emit CampaignUpdated(campaignId, newMerkleRoot, newStartTime, newEndTime);
    }

    /// @notice Deletes a campaign that was created by mistake.
    /// @dev Can only be called before the campaign starts, if it's inactive, and if no mints have occurred.
    /// @param campaignId The ID of the campaign to delete.
    function deleteCampaign(uint256 campaignId) external onlyOwner {
        MintingCampaign storage campaign = campaigns[campaignId];
        if (campaign.merkleRoot == bytes32(0)) revert CampaignDoesNotExist();
        if (campaign.isActive) revert CampaignNotActive(); // Must be inactive
        if (block.timestamp &amp;gt;= campaign.startTime) revert CannotModifyStartedCampaign();
        if (campaignMintCount[campaignId] &amp;gt; 0) revert CampaignHasMints(); // Cannot delete if mints exist

        delete campaigns[campaignId];
        emit CampaignDeleted(campaignId);
    }

    /// @notice Activates or deactivates a campaign.
    /// @param campaignId The ID of the campaign to modify.
    /// @param isActive The new active status.
    function setCampaignActiveStatus(uint256 campaignId, bool isActive) external onlyOwner {
        MintingCampaign storage campaign = campaigns[campaignId];
        if (campaign.merkleRoot == bytes32(0)) revert CampaignDoesNotExist();

        if (isActive) {
            // if (block.timestamp &amp;lt; campaign.startTime) revert MintingWindowNotOpen();
            if (block.timestamp &amp;gt; campaign.endTime) revert CampaignExpired();
        }

        campaign.isActive = isActive;
        emit CampaignActiveStatusChanged(campaignId, isActive);
    }

    /// @notice Allows the contract owner to burn (revoke) a certificate NFT.
    /// @param tokenId The token ID to burn.
    function burn(uint256 tokenId) external onlyOwner {
        address ownerAddr = _ownerOf(tokenId);
        if (ownerAddr == address(0)) revert NonExistentToken();

        uint256 campaignId = tokenToCampaignId[tokenId];
        if (campaignId == 0) revert NonExistentToken();

        // Mark user as eligible to re-mint if needed
        if (hasMintedInCampaign[campaignId][ownerAddr]) {
            hasMintedInCampaign[campaignId][ownerAddr] = false;
        }

        // Reduce mint count on campaign if needed
        if (campaignMintCount[campaignId] &amp;gt; 0) {
            campaignMintCount[campaignId]--;
        }

        delete tokenToCampaignId[tokenId];

        _burn(tokenId);
    }

    /// @notice Pauses all minting in an emergency.
    function pause() external onlyOwner {
        _pause();
    }

    /// @notice Resumes minting after a pause.
    function unpause() external onlyOwner {
        _unpause();
    }

    /// @notice Updates the trusted relayer address.
    /// @param newRelayer The address of the new relayer.
    function updateRelayer(address newRelayer) external onlyOwner {
        if (newRelayer == address(0)) revert ZeroAddress();
        relayer = newRelayer;
        emit RelayerUpdated(newRelayer);
    }

    /// @notice Updates the base URI for a campaign (metadata storage location).
    /// @dev Can be called at any time by owner, even after minting, to fix metadata issues.
    /// @param campaignId The campaign whose metadata URI should be updated.
    /// @param newBaseURI The new base URI pointing to updated metadata.
    function updateCampaignBaseURI(uint256 campaignId, string calldata newBaseURI) external onlyOwner {
        MintingCampaign storage campaign = campaigns[campaignId];
        if (campaign.merkleRoot == bytes32(0)) revert CampaignDoesNotExist();
        if (bytes(newBaseURI).length == 0) revert InvalidInput();

        campaignBaseURI[campaignId] = newBaseURI;
        emit CampaignBaseURIUpdated(campaignId, newBaseURI);
    }

    // --- View Functions ---

    /// @notice Gets all data for a specific campaign.
    /// @param campaignId The ID of the campaign.
    /// @return A MintingCampaign struct in memory.
    function getCampaign(uint256 campaignId) external view returns (MintingCampaign memory) {
        return campaigns[campaignId];
    }

    /// @notice Checks if a user meets the basic requirements to mint (does not check Merkle proof).
    /// @param attendee The address to check.
    /// @param campaignId The campaign to check against.
    /// @return A boolean indicating if the user meets the current criteria to mint.
    function canMint(address attendee, uint256 campaignId) external view returns (bool) {
        MintingCampaign storage campaign = campaigns[campaignId];
        if (!campaign.isActive || campaign.merkleRoot == bytes32(0)) return false;
        if (block.timestamp &amp;lt; campaign.startTime || block.timestamp &amp;gt; campaign.endTime) return false;
        if (hasMintedInCampaign[campaignId][attendee]) return false;
        if (campaignMintCount[campaignId] &amp;gt;= campaign.maxMints) return false;
        return true;
    }

    // --- Soulbound and Metadata Logic ---

    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        address from = _ownerOf(tokenId);
        if (from != address(0) &amp;amp;&amp;amp; to != address(0)) {
            revert NonTransferable();
        }
        return super._update(to, tokenId, auth);
    }

    /// @notice Returns the metadata URI for a given token.
    /// @param tokenId The ID of the token.
    /// @return The metadata URI string.
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        address ownerAddr = _ownerOf(tokenId);
        if (ownerAddr == address(0)) revert NonExistentToken();

        // 1. Find which campaign this token belongs to
        uint256 campaignId = tokenToCampaignId[tokenId];

        // 2. Get the specific baseURI for THAT campaign
        string memory baseURI = campaignBaseURI[campaignId];
        if (bytes(baseURI).length == 0) revert NonExistentToken();

        string memory addrStr = _toAsciiString(ownerAddr);
        return string.concat(baseURI, addrStr, ".json");
    }

    /// @dev Helper function to convert an address to its lowercase hex string representation.
    function _toAsciiString(address x) internal pure returns (string memory) {
        // Convert the address to a bytes32 value by first converting to uint160 and then to uint256.
        // This ensures the address is padded to 32 bytes with leading zeros.
        bytes32 value = bytes32(uint256(uint160(x)));

        // Define the hexadecimal characters for lookup.
        bytes memory alphabet = "0123456789abcdef";

        // Create a bytes array of length 42: 2 bytes for '0x' and 40 bytes for the 20-byte address (each byte becomes two hex characters).
        bytes memory str = new bytes(42);
        str[0] = "0";
        str[1] = "x";

        // Loop through each byte of the address (20 bytes).
        for (uint256 i = 0; i &amp;lt; 20; i++) {
            // Extract the byte at position i + 12 from the bytes32 value.
            // The address is stored in the last 20 bytes of the 32-byte value, so we start at index 12.
            // Get the high nibble (4 bits) of the byte by shifting right by 4 bits.
            // Convert to uint8 to use as an index in the alphabet.
            str[2 + i * 2] = alphabet[uint8(value[i + 12] &amp;gt;&amp;gt; 4)];

            // Get the low nibble (4 bits) of the byte by masking with 0x0F.
            // Convert to uint8 to use as an index in the alphabet.
            str[3 + i * 2] = alphabet[uint8(value[i + 12] &amp;amp; 0x0f)];
        }
        // Convert the bytes array to a string and return it.
        return string(str);
    }

    /// @notice Returns the next token ID that will be minted.
    /// @return The next token ID.
    function nextTokenId() external view returns (uint256) {
        return _nextTokenId;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Building for Trust&lt;/strong&gt;&lt;br&gt;
This contract is a reflection of my core philosophy: every line of code should communicate trust. By prioritizing a scalable multi-tenant architecture, layered security, and thoughtful design patterns like deterministic metadata, we create systems that are not just functional, but reliable and enduring.&lt;/p&gt;

&lt;p&gt;I hope this breakdown was useful. I'm always open to discussing onchain architecture and security.&lt;/p&gt;

&lt;p&gt;Let's build with clarity, purpose, and excellence.&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you found this article thoughtful and reliable, I'd appreciate a like or a comment.&lt;/p&gt;

&lt;p&gt;To see more of my work on secure and efficient on-chain systems, feel free to visit &lt;a href="https://binnadev.vercel.app" rel="noopener noreferrer"&gt;my portfolio&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>security</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Building for Trust: A Guide to Gasless Transactions with ERC-2771</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Thu, 13 Nov 2025 02:55:14 +0000</pubDate>
      <link>https://dev.to/binnadev/building-for-trust-a-guide-to-gasless-transactions-with-erc-2771-3ojf</link>
      <guid>https://dev.to/binnadev/building-for-trust-a-guide-to-gasless-transactions-with-erc-2771-3ojf</guid>
      <description>&lt;p&gt;How to use ERC-2771 and a Merkle Airdrop contract to build thoughtful, user-first dApps by separating the gas-payer from the transaction authorizer.&lt;/p&gt;

&lt;p&gt;As smart contract engineers, we build systems that handle real value. This demands a philosophy built on reliability, thoughtfulness, and excellence. Every line of code we write should communicate trust.&lt;/p&gt;

&lt;p&gt;But what happens when the very design of the network creates friction that breaks this trust?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Human Problem: Gas Fees&lt;/strong&gt;&lt;br&gt;
Imagine a common scenario: Alice is excited to claim her airdrop, a reward for being an early community member. She goes to the claim page, connects her wallet, but when she clicks "Claim", the transaction fails. The reason? She has no ETH in her wallet to pay for gas.&lt;/p&gt;

&lt;p&gt;This is a critical failure in user experience. We’ve presented a user with a "gift" they cannot open.&lt;/p&gt;

&lt;p&gt;As engineers, our first instinct might be to build a "relayer service." We could have a server, let's call it "Bob" that pays the gas on Alice's behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Technical Problem: &lt;code&gt;msg.sender&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
This "thoughtful" solution immediately hits a technical wall: &lt;strong&gt;authorization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a standard transaction, the &lt;code&gt;msg.sender&lt;/code&gt; (the address that initiated the call and is paying the gas) is the ultimate source of truth for authorization.&lt;/p&gt;

&lt;p&gt;If Bob (the relayer) calls the &lt;code&gt;claim()&lt;/code&gt; function and pays the gas, the contract sees: &lt;code&gt;msg.sender = Bob.address&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The contract now believes Bob is the one claiming the airdrop, not Alice. This breaks the entire authorization model.&lt;/p&gt;

&lt;p&gt;How do we build a reliable system that separates "who is paying for gas" from "who is authorizing the action"?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Principled Solution: ERC-2771&lt;/strong&gt;&lt;br&gt;
The community solved this through a standard: &lt;strong&gt;ERC-2771&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ERC-2771 is a standard for "meta-transactions" that provides a trusted, on-chain path to solve this very problem. It introduces a special "middleman" contract called a &lt;strong&gt;Trusted Forwarder&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of a custom, off-chain solution, we use a clear, on-chain protocol. The new flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alice (No ETH)&lt;/strong&gt;: Creates a signed message containing her desired transaction (e.g., "I, Alice, want to call &lt;code&gt;claim()&lt;/code&gt;").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relayer (Bob)&lt;/strong&gt;: Receives this signed message from Alice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relayer (Bob)&lt;/strong&gt;: Wraps Alice's message in a new transaction and sends it to the &lt;strong&gt;Trusted Forwarder&lt;/strong&gt;, paying the gas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusted Forwarder&lt;/strong&gt;: Verifies Alice's signature is valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusted Forwarder&lt;/strong&gt;: Calls our &lt;code&gt;Airdrop&lt;/code&gt; contract, appending Alice's original address (&lt;code&gt;Alice.address&lt;/code&gt;) to the end of the &lt;code&gt;calldata&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airdrop Contract&lt;/strong&gt;: Receives the call from the &lt;strong&gt;Trusted Forwarder&lt;/strong&gt;. Because it's a trusted contract, it knows to look at the end of the &lt;code&gt;calldata&lt;/code&gt; to find the true authorizer (Alice).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the core of reliable, gasless design. Our contract's logic is no longer concerned with &lt;code&gt;msg.sender&lt;/code&gt; (the gas payer) but with the true authorizer of the request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Practical Example: A Secure Merkle Airdrop&lt;/strong&gt;&lt;br&gt;
To demonstrate this, I built a &lt;code&gt;MerkleAirdrop&lt;/code&gt; contract. The design goals were reliability, gas efficiency, and security with native support for gasless claims.&lt;/p&gt;

&lt;p&gt;Here is the full contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IAirdrop} from "./interfaces/IAirdrop.sol";

/**
 * @title MerkleAirdrop
 * @author BinnaDev
 * @notice A modular, gas-efficient, and secure contract for airdrops
 * verified by Merkle proofs.
 * @dev This contract implements the `IAirdrop` interface, uses OpenZeppelin's
 * `BitMaps` for gas-efficient claim tracking, `ReentrancyGuard` for security,
 * and `ERC2771Context` to natively support gasless claims via a trusted
 * forwarder. The Merkle root is immutable, set at deployment for
 * maximum trust.
 */
contract MerkleAirdrop is IAirdrop, ERC2771Context, ReentrancyGuard {
    using BitMaps for BitMaps.BitMap;

    /**
     * @notice The MerMonitor's root of the Merkle tree containing all allocations.
     * @dev This is immutable, meaning it can only be set once at deployment.
     * This is a critical security feature to build trust with users,
     * as the rules of the airdrop can never change.
     */
    bytes32 public immutable MERKLE_ROOT;

    /**
     * @notice The maximum allowed depth for a Merkle proof.
     * @dev This is a security measure to prevent gas-griefing (DOS) attacks
     * where an attacker might submit an excessively long (but valid) proof.
     * 32 is a safe and generous default.
     */
    uint256 public constant MAX_PROOF_DEPTH = 32;

    /**
     * @notice The bitmap storage that tracks all claimed indices.
     * @dev We use the `BitMaps` library (composition) rather than inheriting
     * (inheritance) for better modularity and clarity.
     */
    BitMaps.BitMap internal claimedBitmap;

    /**
     * @notice Initializes the contract with the airdrop's Merkle root
     * and the trusted forwarder for gasless transactions.
     * @param merkleRoot The `bytes32` root of the Merkle tree.
     * @param trustedForwarder The address of the ERC-2771 trusted forwarder.
     * Pass `address(0)` if gasless support is not needed.
     */
    constructor(bytes32 merkleRoot, address trustedForwarder) ERC2771Context(trustedForwarder) {
        MERKLE_ROOT = merkleRoot;
    }

    /**
     * @notice Claims an airdrop allocation by providing a valid Merkle proof.
     * @dev This function follows the Checks-Effects-Interactions pattern.
     * It uses `_msgSender()` to support both direct calls and gasless claims.
     * @param index The unique claim index for this user (from the Merkle tree data).
     * @param claimant The address that is eligible for the claim.
     * @param tokenContract The address of the ERC20 or ERC721 token.
     * @param tokenId The ID of the token (for ERC721); must be 0 for ERC20.
     * @param amount The amount of tokens (for ERC20); typically 1 for ERC721.
     * @param proof The Merkle proof (`bytes32[]`) showing the leaf is in the tree.
     */
    function claim(
        uint256 index,
        address claimant,
        address tokenContract,
        uint256 tokenId,
        uint256 amount,
        bytes32[] calldata proof
    ) external nonReentrant {
        // --- CHECKS ---

        // 1. Check if this index is already claimed (Bitmap check).
        // This is the cheapest check and should come first.
        if (claimedBitmap.get(index)) {
            revert MerkleAirdrop_AlreadyClaimed(index);
        }

        // 2. Check for proof length (Gas-griefing DOS protection).
        if (proof.length &amp;gt; MAX_PROOF_DEPTH) {
            revert MerkleAirdrop_ProofTooLong(proof.length, MAX_PROOF_DEPTH);
        }

        // 3. Check that the sender is the rightful claimant.
        // We use `_msgSender()` to transparently support ERC-2771.
        address sender = _msgSender();
        if (claimant != sender) {
            revert MerkleAirdrop_NotClaimant(claimant, sender);
        }

        // 4. Reconstruct the leaf on-chain.
        // This is a critical security step. We NEVER trust the client
        // to provide the leaf hash directly.
        bytes32 leaf = _hashLeaf(index, claimant, tokenContract, tokenId, amount);

        // 5. Verify the proof (Most expensive check).
        if (!MerkleProof.verify(proof, MERKLE_ROOT, leaf)) {
            revert MerkleAirdrop_InvalidProof();
        }

        // --- EFFECTS ---

        // 6. Mark the index as claimed *before* the interaction.
        // This satisfies the Checks-Effects-Interactions pattern and
        // mitigates reentrancy risk.
        claimedBitmap.set(index);

        // --- INTERACTIONS ---

        // 7. Dispatch the token.
        _dispatchToken(tokenContract, claimant, tokenId, amount);

        // 8. Emit the standardized event.
        emit Claimed("Merkle", index, claimant, tokenContract, tokenId, amount);
    }

    /**
     * @notice Public view function to check if an index has been claimed.
     * @param index The index to check.
     * @return bool True if the index is claimed, false otherwise.
     */
    function isClaimed(uint256 index) public view returns (bool) {
        return claimedBitmap.get(index);
    }

    /**
     * @notice Internal function to hash the leaf data.
     * @dev Must match the exact hashing scheme used in the off-chain
     * generator script. We use a double-hash (H(H(data))) pattern
     * with `abi.encode` for maximum security and standardization.
     * `abi.encode` is safer than `abi.encodePacked` as it pads elements.
     */
    function _hashLeaf(uint256 index, address claimant, address tokenContract, uint256 tokenId, uint256 amount)
        internal
        pure
        returns (bytes32)
    {
        // First hash: abi.encode() is safer than abi.encodePacked()
        // as it pads all elements, preventing ambiguity.
        bytes32 innerHash = keccak256(abi.encode(index, claimant, tokenContract, tokenId, amount));

        // Second hash: This is a standard pattern to ensure all leaves
        // are a uniform hash-of-a-hash.
        return keccak256(abi.encode(innerHash));
    }

    /**
     * @notice Internal function to dispatch the tokens (ERC20 or ERC721).
     * @dev Assumes this contract holds the full supply of airdrop tokens.
     */
    function _dispatchToken(address tokenContract, address to, uint256 tokenId, uint256 amount) internal {
        if (tokenId == 0) {
            // This is an ERC20 transfer.
            if (amount == 0) revert Airdrop_InvalidAllocation();
            bool success = IERC20(tokenContract).transfer(to, amount);
            if (!success) revert Airdrop_TransferFailed();
        } else {
            // This is an ERC721 transfer.
            // The `amount` parameter is ignored (implicitly 1).
            // `safeTransferFrom` is used for security, and our `nonReentrant`
            // guard on `claim()` protects against reentrancy attacks.
            IERC721(tokenContract).safeTransferFrom(address(this), to, tokenId);
        }
    }

    /**
     * @dev Overrides the `_msgSender()` from `ERC2771Context` to enable
     * meta-transactions. This is the heart of our gasless support.
     *
     * Why do this? Because we're also inheriting from other contracts (ReentrancyGuard, IAirdrop) and Solidity requires you to explicitly choose which parent's `_msgSender()` to use when there's potential ambiguity.
     */
    function _msgSender() internal view override(ERC2771Context) returns (address) {
        return ERC2771Context._msgSender();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dissecting the Thoughtful Design&lt;/strong&gt;&lt;br&gt;
This contract is more than just a piece of code; it's a system designed for trust. Let's look at the key decisions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enabling Gasless Claims&lt;/strong&gt;
Enabling ERC-2771 is a deliberate choice made at deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A. &lt;strong&gt;Inheritance&lt;/strong&gt;: We inherit from OpenZeppelin's &lt;code&gt;ERC2771Context&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract MerkleAirdrop is IAirdrop, ERC2771Context, ReentrancyGuard {...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;B. &lt;strong&gt;The Constructor&lt;/strong&gt;: We tell the contract the address of the one-and-only &lt;code&gt;trustedForwarder&lt;/code&gt; it will ever listen to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;constructor(
    bytes32 merkleRoot,
    address trustedForwarder
) ERC2771Context(trustedForwarder) {
    MERKLE_ROOT = merkleRoot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Core: &lt;code&gt;_msgSender()&lt;/code&gt; vs. &lt;code&gt;msg.sender&lt;/code&gt;&lt;/strong&gt;
This is where the magic happens. The &lt;code&gt;ERC2771Context&lt;/code&gt; contract provides a function: &lt;code&gt;_msgSender()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a simplified view of what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It checks, "Is the &lt;code&gt;msg.sender&lt;/code&gt; (the gas payer) my &lt;code&gt;trustedForwarder&lt;/code&gt;?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If YES&lt;/strong&gt;: It knows this is a meta-transaction. It reads the real sender's address from the end of the &lt;code&gt;calldata&lt;/code&gt; and returns it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If NO&lt;/strong&gt;: It knows this is a normal transaction. It simply returns the &lt;code&gt;msg.sender&lt;/code&gt; as usual.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This provides a single, reliable function that transparently handles both gasless calls and regular calls.&lt;/p&gt;

&lt;p&gt;Look at our &lt;code&gt;claim&lt;/code&gt; function's authorization check. It doesn't use &lt;code&gt;msg.sender&lt;/code&gt;. It uses &lt;code&gt;_msgSender()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 3. (Authorization) Check that the sender is the rightful claimant.
// This is the core of our ERC-2771 integration.
address sender = _msgSender();
if (claimant != sender) {
    revert MerkleAirdrop_NotClaimant(claimant, sender);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If Alice calls &lt;code&gt;claim()&lt;/code&gt; directly and pays gas:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_msgSender()&lt;/code&gt; returns &lt;code&gt;Alice.address&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The check passes.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;If Bob the Relayer calls via the Trusted Forwarder:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_msgSender()&lt;/code&gt; returns &lt;code&gt;Alice.address&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The check passes.
We have successfully separated the gas payer from the authorizer.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Note on the &lt;code&gt;override&lt;/code&gt;&lt;/strong&gt;
You'll notice this specific function at the end of the contract:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _msgSender() internal view override(Context, ERC2771Context) returns (address) {
    return ERC2771Context._msgSender();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may look redundant, but it's essential for clarity and correctness in Solidity.&lt;/p&gt;

&lt;p&gt;Our contract inherits from both &lt;code&gt;ERC2771Context&lt;/code&gt; and &lt;code&gt;ReentrancyGuard&lt;/code&gt;. &lt;code&gt;ReentrancyGuard&lt;/code&gt; also has a dependency on &lt;code&gt;_msgSender()&lt;/code&gt; (via the Context contract). Solidity sees this "ambiguity" (which &lt;code&gt;_msgSender&lt;/code&gt; should I use?) and requires us to be explicit.&lt;/p&gt;

&lt;p&gt;Here, we are being deliberate: "When I call &lt;code&gt;_msgSender()&lt;/code&gt;, I am explicitly stating that I want to use the version provided by &lt;code&gt;ERC2771Context&lt;/code&gt;." It's a hallmark of excellent, precise engineering.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Beyond Gasless: Other Pillars of Trust&lt;/strong&gt;
A reliable contract is secure in all aspects. Thoughtful design extends beyond one feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks-Effects-Interactions&lt;/strong&gt;: We mark the claim as used (&lt;code&gt;claimedBitmap.set(index)&lt;/code&gt;) before we make the external call (&lt;code&gt;_dispatchToken&lt;/code&gt;). This is a fundamental pattern to prevent reentrancy attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas-Griefing Protection&lt;/strong&gt;: We enforce a &lt;code&gt;MAX_PROOF_DEPTH&lt;/code&gt;. This prevents an attacker from sending a valid but excessively long proof designed to waste gas and block others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-Chain Hashing&lt;/strong&gt;: We never trust the client to provide a leaf hash. We reconstruct it on-chain (&lt;code&gt;_hashLeaf(...)&lt;/code&gt;) to ensure the proof is for the exact data we expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe Hashing&lt;/strong&gt;: We use &lt;code&gt;abi.encode&lt;/code&gt; instead of &lt;code&gt;abi.encodePacked&lt;/code&gt; in our hashing function. &lt;code&gt;encodePacked&lt;/code&gt; can be ambiguous and lead to collisions; &lt;code&gt;abi.encode&lt;/code&gt; is safer. This is a small, thoughtful choice that enhances reliability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Engineering with Purpose&lt;/strong&gt;&lt;br&gt;
ERC-2771 is more than a technical standard; it's a design philosophy. It empowers us to build thoughtful, human-centered applications that remove the greatest point of friction in Web3: the gas fee.&lt;/p&gt;

&lt;p&gt;By combining this standard with other principles of secure and reliable design, we can build systems that users can truly trust.&lt;/p&gt;

&lt;p&gt;Let's build something you can trust with clarity, purpose, and excellence.&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you found this article thoughtful and reliable, I'd appreciate a like or a comment.&lt;/p&gt;

&lt;p&gt;You can find the full GitHub repo for this project here: &lt;a href="https://github.com/obinnafranklinduru/tailored-airdrop/blob/main/src/MerkleAirdrop.sol" rel="noopener noreferrer"&gt;https://github.com/obinnafranklinduru/tailored-airdrop/blob/main/src/MerkleAirdrop.sol&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To see more of my work on secure and efficient on-chain systems, feel free to visit my portfolio at &lt;a href="https://binnadev.vercel.app" rel="noopener noreferrer"&gt;https://binnadev.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>solidity</category>
      <category>smartcontract</category>
      <category>web3</category>
    </item>
    <item>
      <title>Why I Rebuilt My Developer Portfolio Around Three Core Values (Reliability, Thoughtfulness, and Excellence)</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Wed, 05 Nov 2025 17:41:49 +0000</pubDate>
      <link>https://dev.to/binnadev/why-i-rebuilt-my-developer-portfolio-around-three-core-values-reliability-thoughtfulness-and-3ncm</link>
      <guid>https://dev.to/binnadev/why-i-rebuilt-my-developer-portfolio-around-three-core-values-reliability-thoughtfulness-and-3ncm</guid>
      <description>&lt;p&gt;&lt;em&gt;A reflection on designing a portfolio that practices the same principles as the code I write.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the past few days, I've been "vibe coding" a new portfolio. But I didn't just want to build a resume; I wanted to build a digital extension of my core philosophy.&lt;/p&gt;

&lt;p&gt;As a &lt;strong&gt;Smart Contract Engineer&lt;/strong&gt;, I work with systems that handle real value. When you're building systems that handle real value, trust is everything. That's why I've centered my entire professional brand on three core values: &lt;strong&gt;Reliability&lt;/strong&gt;, &lt;strong&gt;Thoughtfulness&lt;/strong&gt;, and &lt;strong&gt;Excellence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It felt hypocritical to have a brand built on these values if my own website didn't live up to them. So, I decided to build a new digital home from scratch, using those same three values as my guide.&lt;/p&gt;




&lt;h2&gt;
  
  
  🟣 Reliable: A "Living" Portfolio
&lt;/h2&gt;

&lt;p&gt;My old site was a chore to update. This new one is &lt;strong&gt;reliably current&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My &lt;code&gt;/work&lt;/code&gt; page is now a live feed from the &lt;strong&gt;GitHub API&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;My &lt;code&gt;/writing&lt;/code&gt; page pulls directly from my &lt;strong&gt;Dev.to API&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If I push new code or publish a new post, my site updates automatically. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No manual steps-just a single, reliable source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  🟡 Thoughtful: A Human-Centered Experience
&lt;/h2&gt;

&lt;p&gt;A "thoughtful" site has to work for everyone. I obsessed over the details to make it human-centered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's built to be &lt;strong&gt;accessible&lt;/strong&gt;, with clean typography and proper color contrast.&lt;/li&gt;
&lt;li&gt;The design is &lt;strong&gt;minimal&lt;/strong&gt;, &lt;strong&gt;elegant&lt;/strong&gt;, and &lt;strong&gt;precise&lt;/strong&gt;, with no clutter to get in the way.&lt;/li&gt;
&lt;li&gt;I also implemented a complete &lt;strong&gt;JSON-LD schema&lt;/strong&gt; - a technical way of giving search engines a clear, structured map of who I am, making the site more discoverable and "thoughtful" for all.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔵 Excellent: The Polish and Performance
&lt;/h2&gt;

&lt;p&gt;Excellence is in the craft.&lt;br&gt;
This isn't a template, it's a hand-built site using &lt;strong&gt;Next.js 14&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and &lt;strong&gt;Server Components&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It scores &lt;strong&gt;95+ on Lighthouse&lt;/strong&gt; for performance and accessibility, and every interactive element from buttons to project cards has its own subtle, deliberate motion.&lt;/p&gt;

&lt;p&gt;It feels fast, balanced, and complete.&lt;/p&gt;




&lt;p&gt;This project was both a &lt;em&gt;vibe&lt;/em&gt; and a &lt;em&gt;statement&lt;/em&gt;.&lt;br&gt;
It's more than a portfolio, it's my new digital home.&lt;/p&gt;

&lt;p&gt;Let me know what you think.&lt;br&gt;
Let's build something you can trust 🤝.&lt;/p&gt;

&lt;p&gt;👉 Check it out: &lt;a href="https://binnadev.vercel.app" rel="noopener noreferrer"&gt;https://binnadev.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>portfolio</category>
      <category>blockchain</category>
      <category>smartcontract</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
