<?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>Forem: Obinna Duru</title>
    <description>The latest articles on Forem by Obinna Duru (@binnadev).</description>
    <link>https://forem.com/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>Forem: Obinna Duru</title>
      <link>https://forem.com/binnadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/binnadev"/>
    <language>en</language>
    <item>
      <title>The Brief for the Judge: Writing Audit-Ready Documentation</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Tue, 28 Apr 2026 17:00:00 +0000</pubDate>
      <link>https://forem.com/binnadev/the-brief-for-the-judge-writing-audit-ready-documentation-h4n</link>
      <guid>https://forem.com/binnadev/the-brief-for-the-judge-writing-audit-ready-documentation-h4n</guid>
      <description>&lt;p&gt;We have spent the last four posts building &lt;strong&gt;&lt;a href="https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393" rel="noopener noreferrer"&gt;MilestoneCrowdfundUpgradeable protocol&lt;/a&gt;&lt;/strong&gt;. We architected an upgradeable proxy, designed a mathematically rigorous escrow engine, fuzz-tested its invariants, and locked the doors against reentrancy attacks. We built a fortress.&lt;/p&gt;

&lt;p&gt;But there is one final step. A secure fortress is incredibly dangerous if you don't leave behind a blueprint explaining exactly how the traps and locks are supposed to work.&lt;/p&gt;

&lt;p&gt;Most developers hate writing documentation. In Web2, bad documentation means a new developer joins your team and spends three days figuring out an API endpoint instead of one hour. Frustrating, expensive, recoverable.&lt;/p&gt;

&lt;p&gt;In Web3, bad documentation is catastrophic. When you hand your code to a security auditor, bad documentation means they might misunderstand the intended behavior of a function. They might classify a deliberate design choice as a vulnerability, forcing you to waste weeks fixing a non-issue. Worse, they might see a real bug, assume it was your intended logic, and let it pass into production where user funds get drained.&lt;/p&gt;

&lt;p&gt;In this final post, I want to share exactly how I approached documenting &lt;strong&gt;MilestoneCrowdfundUpgradeable&lt;/strong&gt; from NatSpec comments to architectural READMEs and why I believe documentation is the most underrated security tool in Web3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mindset: Preparing the Brief&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I prepare a protocol for an audit, I think of the documentation as the brief I am handing to a judge before a trial.&lt;/p&gt;

&lt;p&gt;The auditor is the judge. The code is the evidence. But without the brief, without the written explanation of intent, the documented invariants, and the explicit threat model, the judge is left to interpret the evidence alone. And interpretation without intent is where dangerous misreadings happen.&lt;/p&gt;

&lt;p&gt;But here is the deeper truth: documentation isn't just for auditors. It is for the version of you that comes back to this codebase in six months after working on three other projects. That person has forgotten everything. The comments and the threat model are the only things standing between future-you and a catastrophic misunderstanding of your own protocol.&lt;/p&gt;

&lt;p&gt;My rule is simple: &lt;strong&gt;If I have to think for more than five seconds about what a piece of code is doing, that piece of code needs a comment.&lt;/strong&gt; Not because the code is bad, but because the next person reading it shouldn't have to spend those same five seconds. In a paid audit, those five seconds across a thousand lines become expensive hours of confusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NatSpec: Before, During, and After&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Solidity uses a comment format called NatSpec (Ethereum Natural Language Specification). My approach to writing it happens in three distinct phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Before (The Specification)&lt;/strong&gt;&lt;br&gt;
I write the &lt;code&gt;@notice&lt;/code&gt; and &lt;code&gt;@param&lt;/code&gt; descriptions before I write the function body. This forces me to articulate what the function is supposed to do in plain English before I write a single line of logic. If I cannot describe it clearly in one sentence, I do not understand it well enough to code it yet. The comment becomes the specification; the code is just the implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. During (The "Why")&lt;/strong&gt;&lt;br&gt;
While writing the body, I add inline comments at critical decision points. In the last post, we looked at this exact code block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// CEI: Effects - Update the ledger first, before money moves!
_pledges[_id][msg.sender] = 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any competent developer can read &lt;code&gt;_pledges[_id][msg.sender] = 0&lt;/code&gt; and know what it does. What they cannot know without a comment is that this line is here specifically to prevent a reentrancy attack. My personal rule: &lt;strong&gt;The code says "what". Only a comment can say "why".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. After (The Edge Cases)&lt;/strong&gt;&lt;br&gt;
After the function is complete, I write the &lt;code&gt;@dev&lt;/code&gt; block. This is where I document the non-obvious: the edge cases I considered and rejected, the invariants this function must maintain, and the assumptions that must be true for it to be safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Power of Visual State Machines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A smart contract is a state machine. It has states (&lt;code&gt;Fundraising&lt;/code&gt;, &lt;code&gt;Succeeded&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt;, &lt;code&gt;Abandoned&lt;/code&gt;) and transitions between those states (&lt;code&gt;finalize&lt;/code&gt;, &lt;code&gt;haltCampaign&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;You can write all of that in prose, but the human brain does not reason about state machines in paragraphs. It reasons about them visually. The moment I drew the state diagram for &lt;code&gt;MilestoneCrowdfundUpgradeable&lt;/code&gt; using PlantUML, a massive gap became visible.&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%2Fmb6ai4uwwr28ekv3jbsv.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%2Fmb6ai4uwwr28ekv3jbsv.png" alt="Campaign Lifecycle Diagram" width="507" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at the diagram, I immediately noticed there was no direct transition from &lt;code&gt;Fundraising&lt;/code&gt; to &lt;code&gt;Abandoned&lt;/code&gt;. A campaign could only be Abandoned after it Succeeded. That was a correct design decision, you shouldn't be able to abandon a project that never even got funded but seeing it drawn forced me to go back and verify that the Solidity code actually enforced this explicitly. The diagram asked a question the code never explicitly answered.&lt;/p&gt;

&lt;p&gt;Diagrams also communicate to people who cannot read Solidity. Your investors, your community, or a tokenomics specialist auditing your math might struggle with the raw code, but they can instantly follow a visual map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The README Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of Web3 repositories just leave the default Foundry or Hardhat README file. Alternatively, they throw every piece of information into one giant, 3,000-word file.&lt;/p&gt;

&lt;p&gt;One giant file is a red flag. It tells an auditor that the author did not think carefully about who would be reading it. I split the &lt;code&gt;MilestoneCrowdfundUpgradeable&lt;/code&gt; documentation into specific hubs tailored to specific readers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol_Architecture.md:&lt;/strong&gt; Written for the &lt;strong&gt;Auditor&lt;/strong&gt;. Their primary question is: What is this protocol supposed to do, and what rules must it never break? This document answers that immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration_Guide.md:&lt;/strong&gt; Written for the &lt;strong&gt;Frontend Developer&lt;/strong&gt;. Their primary question is: How do I call these functions correctly without breaking anything? They need the function signatures and error codes, not the threat model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident_Response.md:&lt;/strong&gt; Written for the &lt;strong&gt;Security Researcher&lt;/strong&gt;. If they find a live vulnerability, their primary question is: Who has the authority to pause this, and how do I reach them right now? If they have to dig through installation instructions to find your emergency contact, you are wasting critical seconds during a hack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threat_Model.md:&lt;/strong&gt; Written for the &lt;strong&gt;Skeptical Reviewer&lt;/strong&gt;. This is the document most developers skip. It says: Here are the attacks we considered, here is why we are protected, and here are the residual risks we knowingly accepted. You cannot write a threat model without thinking like an attacker, and you cannot think like an attacker without finding things you missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A well-structured repository is a signal of engineering maturity. Before an auditor reads a single line of your Solidity, they have already formed a judgment about how seriously you take correctness based on your file structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hardest Part: Combating Drift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The hardest part is not writing documentation. It is keeping it true.&lt;/p&gt;

&lt;p&gt;Code changes. Documentation does not change automatically. The gap between them is one of the most dangerous states a codebase can be in.&lt;/p&gt;

&lt;p&gt;I experienced this directly. Early in the design of the fiat bridge, &lt;code&gt;pledgeFiatOnBehalf&lt;/code&gt; was intended to apply a reduced platform fee. The NatSpec reflected that. Later, the stakeholder and I decided fiat pledges would be completely fee-exempt on-chain, with fees handled off-chain by Stripe.&lt;/p&gt;

&lt;p&gt;I changed the code. I didn't immediately update the comment.&lt;/p&gt;

&lt;p&gt;I caught it during a final review pass, but imagine if I hadn't. An auditor reading that comment would have expected fee logic that didn't exist, flagged its absence as a high-severity bug, and forced us to waste days in remediation meetings arguing over a phantom issue.&lt;/p&gt;

&lt;p&gt;The discipline I developed from that experience is strict: &lt;strong&gt;Every time you change a function body, you update the NatSpec before you close the file.&lt;/strong&gt; Not before you commit. Immediately. The comment is a structural part of the function, not a separate chore to do later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BinnaDev Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a junior developer tells me, "My code is clean, it's self-documenting, I don't need to write a README," my response is always the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Show me the part of your code that explains why you made that design decision."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clean code tells you &lt;em&gt;what&lt;/em&gt;. Only documentation tells you &lt;em&gt;why&lt;/em&gt;. And in smart contract engineering, the why is where all the security lives. "Self-documenting code" makes sense in Web2 where the worst-case scenario is a confused colleague. It does not make sense in Web3 where the worst-case scenario is a misunderstood intent that costs your users everything they trusted you with.&lt;/p&gt;

&lt;p&gt;Here is the practical test: If a professional auditor had to judge whether a specific line in your contract was an intentional design choice or a bug, would your code give them enough information to judge correctly without asking you a single question? If the answer is no, you need documentation.&lt;/p&gt;

&lt;p&gt;And I will be completely honest with you on how I execute this practically.&lt;/p&gt;

&lt;p&gt;English is not my first language. Documentation is fundamentally an asynchronous communication tool, you are writing for a stranger who cannot ask you a follow-up question, they have to wait for your response. In this context, a grammatical error doesn't just look careless; it creates genuine ambiguity. Did the developer mean this or that? To solve this, &lt;strong&gt;I use AI.&lt;/strong&gt; Agile engineering tells us to prioritize working software over comprehensive documentation. That doesn't mean skip the docs; it means be smart about where your mental energy goes. The architecture, the invariant math, the security decisions, those required 100% of my focus. I used AI to help draft NatSpec suggestions and catch grammatical ambiguity so my thinking wasn't limited by the mechanics of expressing it in a second language. The ideas are mine entirely. The precision of communicating them is a collaboration.&lt;/p&gt;

&lt;p&gt;Your code being clean means you are a careful developer. Your protocol being documented means you respect the people who will put their money into what you built. Both are required. You have the tools. Use them.&lt;/p&gt;

&lt;p&gt;This concludes our five-part journey building the &lt;strong&gt;&lt;a href="https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393" rel="noopener noreferrer"&gt;MilestoneCrowdfundUpgradeable protocol&lt;/a&gt;&lt;/strong&gt;. From upgradeable proxies to defensive math, stateful fuzzing, reentrancy guards, and audit-ready documentation.&lt;/p&gt;

&lt;p&gt;I hope this series has given you a real-world look at what security-first Web3 engineering actually looks like. Keep building, stay paranoid, and write excellent code.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>ai</category>
      <category>blockchain</category>
      <category>smartcontract</category>
    </item>
    <item>
      <title>Thinking Like an Attacker: The Airbags and Seatbelts of Smart Contract Security</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Tue, 28 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://forem.com/binnadev/thinking-like-an-attacker-the-airbags-and-seatbelts-of-smart-contract-security-4g4a</link>
      <guid>https://forem.com/binnadev/thinking-like-an-attacker-the-airbags-and-seatbelts-of-smart-contract-security-4g4a</guid>
      <description>&lt;p&gt;In our last post, we built a mathematical proving ground using &lt;a href="https://www.getfoundry.sh" rel="noopener noreferrer"&gt;Foundry&lt;/a&gt;. We used stateful fuzzing to prove that the rules of our &lt;strong&gt;&lt;a href="https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393" rel="noopener noreferrer"&gt;MilestoneCrowdfundUpgradeable protocol&lt;/a&gt;&lt;/strong&gt; work exactly as intended.&lt;/p&gt;

&lt;p&gt;But testing only proves that the contract behaves correctly when people follow the rules. What happens when someone actively tries to break them?&lt;/p&gt;

&lt;p&gt;In Web2, when you think about security, you think about the perimeter. Who can get in? You build firewalls, you require authentication, you set up rate limiting. The attacker is outside the system, trying to break down the door.&lt;/p&gt;

&lt;p&gt;In Web3, there is no perimeter. Your contract is public. The state is public. Every single function is readable by anyone on earth the moment you deploy it. The attacker is not trying to get past a wall, they are standing inside the room with you, reading your rulebook, looking for a sentence that contradicts itself.&lt;/p&gt;

&lt;p&gt;So, my security-first mindset when I sit down to write Solidity is this: &lt;strong&gt;I am writing rules for a system that a brilliant, motivated, financially incentivized adversary will study longer and harder than I wrote it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post, I want to show you exactly how I design against those adversaries. We are going to look at the most infamous hack in Web3 history, how to prevent it using the golden rule of smart contracts, and the real-world edge cases I had to actively design around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Refund Kiosk Glitch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To understand the most dangerous exploit in smart contracts, you don't need to understand code yet. You need to understand the refund kiosk glitch.&lt;/p&gt;

&lt;p&gt;Imagine a store installs a new, automated self-service refund kiosk. It works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You scan your receipt.&lt;/li&gt;
&lt;li&gt;The machine dispenses your cash.&lt;/li&gt;
&lt;li&gt;The machine marks your receipt as "refunded" in the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A clever person notices something. Between step 2 and step 3, there is a processing gap: a brief moment while the database updates.&lt;/p&gt;

&lt;p&gt;So, the attacker builds a device and physically attaches it to the kiosk's cash dispenser slot. When cash drops into the tray, the weight of the notes triggers a pressure sensor inside the device, which automatically scans the receipt again &lt;em&gt;immediately&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The attacker does not press any buttons. The act of receiving cash is itself the trigger for the next scan.&lt;/p&gt;

&lt;p&gt;The kiosk checks the database: &lt;em&gt;"Is this receipt already refunded?"&lt;/em&gt; The database still says no, because step 3 hasn't happened yet. So the kiosk dispenses again. Cash drops. The pressure sensor fires. The receipt scans again. The database still says no. It dispenses again.&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%2F9nvrx9sykb3a5nzkado1.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%2F9nvrx9sykb3a5nzkado1.png" alt="Attack Scenario: Refund Glitch Exploit"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This loop continues until the machine is completely empty. Nobody held anyone at gunpoint. The machine was following its own rules perfectly, it checked the ledger every single time before it paid. It just checked a ledger that was always one step behind reality.&lt;/p&gt;

&lt;p&gt;The fix is incredibly simple. You just change the order of operations at the kiosk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan your receipt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mark it as refunded in the database immediately.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Now, dispense the cash.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, when the cash drops and the pressure sensor fires a second scan, the kiosk checks the database, sees it is already refunded, and dispenses nothing. The loop never starts. By updating the internal record before handing over the cash, we close the exploitation gap entirely.&lt;/p&gt;

&lt;p&gt;In smart contract engineering, this fix is called &lt;strong&gt;Checks-Effects-Interactions (CEI).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check:&lt;/strong&gt; Does this user have a valid claim?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effect:&lt;/strong&gt; Zero their balance in the ledger right now, before a single coin moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaction:&lt;/strong&gt; Now, send the money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reentrancy: The Kiosk on Ethereum&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that you understand the kiosk glitch, you already understand &lt;strong&gt;Reentrancy&lt;/strong&gt;. Because Reentrancy is exactly that glitch, running on a blockchain.&lt;/p&gt;

&lt;p&gt;In Ethereum, when your smart contract sends ETH to an address, if that address belongs to another smart contract, it can execute code the exact moment it receives the ETH. That receiving code is called a &lt;code&gt;receive()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;receive()&lt;/code&gt; function is the pressure sensor. The attacker writes it once, deploys their contract, and the blockchain executes it automatically the moment ETH arrives. They do not manually trigger anything. The callback is a feature of how ETH transfers work, turned into a weapon.&lt;/p&gt;

&lt;p&gt;Here is what the attacker's contract looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract Attacker {
    MilestoneCrowdfund public target;

    // The pressure sensor: When this contract receives ETH, 
    // it immediately calls claimRefund again, before the first call finishes!
    receive() external payable {
        target.claimRefund(campaignId);
    }

    function attack() external {
        target.claimRefund(campaignId);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we sent the money before updating our internal ledger, this attacker would drain our entire protocol in a single transaction. But because we use the Checks-Effects-Interactions pattern, look at the exact order of the &lt;code&gt;claimRefund&lt;/code&gt; function inside &lt;code&gt;MilestoneCrowdfundUpgradeable&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;// 1. CHECKS: Does the user have a valid claim?
uint256 userPledge = _pledges[_id][msg.sender];
if (userPledge == 0) revert MilestoneCrowdfund__NoContribution();

// 2. EFFECTS: Update the ledger first, before money moves!
_pledges[_id][msg.sender] = 0;

// 3. INTERACTIONS: Now the money moves.
(bool success,) = payable(msg.sender).call{value: refundAmount}("");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By zeroing &lt;code&gt;_pledges[_id][msg.sender]&lt;/code&gt; before the ETH moves, every malicious reentrant call finds a zero balance and reverts immediately. The ledger is never one step behind.&lt;/p&gt;

&lt;p&gt;As a second line of defense, I also use OpenZeppelin's &lt;code&gt;nonReentrant&lt;/code&gt; modifier. It works by setting a lock flag at the start of the function. Think of it as a door that locks behind you the moment you step inside. If the malicious &lt;code&gt;receive()&lt;/code&gt; function tries to call &lt;code&gt;claimRefund&lt;/code&gt; again, it slams into that locked door, and the entire transaction instantly reverts.&lt;/p&gt;

&lt;p&gt;CEI makes the contract logically correct. &lt;code&gt;nonReentrant&lt;/code&gt; makes it mechanically impossible to reenter. I tell every junior developer: do not choose between them. Use both. CEI is the airbag. &lt;code&gt;nonReentrant&lt;/code&gt; is the seatbelt. You want both in the car.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Real Edge Cases I Had to Design Around&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Security isn't just about stopping hackers; it's about mitigating the risks of your own design choices. There were two specific edge cases I had to actively design around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Live Fee Rate&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;MilestoneCrowdfundUpgradeable&lt;/code&gt;, the platform fee (&lt;code&gt;defaultFeeBps&lt;/code&gt;) is a global variable that the owner can change at any time. This means two donors to the exact same campaign could pay different effective fees if the owner changes the rate between their pledges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why design it this way?&lt;/strong&gt; Because the stakeholder explicitly requested the flexibility to run dynamic fee promotions. I immediately flagged the vulnerability to them: what if a compromised owner key raised the fee to 5% right before a massive whale pledge, and then immediately lowered it back? This would silently skim thousands of dollars from a single donor, and unless you were watching the transaction pool in real-time, it would be almost invisible.&lt;/p&gt;

&lt;p&gt;I agreed to the stakeholder's requirement, but only with a strict mitigation strategy. The defense here isn't in the Solidity code, it is in the governance architecture. The owner is strictly documented as an Admin Multisig wallet. A single compromised key cannot change the fee unilaterally. The system around the code must be designed with the same rigor as the code itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Dust Sweep&lt;/strong&gt;&lt;br&gt;
When dividing milestones by percentages, integer division always leaves a tiny fraction of a cent (wei) permanently locked in the contract. To prevent this "dust" from being lost forever, my contract uses a special code path for the final milestone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (c.milestonesReleased == c.milestoneCount) {
    amountToRelease = c.totalRaised - c.totalWithdrawn;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It simply sweeps whatever is left. But I had to ask myself rigorously: Can this sweep ever release MORE than it should? The mathematical guarantee that it cannot is my invariant: &lt;code&gt;totalWithdrawn &amp;lt;= totalRaised&lt;/code&gt;. That is precisely what that same 4,495-second fuzz run from the last post was verifying. The fuzz test isn't decoration; it is the mathematical evidence that this final sweep is safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BinnaDev Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are a junior developer about to deploy your first contract that holds real ETH, here is my ultimate advice to you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy until you can answer this question about every function that sends money: "What happens if the recipient is a malicious contract?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not a normal wallet. A smart contract. With a &lt;code&gt;receive()&lt;/code&gt; function you did not write, controlled by someone who wants your users' funds, with a pressure sensor already wired and waiting.&lt;/p&gt;

&lt;p&gt;If you cannot answer that question confidently for every single external call in your codebase, you are not ready to deploy. Go back and apply the Checks-Effects-Interactions pattern to every function that moves value. Add &lt;code&gt;nonReentrant&lt;/code&gt; to every function that moves value. Then ask the question again.&lt;/p&gt;

&lt;p&gt;The developers who get hacked are not the ones who don't know about reentrancy. They are the ones who know about it in theory, but did not sit down with their own code and ask that exact question. Knowledge without application is not protection.&lt;/p&gt;

&lt;p&gt;Read your own code as if you are the attacker. The moment you find something that makes you uncomfortable as the attacker, you have found something to fix as the engineer. If you want a structured baseline for this evaluation, I highly recommend reading Trail of Bits' excellent post, &lt;a href="https://blog.trailofbits.com/2023/08/14/can-you-pass-the-rekt-test" rel="noopener noreferrer"&gt;Can You Pass the Rekt Test?&lt;/a&gt; It is a mandatory checklist for any serious Web3 engineering team.&lt;/p&gt;

&lt;p&gt;Sleep comes after that review. Not before.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We have architected the protocol, proven the math, and secured the vault. But a secure protocol is useless if no one knows how to safely interact with it. In our fifth and final post, we are going to talk about the most underrated skill in Web3: &lt;strong&gt;Writing Audit-Ready Documentation&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>smartcontract</category>
      <category>blockchain</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Stop Guessing, Start Proving: A Guide to Stateful Fuzzing in Foundry</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:55:29 +0000</pubDate>
      <link>https://forem.com/binnadev/stop-guessing-start-proving-a-guide-to-stateful-fuzzing-in-foundry-57l4</link>
      <guid>https://forem.com/binnadev/stop-guessing-start-proving-a-guide-to-stateful-fuzzing-in-foundry-57l4</guid>
      <description>&lt;p&gt;We are building &lt;strong&gt;MilestoneCrowdfundUpgradeable&lt;/strong&gt;: a smart contract that holds donor funds in escrow and releases them only when real-world milestones are verified. In our last post, we designed the engine for this protocol. We built a theoretical fortress guarded by strict mathematical laws.&lt;/p&gt;

&lt;p&gt;But in Web3, a theoretical fortress isn't good enough. If you write a web app and it has a bug, a button doesn't work. If you write a smart contract and it has a bug, people lose their life savings. Testing isn't an afterthought; it is a matter of life or death for your protocol.&lt;/p&gt;

&lt;p&gt;To test this protocol, I didn't just write a few scripts to see if the functions worked. I built a mathematical proving ground. In this post, I want to show you exactly how I did that. We are going to talk about why I switched to Foundry, how to use a "fuzzer" to find your blind spots, the magic of Ghost Variables, and the hardest lesson I learned about testing state machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Shift: Why I Chose Foundry Over Hardhat&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I write tests in &lt;strong&gt;&lt;a href="https://hardhat.org" rel="noopener noreferrer"&gt;Hardhat&lt;/a&gt;&lt;/strong&gt;, I am writing &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; that talks to a Solidity contract. There is a translation layer between my brain and the blockchain. I find myself fighting JavaScript's &lt;code&gt;async/await&lt;/code&gt;, weird big-number edge cases, and ABI encoding at the exact same time I am trying to think about protocol correctness. It's exhausting.&lt;/p&gt;

&lt;p&gt;Foundry breaks that barrier. In &lt;strong&gt;&lt;a href="https://www.getfoundry.sh" rel="noopener noreferrer"&gt;Foundry&lt;/a&gt;&lt;/strong&gt;, your tests are written in &lt;a href="https://www.soliditylang.org/" rel="noopener noreferrer"&gt;Solidity&lt;/a&gt;. The EVM (the actual engine that runs Ethereum) is the test runtime. That means there is no JavaScript middleman translating your code. If I want to pretend to be a user named Alice, I just write &lt;code&gt;vm.prank(alice)&lt;/code&gt;. If I want to fast-forward time by 30 days, I write &lt;code&gt;vm.warp(block.timestamp + 30 days)&lt;/code&gt;. It reads like pseudocode.&lt;/p&gt;

&lt;p&gt;But the single biggest reason I switched, the one that changed how I think about testing entirely is &lt;strong&gt;exactness&lt;/strong&gt;. In Hardhat, you approximate gas. In Foundry, the gas numbers in your test output are the exact gas numbers you will see on mainnet. When you're designing a crowdfunding protocol where everyday donors are paying for every pledge call, that exactness matters enormously.&lt;/p&gt;

&lt;p&gt;Hardhat is an incredible tool for deployment and scripting, but Foundry was built from the ground up for testing. That is not a knock on Hardhat, it is just not the same tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateless Fuzzing: Why Random Beats Hardcoded&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the problem with writing &lt;code&gt;pledge(100)&lt;/code&gt; in your test suite. Your brain picked the number 100. Your brain also wrote the smart contract. So your test is testing your own assumptions, not the contract's actual behavior. You have the same blind spots on both sides of the assertion.&lt;/p&gt;

&lt;p&gt;Stateless Fuzzing breaks that loop. A fuzzer is a robot that throws thousands of random, chaotic inputs at your contract to see what breaks.&lt;/p&gt;

&lt;p&gt;For example, I wrote a function to calculate the platform fee (&lt;code&gt;_calculateFeeAndNet&lt;/code&gt;). The obvious test is: &lt;code&gt;gross = 1000&lt;/code&gt;, &lt;code&gt;fee = 5%&lt;/code&gt;, expect &lt;code&gt;net = 950&lt;/code&gt;. That passes. Fine.&lt;/p&gt;

&lt;p&gt;But what does the Foundry fuzzer actually throw at it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gross = 1&lt;/code&gt;, &lt;code&gt;fee = 4.99%&lt;/code&gt;. (Does integer division correctly floor the fee to zero, leaving the full amount as the net?)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gross = type(uint256).max&lt;/code&gt;. (Does it trigger an overflow crash before the math finishes?)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fee = 0&lt;/code&gt;. (Does the contract actually return &lt;code&gt;(gross, 0)&lt;/code&gt;, or does it accidentally return &lt;code&gt;(0, 0)&lt;/code&gt;?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those are inputs I would naturally write. The fuzzer doesn't have my assumptions. That's its superpower.&lt;/p&gt;

&lt;p&gt;This was crucial for my Basis Points (BPS) math. I wrote a helper to divide 10,000 BPS across &lt;code&gt;n&lt;/code&gt; milestones. I could have hardcoded an array of 3 milestones and called it done. Instead, the fuzzer randomly varies &lt;code&gt;n&lt;/code&gt; on every single run. Eventually, it tries &lt;code&gt;n=1&lt;/code&gt; (a single milestone getting 100%) and &lt;code&gt;n=20&lt;/code&gt;(20 milestones, each getting ~500 BPS, with the last one absorbing all the mathematical rounding dust). Those extreme edge cases stress the math in ways a handwritten array never would.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateful Fuzzing &amp;amp; Ghost Variables: The Bank Auditor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stateless fuzzing tests one function at a time. But a crowdfunding contract isn't a calculator, it's a state machine. The bug is almost never in a single function in isolation. It's in the sequence. &lt;code&gt;pledgeETH&lt;/code&gt; is fine. &lt;code&gt;withdrawMilestone&lt;/code&gt; is fine. But what happens if the sequence is &lt;code&gt;pledgeETH -&amp;gt; setFee -&amp;gt; pledgeETH -&amp;gt; finalize -&amp;gt; withdrawMilestone&lt;/code&gt;? That is where the accounting drifts.&lt;/p&gt;

&lt;p&gt;To test sequences, we use &lt;strong&gt;Stateful Fuzzing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To explain how this works, imagine you are a Bank Auditor. You want to verify that a bank teller never gives out more money than was deposited. You could stand at the counter and check after every single transaction. But the teller's official ledger is locked inside a vault you can't open. You can only see the cash they hand to customers.&lt;/p&gt;

&lt;p&gt;So, you bring your own notepad. Every time someone deposits $100, you write "+$100" on your notepad. Every time someone withdraws, you write "-$50". At the end of the day, your notepad says the vault should hold exactly $50. If the vault actually holds $40, something went wrong in the sequence of the day's transactions.&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%2F2cha26bf1ofo33hz2ysu.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%2F2cha26bf1ofo33hz2ysu.png" alt="This diagram illustrates a fuzzing-based testing workflow for a smart contract acting as a vault." width="800" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my Foundry tests, that "notepad" is called a &lt;strong&gt;Ghost Variable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The actual pledge ledger inside my smart contract (&lt;code&gt;_pledges&lt;/code&gt;) is private. The test suite can't read it easily. So, I built a &lt;code&gt;Handler&lt;/code&gt; contract that acts as the Auditor. It carries&lt;code&gt;ghost_netPledge&lt;/code&gt;, its own running tally of what the contract should contain. Every time a random pledge succeeds, the handler writes it down. Every time a refund succeeds, it zeroes that entry.&lt;/p&gt;

&lt;p&gt;At the end of thousands of random actions, the test asks: &lt;em&gt;Does the real smart contract match my notepad?&lt;/em&gt; Imagine a scenario where Alice pledges 1 ETH, but a bug prevents the platform fee from being deducted. My Ghost Variable notepad records &lt;code&gt;0.95 ETH&lt;/code&gt; (the net), but the contract's &lt;code&gt;getPledge()&lt;/code&gt; returns &lt;code&gt;1.00 ETH&lt;/code&gt;. The moment they diverge, the invariant fires. The fuzzer stops and prints the exact 12-call sequence that broke the math. That sequence is my counterexample. That's the bug report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Struggle: 4,495 Seconds of Patience&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%2Fusaqpkq0jqip8ki58xgx.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%2Fusaqpkq0jqip8ki58xgx.png" alt="Test Results Summary: The fuzzing campaign completed successfully, 163 tests passed, 0 failed" width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the part nobody talks about in tutorials.&lt;/p&gt;

&lt;p&gt;You can see it in the screenshot from my terminal. &lt;strong&gt;4,495 seconds&lt;/strong&gt;. That is over an hour and fifteen minutes of my laptop running at full capacity just to complete one single test run.&lt;/p&gt;

&lt;p&gt;That number tells the real story of stateful fuzzing. My fuzzer ran this long because of the rigorous constraints I set. Here is exactly how I configured it. &lt;code&gt;runs&lt;/code&gt; is how many random sequences the fuzzer invents, &lt;code&gt;depth&lt;/code&gt; is how many calls deep each sequence goes, and the &lt;code&gt;seed&lt;/code&gt; ensures the run is mathematically reproducible so I can track down bugs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[invariant]&lt;/span&gt;
&lt;span class="py"&gt;runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="py"&gt;depth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; 
&lt;span class="py"&gt;fail_on_revert&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;call_override_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="nn"&gt;[fuzz]&lt;/span&gt;
&lt;span class="py"&gt;runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="py"&gt;max_test_rejects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;
&lt;span class="py"&gt;seed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0x63726f776466756e64"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fuzzer is not running one test, it is inventing thousands of random call sequences, executing them, and checking whether the math breaks. That computational cost is the point. You are paying for thoroughness with time.&lt;/p&gt;

&lt;p&gt;But the truly humbling part is the feedback loop.&lt;/p&gt;

&lt;p&gt;When you write a normal unit test and something breaks, you fix it and rerun it in seconds. With a stateful fuzz suite, the cycle feels like this:&lt;br&gt;
&lt;em&gt;Find a bug -&amp;gt; fix the contract -&amp;gt; wait 75 minutes -&amp;gt; find another bug -&amp;gt; fix -&amp;gt; wait another 75 minutes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, to be fair to Foundry, it does provide a lifesaver here. When a stateful fuzz run fails, Foundry gives you the exact seed and call sequence, allowing you to replay just that one failed scenario in seconds without having to rerun the entire 75-minute suite. But the broader lesson remains.&lt;/p&gt;

&lt;p&gt;You learn very quickly to think carefully before you type. Every structural change carries a heavy price tag in computational validation. That constraint made me a more deliberate engineer. I stopped making small speculative fixes and started reasoning through the problem fully on paper before touching the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Coverage Illusion&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%2Fvkiauxxrlqq08mj32mec.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%2Fvkiauxxrlqq08mj32mec.png" alt="Test Results Summary: The fuzzing campaign completed successfully, 163 tests passed, 0 failed" width="800" height="184"&gt;&lt;/a&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%2Fzqzgyxtzdvp4rbsy3msw.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%2Fzqzgyxtzdvp4rbsy3msw.png" alt="Test Results Summary" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now look at the numbers in that screenshot more carefully.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MilestoneCrowdfundUpgradeable.sol&lt;/code&gt; - &lt;code&gt;100% line coverage&lt;/code&gt;, &lt;code&gt;100% function coverage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A junior developer sees that and thinks: &lt;em&gt;We are done, the protocol is safe&lt;/em&gt;. I want to push back on that directly. 100% coverage does not mean 100% secure. Not even close.&lt;/p&gt;

&lt;p&gt;Coverage only tells you that the fuzzer visited every line. It does not tell you whether the mathematical rules governing those lines can be broken by a clever sequence of calls. A line can be executed ten thousand times and still contain an accounting bug that only surfaces on the ten-thousand-and-first call in a specific order.&lt;/p&gt;

&lt;p&gt;This is why the &lt;strong&gt;invariants&lt;/strong&gt; matter more than the coverage number. The invariants are the rules that must never break:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;totalWithdrawn&lt;/code&gt; must never exceed &lt;code&gt;totalRaised&lt;/code&gt;, otherwise, the creator is stealing from future refund claimants.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sum(milestoneBps)&lt;/code&gt; must always equal 10,000, otherwise, the math will trap dust in the contract or fail to release the full amount.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;totalWithdrawn + totalRefunded&lt;/code&gt; must never exceed &lt;code&gt;totalRaised&lt;/code&gt;, otherwise, the contract is paying out money that was never deposited, meaning it has become completely insolvent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those rules hold across thousands of random sequences, the protocol math is battle-tested. If coverage is 100% but an invariant breaks, you have a vulnerable protocol with a false sense of security. I would rather have 80% coverage and unbreakable invariants than 100% coverage and uninspected state transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Advice I Would Give My Fellow Dev&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stop writing tests that only ask &lt;em&gt;"did this function return the right number?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Start writing tests that ask &lt;em&gt;"can this sequence of 50 random actions, executed by strangers in any order, break the rules this protocol was built on?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Unit tests are essential, do not skip them. But they test your assumptions. The fuzzer tests your blind spots. Once you understand state machines and invariants, you stop guessing whether your protocol is safe and start proving it. That shift in thinking from hoping the code is correct to mathematically verifying it is the difference between a developer and an engineer.&lt;/p&gt;

&lt;p&gt;The argument for trusting your test suite is never complete until you prove it works. Before I trusted my invariants, I deliberately broke my own contract. I went into &lt;code&gt;pledgeETH&lt;/code&gt; and changed &lt;code&gt;c.totalRaised += net&lt;/code&gt; to &lt;code&gt;c.totalRaised += gross&lt;/code&gt;. I ran the suite. Instantly, Invariant 6 fired and the test failed. That is the difference between claiming your alarm system works and proving it by tripping it yourself.&lt;/p&gt;

&lt;p&gt;The 4,495 seconds were worth every one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your protocol can pass every invariant and still be drained in a single transaction. In the next post, we look at how.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>foundry</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>From Promises to Proof: Designing a Defensive Escrow Protocol</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 27 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://forem.com/binnadev/from-promises-to-proof-designing-a-defensive-escrow-protocol-h92</link>
      <guid>https://forem.com/binnadev/from-promises-to-proof-designing-a-defensive-escrow-protocol-h92</guid>
      <description>&lt;p&gt;In the last post, we looked at how to make smart contracts upgradeable safely. But an upgradeable proxy is just an empty building. Today, we are walking inside the building to look at the engine.&lt;/p&gt;

&lt;p&gt;If you are new to Web3, you might wonder why we even need smart contracts for crowdfunding.&lt;/p&gt;

&lt;p&gt;Think about traditional crowdfunding platforms like &lt;a href="https://www.kickstarter.com/" rel="noopener noreferrer"&gt;Kickstarter&lt;/a&gt;. You trust the creator, you send them your money upfront, and you hope they deliver. Or, think about basic crypto transfers: you send ETH directly to a developer's wallet.&lt;/p&gt;

&lt;p&gt;Both scenarios have a simple, critical flaw: &lt;strong&gt;you are giving away 100% of your money based on a promise.&lt;/strong&gt; There is no enforced rule that says, "You only get paid if you actually do the work." If the creator vanishes after taking your money, you are out of luck.&lt;/p&gt;

&lt;p&gt;What I wanted to fix with the &lt;strong&gt;&lt;a href="https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393" rel="noopener noreferrer"&gt;MilestoneCrowdfundUpgradeable protocol&lt;/a&gt;&lt;/strong&gt; wasn't fundraising. It was execution trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mental Model: What is a Defensive Escrow?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I framed the architecture around a concept I call a &lt;strong&gt;Defensive Escrow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of sending money directly to a person, you send it to a smart contract. Think of the smart contract as an unbreakable, unbiased robot middleman. The robot locks the money in a vault. It only releases the funds chunk by chunk as the creator proves they've completed specific milestones.&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%2Fltbp4nyjyxo4mn8bguu6.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%2Fltbp4nyjyxo4mn8bguu6.png" alt="MilestoneCrowdfundUpgradeable architecture Image 1" width="800" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the creator fails or disappears, the robot protects you automatically by returning whatever is left in the vault.&lt;/p&gt;

&lt;p&gt;Here is how I designed the mechanics to make that happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Math Problem: Why 10,000 Basis Points?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you build financial systems in Solidity, you quickly learn one thing: the Ethereum blockchain hates decimals.&lt;/p&gt;

&lt;p&gt;If you try to divide $100 by 3, a normal computer gives you $33.3333... But Solidity doesn't do fractions well. Those leftover &lt;code&gt;.3333&lt;/code&gt; fractions (we call them "dust") can get permanently trapped in the contract. Over time, the math breaks.&lt;/p&gt;

&lt;p&gt;To remove this ambiguity, I standardized everything using a financial concept called &lt;strong&gt;Basis Points (BPS)&lt;/strong&gt;. In this system, &lt;code&gt;10,000 BPS&lt;/code&gt; is exactly equal to &lt;code&gt;100%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of giving a creator a fixed amount of tokens, we give them slices of a pie.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phase 1 is &lt;code&gt;4000 BPS&lt;/code&gt; (40% of the pie).&lt;/li&gt;
&lt;li&gt;Phase 2 is &lt;code&gt;4000 BPS&lt;/code&gt; (40% of the pie).&lt;/li&gt;
&lt;li&gt;Phase 3 is &lt;code&gt;2000 BPS&lt;/code&gt; (20% of the pie).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why is this pie method so important? &lt;strong&gt;Stretch goals.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a charity sets a goal of $10,000, but the campaign goes viral and raises $50,000. If our code said &lt;em&gt;"Give the creator exactly $4,000 for Phase 1,"&lt;/em&gt; the contract would get confused by the extra $40,000 sitting in the vault.&lt;/p&gt;

&lt;p&gt;But because our milestones are percentages tied to the total amount raised, the math adapts automatically.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;40% of $10,000 is $4,000.&lt;/li&gt;
&lt;li&gt;40% of $50,000 is $20,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pie gets bigger, but the sizes of the slices stay exactly the same. The code doesn't break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful Failure: The "Abandoned" State&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most smart contracts only understand black and white: Success or Failure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the campaign fails to hit its goal, the robot opens the vault and gives everyone a 100% refund. Easy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what if the campaign succeeds, the creator takes 40% of the funds to start Phase 1, and then they vanish? You cannot pretend nothing happened. 100% of the money isn't there anymore.&lt;/p&gt;

&lt;p&gt;This is where the "Defensive" part of the Defensive Escrow kicks in. I added a state called &lt;strong&gt;Abandoned.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a creator goes rogue, an Admin can press a &lt;code&gt;haltCampaign&lt;/code&gt; button. The robot permanently locks the vault.&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%2Fp5lw7httxl6u6m8ljayj.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%2Fp5lw7httxl6u6m8ljayj.png" alt="MilestoneCrowdfundUpgradeable architecture Image 2" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The concept is simple: you are refunded based on the work that was never unlocked. If you pledged 10 ETH, and the creator ran away after taking 40%, the robot mathematically guarantees you get your remaining 6 ETH back.&lt;/p&gt;

&lt;p&gt;This turns a "crypto scam" into a structured, safe process. There is no emotional arguing over who gets what. The math just settles the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fiat Bridge: Bringing Web2 to Web3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most Web3 systems make a fatal assumption: they assume every user already has a crypto wallet and knows how to pay gas fees. That assumption kills adoption.&lt;/p&gt;

&lt;p&gt;Now, you might be thinking: &lt;em&gt;"Wait, Obinna, don't we already have smart accounts where users can log in with their Gmail? Aren't there easy fiat on-ramps and off-ramps today?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yes, absolutely. The Web3 ecosystem has made massive strides with &lt;a href="https://ethereum.org/roadmap/account-abstraction/" rel="noopener noreferrer"&gt;Account Abstraction&lt;/a&gt; and &lt;a href="https://docs.metamask.io/embedded-wallets" rel="noopener noreferrer"&gt;embedded wallets&lt;/a&gt;. However, I still chose to architect a direct platform-to-fiat bridge called &lt;code&gt;pledgeFiatOnBehalf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because true adoption requires absolute user flexibility. Some people simply do not want to go through a crypto on-ramp or manage a smart account even a hidden one. They want to type their credit card into a familiar website and be done in 30 seconds, remaining entirely Web2-native. By building a custodial bridge, we allow those users to participate natively, while their economic support is still mathematically secured in our smart contract.&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%2Fqygz76vf1uwany59njv0.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%2Fqygz76vf1uwany59njv0.png" alt="MilestoneCrowdfundUpgradeable architecture Image 3" width="800" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If Bob pays $100 with a credit card on our website, our secure backend sees the payment. Then, our own "Platform Wallet" interacts with the smart contract, depositing $100 worth of crypto into the vault on &lt;em&gt;Bob's behalf.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We act as a custodian for Bob. The messy credit card logic stays entirely off-chain, while the blockchain remains the ultimate, pristine source of truth for the project's funding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trust Model: "Wait, isn't this centralized?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have been reading closely, you might have noticed a recurring theme: an Admin or Platform wallet has the power to approve milestones, press the "Halt" button, and bridge fiat payments.&lt;/p&gt;

&lt;p&gt;A developer might look at this and ask, &lt;em&gt;"Wait, Obinna, isn't this centralized?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is a great question. Yes, there is a human element. But having an administrative role is not the same as having total system control. To understand why, you need to understand the concept of an &lt;strong&gt;Invariant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In smart contract engineering, an Invariant is a mathematical law written into the code that can never, ever be broken. Not even by the creator. Not even by the Admin.&lt;/p&gt;

&lt;p&gt;Here is the distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What the Admin CAN do:&lt;/strong&gt; They can pause the protocol, tell the robot a milestone is finished, or press the "Halt" button to trigger refunds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the Admin CANNOT do:&lt;/strong&gt; They cannot withdraw user funds to their own wallet. They cannot bypass the milestone math. They cannot rewrite the refund logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real protection is not governance, it's the strict math constraints written into the robot's code.&lt;br&gt;
The trust model is simple: &lt;strong&gt;Humans can trigger states, but they cannot violate the financial laws of the system.&lt;/strong&gt; That is the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BinnaDev Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I step back and summarize the architecture, this isn't just "crowdfunding with milestones."&lt;/p&gt;

&lt;p&gt;It is a system where capital is locked into predictable, mathematical rules. Failure is not a disastrous rug-pull; it is a structured, mathematically safe process. Fiat and crypto are unified so anyone can participate on their own terms.&lt;/p&gt;

&lt;p&gt;Or, to put it in one line:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Immutability handles correctness. Milestones handle trust. Invariants handle safety.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Now that we have written the rules of the protocol, how do we prove they actually work? How do we know a hacker can't break the math? In the next post, we are going to dive into &lt;strong&gt;&lt;a href="https://www.getfoundry.sh/forge/testing" rel="noopener noreferrer"&gt;Foundry Testing&lt;/a&gt;&lt;/strong&gt;. I'll show you how I test these invariants to mathematically prove they hold up against any attack.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>smartcontract</category>
    </item>
    <item>
      <title>Immutability by Default, Upgradeability by Necessity: Lessons from a Crowdfunding Protocol</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Sat, 25 Apr 2026 01:09:08 +0000</pubDate>
      <link>https://forem.com/binnadev/immutability-by-default-upgradeability-by-necessity-lessons-from-a-crowdfunding-protocol-3jm7</link>
      <guid>https://forem.com/binnadev/immutability-by-default-upgradeability-by-necessity-lessons-from-a-crowdfunding-protocol-3jm7</guid>
      <description>&lt;p&gt;Smart contracts handle real value, so I believe every line of code should communicate trust.&lt;/p&gt;

&lt;p&gt;When you first start learning Solidity, you are taught one golden rule: &lt;em&gt;smart contracts are immutable&lt;/em&gt;. Deploying a contract is like launching a rocket into space, once it leaves the launchpad, you can't easily change the wiring. If there is a bug, the code is set in stone.&lt;/p&gt;

&lt;p&gt;But recently, while building &lt;strong&gt;&lt;a href="https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393" rel="noopener noreferrer"&gt;MilestoneCrowdfundUpgradeable&lt;/a&gt;&lt;/strong&gt;: an onchain escrow protocol verified on &lt;strong&gt;&lt;a href="https://polygon.technology/" rel="noopener noreferrer"&gt;Polygon&lt;/a&gt;&lt;/strong&gt;. I hit a classic Web3 crossroads.&lt;/p&gt;

&lt;p&gt;My stakeholder had a very practical requirement: &lt;em&gt;flexibility&lt;/em&gt;. We knew we had future upgrades planned for the protocol, but we couldn't ask our users to interact with a new contract address every single month. That is a terrible user experience and a quick way to erode trust. We needed a single, reliable entry point for users, while maintaining the ability to upgrade the underlying logic.&lt;/p&gt;

&lt;p&gt;We needed an upgradeable contract.&lt;/p&gt;

&lt;p&gt;In this post (the first of a five-part series), I want to sit down with you and share exactly how I approached &lt;strong&gt;Upgradeability&lt;/strong&gt;. If you are a beginner or intermediate developer trying to wrap your head around proxies, storage collisions, and initialization traps, this post is for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Actually is a Proxy? (The Restaurant Analogy)&lt;/strong&gt;&lt;br&gt;
Before we talk about how to upgrade, we have to understand the architecture. How do you change code that is supposed to be unchangeable?&lt;/p&gt;

&lt;p&gt;You split the contract into two pieces. I like to explain it using a restaurant analogy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Proxy (The Building &amp;amp; Cash Register):&lt;/strong&gt; This contract has a permanent address. Users only ever interact with this contract. It holds all the money (ETH) and all the data (state).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Implementation (The Kitchen Staff &amp;amp; Recipe):&lt;/strong&gt; This contract holds the actual logic (the code for &lt;code&gt;pledge()&lt;/code&gt;, &lt;code&gt;refund()&lt;/code&gt;, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a user sends money to the Proxy, the Proxy uses a special Ethereum command called &lt;code&gt;delegatecall&lt;/code&gt;. It basically says to the Implementation: &lt;em&gt;"Hey Kitchen, borrow my ingredients and tell me how to process this order, but leave the money in my cash register."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If we find a bug in our recipe, the Admin simply deploys a brand new Implementation contract (a new Kitchen Staff), and tells the Proxy to start asking them for instructions instead. The building address and the cash register never change.&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%2Fnc8q35dohxlp73i7ylrc.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%2Fnc8q35dohxlp73i7ylrc.png" alt="MilestoneCrowdfundUpgradeable architecture Image 1" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a few ways to build this. I chose a pattern called &lt;strong&gt;UUPS&lt;/strong&gt; (Universal Upgradeable Proxy Standard) via the &lt;a href="https://www.openzeppelin.com/solidity-contracts" rel="noopener noreferrer"&gt;OpenZeppelin library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Without getting too bogged down in jargon, older patterns (like Transparent Proxies) put the "upgrade manager" in the Proxy itself. UUPS puts the "upgrade manager" in the Implementation contract. I chose UUPS because it makes the Proxy lightweight, which makes transactions cheaper (lower gas fees) for our users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Constructor Trap (And How to Get Hacked)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you write a normal smart contract, you use a &lt;code&gt;constructor()&lt;/code&gt; to set up your initial variables (like assigning the &lt;code&gt;owner&lt;/code&gt;). Constructors run exactly once, during deployment.&lt;br&gt;
But in a proxy setup, the Proxy deploys after the Implementation. The Proxy can't trigger the Implementation's constructor. So, the golden rule of upgradeability is: &lt;strong&gt;Do not use constructors. Use an &lt;code&gt;initialize()&lt;/code&gt; function instead.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first, it feels like you can just swap the words and move on. But here is the trap: &lt;em&gt;The implementation contract is still a live contract sitting on the blockchain&lt;/em&gt;. Imagine you deploy your Implementation contract, and it has an &lt;code&gt;initialize()&lt;/code&gt; function that sets the owner. Your Proxy connects to it and calls &lt;code&gt;initialize()&lt;/code&gt;. Great! Your Proxy is the &lt;code&gt;owner&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But what about the Implementation contract itself? It's just floating out there. If you don't lock it, a hacker can call &lt;code&gt;initialize()&lt;/code&gt; directly on the Implementation contract, make themselves the owner, and potentially command it to self-destruct. If the Implementation is destroyed, your Proxy is permanently broken.&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%2F4lus79cngg2nkb7dzwqb.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%2F4lus79cngg2nkb7dzwqb.png" alt="MilestoneCrowdfundUpgradeable architecture Image 2" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To prevent this, my approach is strict:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All protocol setup goes into &lt;code&gt;initialize()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I immediately lock the Implementation contract using a special constructor.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    // This locks the Implementation contract so no one can initialize it directly
    _disableInitializers(); 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;From a mindset perspective: &lt;strong&gt;Assume attackers will interact with your implementation contract directly.&lt;/strong&gt; Once you internalize that, locking it stops being a confusing "trap" and simply becomes part of your default security posture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage Management: The Giant Spreadsheet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upgradeable contracts are notorious for "storage collisions."&lt;/p&gt;

&lt;p&gt;Think of smart contract storage like a giant, invisible spreadsheet. Row 1 is &lt;code&gt;owner&lt;/code&gt;. Row 2 is &lt;code&gt;totalRaised&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you upgrade to Implementation V2, the Proxy still uses that exact same spreadsheet. If you accidentally write V2 so that a new variable called &lt;code&gt;isCampaignActive&lt;/code&gt; is placed in Row 1, you just overwrote the &lt;code&gt;owner&lt;/code&gt;! That is a storage collision, and it is catastrophic.&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%2F1080j4lu5eeb66270z71.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%2F1080j4lu5eeb66270z71.png" alt="MilestoneCrowdfundUpgradeable architecture Image 3" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To prevent this, my approach is conservative and explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Append-only storage:&lt;/strong&gt; Never reorder variables. Never delete variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage Gaps:&lt;/strong&gt; I use the classic &lt;code&gt;uint256[50] private __gap;&lt;/code&gt; at the bottom of my contracts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A storage gap is literally just claiming 50 empty rows at the bottom of your spreadsheet. If I ever need to add a new variable in V2, I take one row away from the gap (&lt;code&gt;uint256[49] private __gap;&lt;/code&gt;) and add my new variable.&lt;/p&gt;

&lt;p&gt;Why gaps instead of newer, complex patterns (like &lt;strong&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-7201" rel="noopener noreferrer"&gt;ERC-7201&lt;/a&gt;&lt;/strong&gt;)? Because it's simple, battle-tested, and auditors easily understand it. Storage layout is not "code you can refactor"; it's "state you must preserve forever." Discipline is your real protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hardest Lesson: Hunting for Ghosts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The single most frustrating moment I had while setting up this project in &lt;strong&gt;&lt;a href="https://www.getfoundry.sh/" rel="noopener noreferrer"&gt;Foundry&lt;/a&gt;&lt;/strong&gt; (my testing framework) came down to one line of code.&lt;/p&gt;

&lt;p&gt;In Web3, we use something called a &lt;code&gt;ReentrancyGuard&lt;/code&gt; to stop hackers from double-spending money. Because I was building an upgradeable contract, I kept trying to import &lt;code&gt;ReentrancyGuardUpgradeable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It just wasn't there. Most older tutorials, blog posts, and AI tools told me to use the Upgradeable version, so it felt like I was doing something wrong.&lt;/p&gt;

&lt;p&gt;What finally clicked was reading the source code of the newest OpenZeppelin version. I realized that the standard &lt;code&gt;ReentrancyGuard&lt;/code&gt; no longer relied on a constructor. It used internal logic that worked perfectly fine behind a proxy. I didn't need an upgradeable version anymore; I could just use the normal one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReentrancyGuard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@openzeppelin/contracts/utils/ReentrancyGuard.sol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frustrating part wasn't the code, it was the mismatch between my old mental model ("everything must have an Upgradeable suffix") and reality (some contracts are now inherently proxy-safe).&lt;/p&gt;

&lt;p&gt;It forced a deeper shift in how I work: &lt;strong&gt;I stopped coding from patterns, and started reasoning from first principles.&lt;/strong&gt; Upgradeability isn't just about making a proxy; it's about understanding how every single tool you import behaves under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BinnaDev Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a junior developer asked me today, "Obinna, should I make my next project upgradeable?" my answer would be:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It depends but try not to use it unless you have a clear, defensible reason.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Upgradeability is not a free feature. You gain flexibility, but you introduce new attack surfaces and complexity. If you can ship your protocol as immutable, do that first. It gives you a simpler mental model, fewer failure modes, and it is vastly easier to secure.&lt;/p&gt;

&lt;p&gt;Only reach for upgradeability if the system will hold significant user funds, has evolving logic, or needs long-term maintenance. At that point, upgradeability becomes a risk management tool, not a convenience.&lt;/p&gt;

&lt;p&gt;My guiding principle is this: &lt;strong&gt;Immutability by default, upgradeability by necessity.&lt;/strong&gt; If you can't clearly explain why it must be upgradeable, who controls the upgrades, and how users are protected, then you aren't ready to use it yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment Update&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;MilestoneCrowdfundUpgradeable&lt;/strong&gt; contract has been successfully deployed on &lt;strong&gt;&lt;a href="https://polygon.technology/" rel="noopener noreferrer"&gt;Polygon&lt;/a&gt;&lt;/strong&gt; (Chain ID: 137), marking the transition from design and testing into a live, verifiable environment.&lt;/p&gt;

&lt;p&gt;Here are the core deployment details:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Proxy Address:&lt;/strong&gt; &lt;a href="https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393" rel="noopener noreferrer"&gt;https://polygonscan.com/address/0xf83aaB5f1fAA1a7a74AD27E2f8058801EaA31393&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation Address:&lt;/strong&gt; &lt;a href="https://polygonscan.com/address/0x003e81b1b080029b87c728590c9bfec339180625" rel="noopener noreferrer"&gt;https://polygonscan.com/address/0x003e81b1b080029b87c728590c9bfec339180625&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This deployment reflects the architectural decisions discussed earlier: a proxy-based upgradeable system with clearly defined control boundaries and fund flow roles. With the contract now live, the focus shifts from infrastructure to behavior, how funds move, how milestones are enforced, and how the protocol responds under real-world conditions.&lt;/p&gt;

&lt;p&gt;In the next post, we'll dive into the actual mechanics of the MilestoneCrowdfundUpgradeable protocol itself. I'll walk you through how we designed the escrow, the math behind milestone-based releases, and the trust model that protects users if a creator decides to walk away.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>solidity</category>
      <category>smartcontract</category>
    </item>
    <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://forem.com/binnadev/integrating-trust-a-developers-guide-to-the-resume-protocol-1f72</link>
      <guid>https://forem.com/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://forem.com/binnadev/your-career-onchain-building-a-resume-protocol-with-purpose-and-trust-3p67</link>
      <guid>https://forem.com/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://forem.com/binnadev/engineering-trust-my-journey-building-a-decentralized-stablecoin-a9p</link>
      <guid>https://forem.com/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://forem.com/binnadev/monad-is-fast-your-code-should-be-reliable-a-new-foundry-starter-kit-28h1</link>
      <guid>https://forem.com/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://forem.com/binnadev/building-a-reusable-multi-event-soulbound-nft-contract-in-solidity-1hmm</link>
      <guid>https://forem.com/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://forem.com/binnadev/building-for-trust-a-guide-to-gasless-transactions-with-erc-2771-3ojf</link>
      <guid>https://forem.com/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://forem.com/binnadev/why-i-rebuilt-my-developer-portfolio-around-three-core-values-reliability-thoughtfulness-and-3ncm</link>
      <guid>https://forem.com/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>
