<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Obinna Duru</title>
    <description>The latest articles on DEV Community by Obinna Duru (@binnadev).</description>
    <link>https://dev.to/binnadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3596254%2Fe41e7fc6-92fd-4450-8764-75345f856471.jpg</url>
      <title>DEV Community: Obinna Duru</title>
      <link>https://dev.to/binnadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/binnadev"/>
    <language>en</language>
    <item>
      <title>Understanding Gas on the Blockchain: A Practical Guide Using Real Transactions</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:33:56 +0000</pubDate>
      <link>https://dev.to/binnadev/understanding-gas-on-the-blockchain-a-practical-guide-using-real-transactions-513j</link>
      <guid>https://dev.to/binnadev/understanding-gas-on-the-blockchain-a-practical-guide-using-real-transactions-513j</guid>
      <description>&lt;p&gt;When I first encountered the concept of gas in blockchain, I understood on the surface that it measured computational work. However, I did not fully understand what that meant in practice, how gas is actually calculated under the hood, why transactions need gas limits, or how all of those numbers come together to form a transaction fee.&lt;/p&gt;

&lt;p&gt;Reading high-level definitions is one thing, but opening a block explorer and staring at a raw transaction receipt is another. Where does a specific number like &lt;code&gt;49,785&lt;/code&gt; or &lt;code&gt;152,492&lt;/code&gt; gas come from? Why does a transaction that transfers &lt;code&gt;0 ETH&lt;/code&gt; or &lt;code&gt;0 MON&lt;/code&gt; still cost money? And what actually happens to the gas you don't use?&lt;/p&gt;

&lt;p&gt;For me, the best way to truly grasp a technical concept--to the point where you can confidently explain it to someone else--is to connect the abstract math to a physical, real-world analogy and then test it against real data.&lt;/p&gt;

&lt;p&gt;In this article, we are going to break down how gas works from the ground up. We will build a simple physical mental model, look at how the Ethereum Virtual Machine (EVM) counts work, and then inspect &lt;strong&gt;two real blockchain transactions&lt;/strong&gt;: one where I interacted directly with a smart contract on &lt;strong&gt;Monad Testnet&lt;/strong&gt;, and a second transaction hash I pulled from the &lt;strong&gt;Ethereum Sepolia Testnet&lt;/strong&gt; explorer to see how the two networks compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating the Five Core Concepts
&lt;/h2&gt;

&lt;p&gt;When people talk about "gas," they often lump several different things into one blurry idea of "the fee I pay." To make transaction receipts easy to read, we first need to separate five distinct concepts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gas:&lt;/strong&gt; The unit used to measure the computational work required to process a transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas Limit:&lt;/strong&gt; The maximum amount of gas a transaction is allowed to consume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas Used:&lt;/strong&gt; The amount of gas the transaction actually consumed during execution. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas Price:&lt;/strong&gt; The monetary rate (in cryptocurrency) paid per single unit of gas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Fee (Gas Fee):&lt;/strong&gt; The final amount of cryptocurrency paid for processing the transaction, calculated according to the network's rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These concepts are closely related, but they do not mean the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Relatable Analogy: The Delivery Truck&lt;/strong&gt;&lt;br&gt;
Think of executing a blockchain transaction like hiring a delivery truck to drive across town and flip a switch at a warehouse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gas (The Work)&lt;/strong&gt;: Driving 15 kilometers up a steep hill physically requires &lt;strong&gt;10 liters of fuel&lt;/strong&gt;. Notice that "liters" measures physical effort, not money. Whether fuel is cheap or expensive today, the engine still burns 10 liters to climb that hill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas Limit (Your Fuel Cap)&lt;/strong&gt;: Before the driver leaves, you hand them a fuel card authorized for a maximum of &lt;strong&gt;12 liters&lt;/strong&gt;. You are saying: &lt;em&gt;"This trip should take 10 liters, so I am allowing up to 12 liters as a safety buffer. If the road is blocked and you burn all 12 liters before getting there, stop driving immediately."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas Used (Actual Consumption)&lt;/strong&gt;: The driver completes the trip and the engine actually burns &lt;strong&gt;10 liters&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas Price (Price per Liter)&lt;/strong&gt;: At the pump today, fuel costs &lt;strong&gt;$2 per liter&lt;/strong&gt;. Tomorrow, during heavy traffic demand, it might cost &lt;strong&gt;$4 per liter&lt;/strong&gt;. The distance and effort didn't change, only the price per unit of fuel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Fee (The Total Bill)&lt;/strong&gt;: The final payment calculated from the fuel and the price per liter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that picture in mind, let's briefly look at how the blockchain measures those "liters of fuel" under the hood.&lt;/p&gt;
&lt;h2&gt;
  
  
  Under the Hood: How Does the Blockchain Count Gas?
&lt;/h2&gt;

&lt;p&gt;When you submit a transaction on an EVM-compatible blockchain, the network doesn't guess how hard your transaction worked. Instead, every tiny instruction the computer executes--called an &lt;strong&gt;opcode&lt;/strong&gt;--has a fixed cost measured in gas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Base Cover Charge (&lt;code&gt;21,000&lt;/code&gt; Intrinsic Gas)&lt;/strong&gt;: Before the blockchain even opens a smart contract, every standard transaction costs a flat minimum of &lt;strong&gt;21,000 gas&lt;/strong&gt;. This covers the basic work of verifying your cryptographic signature, checking your account nonce, and updating your account balance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic Math &amp;amp; Memory&lt;/strong&gt;: Inside a smart contract, simple operations like adding two numbers together (&lt;code&gt;ADD&lt;/code&gt;) cost just &lt;strong&gt;3 gas&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading &amp;amp; Writing Permanent Storage&lt;/strong&gt;: Updating the blockchain's permanent database is the heaviest work a transaction can do, because every node on the network has to store that state change. Reading a variable from permanent storage for the first time in a transaction (&lt;code&gt;SLOAD&lt;/code&gt;) costs &lt;strong&gt;2,100 gas&lt;/strong&gt;. Writing a brand-new non-zero value into an empty storage slot (&lt;code&gt;SSTORE&lt;/code&gt; from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt;) costs &lt;strong&gt;20,000 gas&lt;/strong&gt;, while updating an already non-zero slot (&lt;code&gt;SSTORE&lt;/code&gt; from &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;) costs &lt;strong&gt;2,900 gas&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As your transaction runs, an internal meter starts at &lt;code&gt;21,000&lt;/code&gt; and adds up the exact cost of every instruction executed along the way.&lt;/p&gt;

&lt;p&gt;Now let's see how this looks on two real transaction receipts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Case Study One: Calling &lt;code&gt;increment()&lt;/code&gt; on Monad Testnet
&lt;/h2&gt;

&lt;p&gt;For our first case study, I interacted directly with a smart contract deployed on Monad Testnet by calling its &lt;code&gt;increment()&lt;/code&gt; function.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkaog0dvi3lq3dmltmzd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkaog0dvi3lq3dmltmzd.png" alt="Monad Transaction hash receipt" width="799" height="419"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2oaaudb9nvxktzhfirf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2oaaudb9nvxktzhfirf.png" alt="Monad Transaction hash receipt" width="799" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the exact details from the transaction receipt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Monad Testnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function Called&lt;/td&gt;
&lt;td&gt;&lt;code&gt;increment()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Status&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Value&lt;/td&gt;
&lt;td&gt;0 MON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Limit&lt;/td&gt;
&lt;td&gt;49,785&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Used (as reported)&lt;/td&gt;
&lt;td&gt;49,785&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Usage&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Price&lt;/td&gt;
&lt;td&gt;102.82 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Fee&lt;/td&gt;
&lt;td&gt;0.0051188937 MON&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why Did a &lt;code&gt;0 MON&lt;/code&gt; Transaction Still Incur a Fee?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Notice that the &lt;strong&gt;Transaction value&lt;/strong&gt; is &lt;code&gt;0 MON&lt;/code&gt;. My transaction didn't send any MON tokens to another person or contract, yet it still charged a fee of &lt;code&gt;0.0051188937 MON&lt;/code&gt;. Why?   &lt;/p&gt;

&lt;p&gt;In our delivery truck analogy, &lt;strong&gt;Transaction value&lt;/strong&gt; is the cash you put inside an envelope to send to someone across town. Even if you send the driver with &lt;code&gt;$0&lt;/code&gt; in the envelope just to flip a switch at the warehouse, the truck still has to burn fuel to drive there and flip the switch.&lt;/p&gt;

&lt;p&gt;Look at the Solidity contract behind &lt;code&gt;increment()&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;

contract Counter {
    uint256 public count;

    function increment() external {
        count = count + 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This contract stores a number called &lt;code&gt;count&lt;/code&gt; on the blockchain. Whenever someone calls &lt;code&gt;increment()&lt;/code&gt;, the contract increases &lt;code&gt;count&lt;/code&gt; by one (changing it from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt;, or &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;). Even though the code looks tiny, the blockchain still has to process the transaction, run the contract's instructions, read the current value of &lt;code&gt;count&lt;/code&gt;, and write the updated number into permanent storage.&lt;/p&gt;

&lt;p&gt;The transaction fee pays for that computational processing and state update, not for transferring currency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Was the Fee Calculated--And Why Was Gas Usage 100%?&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2oaaudb9nvxktzhfirf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2oaaudb9nvxktzhfirf.png" alt="Monad Transaction hash receipt" width="799" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On this receipt, the &lt;strong&gt;Gas limit&lt;/strong&gt; was &lt;code&gt;49,785&lt;/code&gt;, and the &lt;strong&gt;Gas used&lt;/strong&gt;, as reported was also &lt;strong&gt;49,785&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;Gas Usage Percentage = (49,785 ÷ 49,785) × 100 = 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if we multiply the Gas limit (&lt;code&gt;49,785&lt;/code&gt;) by the Gas price (&lt;code&gt;102.82 Gwei&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;$49,785 x 102.82 Gwei = 5,118,893.7 Gwei
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the conversion of $10⁹ Gwei per &lt;code&gt;1 MON&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;Transaction Fee = 5,118,893.7 ÷ 10⁹ = 0.0051188937 MON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matches the displayed transaction fee down to the last digit.&lt;/p&gt;

&lt;p&gt;But this raises an important question: &lt;strong&gt;Why does Monad calculate the fee using the &lt;code&gt;Gas Limit&lt;/code&gt;, and why did the receipt report 100% gas usage?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike Ethereum, which charges you based on the gas your transaction actually used after running, &lt;strong&gt;Monad charges based on the &lt;code&gt;Gas Limit&lt;/code&gt; you declare upfront&lt;/strong&gt; (and sets gasUsed equal to &lt;code&gt;gasLimit&lt;/code&gt; on the top-level receipt).&lt;/p&gt;

&lt;p&gt;To understand why, compare &lt;strong&gt;a supermarket checkout (Ethereum)&lt;/strong&gt; with &lt;strong&gt;booking cargo on an airplane (Monad)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum (Supermarket Checkout)&lt;/strong&gt;: When an Ethereum validator builds a block, it runs each user's transaction on the spot before picking the next one. Because it runs the code immediately, it sees the exact &lt;code&gt;Gas Used&lt;/code&gt; right away and knows exactly how much space is left in the block for the next person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monad (Booking Cargo on a Flight)&lt;/strong&gt;: To process thousands of transactions per second, Monad uses asynchronous execution: validators pack transactions into a block and lock in the order before running the smart contract code. Because the network hasn't run your code yet when packing the block, it doesn't know what your actual &lt;code&gt;Gas Used&lt;/code&gt; will be--it only sees the &lt;code&gt;Gas Limit&lt;/code&gt; you asked to reserve!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Monad let users reserve massive amounts of block space using a huge &lt;code&gt;Gas Limit&lt;/code&gt; and then only charged them for a tiny &lt;code&gt;Gas Used&lt;/code&gt;afterward, someone could reserve an entire block, crowd out everyone else on the network, and pay almost nothing. Just like reserving 10 seats on a bus or cargo flight where other passengers have to be turned away at the gate, on Monad you pay for the exact capacity you reserve upfront (&lt;code&gt;Gas Limit&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study Two: Inspecting a Transaction on Ethereum Sepolia Testnet
&lt;/h2&gt;

&lt;p&gt;To see how standard Ethereum fee accounting differs, I went to the &lt;strong&gt;Ethereum Sepolia Testnet&lt;/strong&gt; block explorer and pulled up a random transaction hash to inspect its receipt. In this transaction, the sender called the &lt;code&gt;close()&lt;/code&gt; function on a 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frwdvxbyxaxymfob5lg7h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frwdvxbyxaxymfob5lg7h.png" alt="Ethereum Transaction hash receipt" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the transaction details:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Ethereum Sepolia Testnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function Called&lt;/td&gt;
&lt;td&gt;&lt;code&gt;close()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Status&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Value&lt;/td&gt;
&lt;td&gt;0 ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Limit&lt;/td&gt;
&lt;td&gt;156,059&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Used&lt;/td&gt;
&lt;td&gt;152,492&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Usage&lt;/td&gt;
&lt;td&gt;97.71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective Gas Price (as reported)&lt;/td&gt;
&lt;td&gt;0.000153611 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Fee&lt;/td&gt;
&lt;td&gt;0.000000023424448612 ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Gas Limit vs. Gas Used (And What Happens to Unused Gas)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just like the Monad transaction, this transaction sent &lt;code&gt;0 ETH&lt;/code&gt; in value while paying a fee to execute smart contract logic. However, look at the difference between the limit and the actual consumption.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gas limit: &lt;code&gt;156,059&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gas used: &lt;code&gt;152,49&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;156,059 - 152,492 = 3,567 unused gas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Percentage Consumed = (152,492 ÷ 156,059) × 100 ≈ 97.71%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the transaction consumed &lt;code&gt;97.71%&lt;/code&gt; of its limit and left &lt;code&gt;3,567&lt;/code&gt; gas (&lt;code&gt;2.29%&lt;/code&gt;) untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to that &lt;code&gt;3,567&lt;/code&gt; unused gas on Ethereum?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a transaction begins on Ethereum, the protocol temporarily locks enough ETH from the sender's balance to cover the entire &lt;code&gt;156,059 Gas Limit&lt;/code&gt; (just like a fuel pump pre-authorizing your card before you pump). As the code executes, the meter ticks up to &lt;code&gt;152,492&lt;/code&gt;. The moment execution finishes, the leftover &lt;code&gt;3,567&lt;/code&gt; gas is &lt;strong&gt;immediately refunded&lt;/strong&gt; back to the sender's wallet inside the exact same transaction, and that unused capacity is freed up for other transactions in the block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the Sepolia Transaction Fee Was Calculated&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because Ethereum refunds unused gas, the final fee is calculated strictly on &lt;strong&gt;Gas used&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;Transaction fee = Gas used x Effective gas price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiplying the &lt;code&gt;152,492&lt;/code&gt; gas actually consumed by the &lt;code&gt;0.000153611 Gwei&lt;/code&gt; effective gas price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;152,492 x 0.000153611 Gwei = 23.424448612 Gwei
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since 1 ETH = 10⁹ Gwei:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fee = 23.424448612 ÷ 10⁹ = 0.000000023424448612 ETH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the unused &lt;code&gt;3,567&lt;/code&gt; gas was never charged to the sender.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Know What Gas Limit to Set?
&lt;/h2&gt;

&lt;p&gt;Looking at numbers like &lt;code&gt;49,785&lt;/code&gt; on Monad and &lt;code&gt;156,059&lt;/code&gt; on Sepolia, a natural question comes up: &lt;strong&gt;How does anyone know how much gas a contract will need before sending the transaction?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't have to calculate opcodes manually. In practice, gas limits are estimated automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wallet &amp;amp; RPC Simulation (&lt;code&gt;eth_estimateGas&lt;/code&gt;)&lt;/strong&gt;: Before you click "Confirm" in your wallet, the wallet silently calls a method on an RPC node called &lt;code&gt;eth_estimateGas&lt;/code&gt;. The node runs a &lt;strong&gt;free practice simulation (dry run)&lt;/strong&gt; of your transaction against the current state of the blockchain in its local memory, counts the exact gas used, and reports that number back. Your wallet then adds a small safety buffer (often 10% to 20%) in case the blockchain state changes slightly before your transaction lands in a block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Tooling (Foundry)&lt;/strong&gt;: When writing smart contracts in Foundry, running &lt;code&gt;forge test --gas-report&lt;/code&gt; or &lt;code&gt;cast estimate&lt;/code&gt; simulates your function calls and prints out the exact gas numbers before you ever deploy to a live network.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Practical takeaway: Because Monad charges you for your declared &lt;code&gt;Gas Limit&lt;/code&gt;, you should rely on &lt;code&gt;eth_estimateGas&lt;/code&gt; with a modest buffer rather than manually typing in an artificially huge gas limit!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Understanding Gas Price and Gwei
&lt;/h2&gt;

&lt;p&gt;Now that we've covered &lt;code&gt;Gas Limit&lt;/code&gt; and &lt;code&gt;Gas Used&lt;/code&gt;, let's look at &lt;strong&gt;Gas Price&lt;/strong&gt; and &lt;strong&gt;Gwei&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While gas measures the computational effort (liters of fuel), &lt;strong&gt;gas price&lt;/strong&gt; is the monetary cost per single unit of gas (price per liter). Because a single unit of gas costs a tiny fraction of &lt;code&gt;1 ETH&lt;/code&gt; or &lt;code&gt;1 MON&lt;/code&gt;, we express gas prices in &lt;strong&gt;Gwei&lt;/strong&gt; (short for &lt;em&gt;Giga-wei&lt;/em&gt;) so we don't have to read strings of nine zeros:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 ETH (or MON) = 10⁹ Gwei = 1,000,000,000 Gwei
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 Gwei = 0.000000001 ETH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone says &lt;em&gt;"Gas price is 20 Gwei,"&lt;/em&gt; they don't mean the whole transaction costs 20 Gwei. They mean &lt;strong&gt;each single unit of gas costs 20 billionths of an ETH&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suppose a transaction consumes &lt;code&gt;50,000&lt;/code&gt; gas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At &lt;strong&gt;10 Gwei&lt;/strong&gt; per gas: &lt;strong&gt;50,000 x 10 Gwei = 500,000 Gwei&lt;/strong&gt; (&lt;strong&gt;0.0005 ETH&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;At &lt;strong&gt;20 Gwei&lt;/strong&gt; per gas: &lt;strong&gt;50,000 x 20 Gwei = 1,000,000 Gwei&lt;/strong&gt; (&lt;code&gt;0.001 ETH&lt;/code&gt;). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that the computational work (&lt;code&gt;50,000&lt;/code&gt; gas) stayed completely identical--the fee doubled purely because network demand pushed the price per unit of gas twice as high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding EIP-1559: Base Fee, Priority Fee, and Max Fee
&lt;/h2&gt;

&lt;p&gt;Both the Monad and Sepolia receipts identify the transaction type as &lt;strong&gt;EIP-1559 (Type 2)&lt;/strong&gt;. Under EIP-1559, the gas price is broken down into specific components. Let's look at the fee breakdown from the Sepolia receipt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fee Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Ethereum Sepolia Testnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base Fee&lt;/td&gt;
&lt;td&gt;0.000000015 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max Priority Fee&lt;/td&gt;
&lt;td&gt;0.000153596 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max Fee&lt;/td&gt;
&lt;td&gt;0.000153614 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective Gas Price&lt;/td&gt;
&lt;td&gt;0.000153611 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is how these four numbers fit together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Base fee (&lt;code&gt;0.000000015 Gwei&lt;/code&gt;)&lt;/strong&gt;: The mandatory minimum rate per unit of gas for that block, determined automatically by the network based on how full previous blocks were. On Ethereum, the base fee is &lt;strong&gt;burned&lt;/strong&gt; (destroyed) rather than paid to the block proposer.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max priority fee (&lt;code&gt;0.000153596 Gwei&lt;/code&gt;)&lt;/strong&gt;: The maximum tip per unit of gas the sender offers directly to the block proposer (validator) to incentivize them to include the transaction. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max fee (&lt;code&gt;0.000153614 Gwei&lt;/code&gt;)&lt;/strong&gt;: The absolute ceiling per unit of gas the sender is willing to pay in total (&lt;code&gt;Base fee + Priority fee&lt;/code&gt;) in case the base fee jumps while the transaction is waiting.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effective gas price (&lt;code&gt;0.000153611 Gwei&lt;/code&gt;)&lt;/strong&gt;: The actual rate per unit of gas charged when the block was produced:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Effective gas price = Base fee + Effective priority fee
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.000000015 Gwei + 0.000153596 Gwei = 0.000153611 Gwei
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;0.000153611 Gwei&lt;/code&gt; was safely below the sender's &lt;code&gt;Max fee&lt;/code&gt; cap of &lt;code&gt;0.000153614 Gwei&lt;/code&gt;, the transaction paid the exact sum of the base fee and priority tip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do Transactions Need a Gas Limit? (And Why Gas Costs Change)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Preventing Runaway Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why does every blockchain transaction require a &lt;code&gt;Gas Limit&lt;/code&gt; in the first place? Consider a contract function with a massive loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function expensiveOperation() external {
    uint256 total;

    for (uint256 i = 0; i &amp;lt; 1_000_000; i++) {
        total += i;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a transaction could execute without an upper bound--or accidentally hit an infinite loop--every node on the network would get stuck executing that loop forever, halting the blockchain.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Gas Limit&lt;/strong&gt; acts as a circuit breaker. If a transaction burns through all of its allotted gas before finishing, it stops with an &lt;strong&gt;Out of Gas&lt;/strong&gt; error. When that happens, all state changes are reverted as if the transaction never happened, &lt;strong&gt;but the sender still pays for the gas consumed&lt;/strong&gt; because the network already spent real computational work running the code up to that point.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep in mind: having plenty of gas does not guarantee a transaction will succeed. If a contract fails an internal &lt;code&gt;require&lt;/code&gt; check or permission rule, the transaction will still revert even with gas left over.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why Two Calls to the Same Function Can Use Different Gas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is also easy to assume that calling the same function with the same inputs will always consume the exact same amount of gas. In practice, gas usage depends on the exact state of the contract at the moment of execution.&lt;/p&gt;

&lt;p&gt;For example, in our &lt;code&gt;Counter&lt;/code&gt; contract, the very first call to &lt;code&gt;increment()&lt;/code&gt; updates &lt;code&gt;count&lt;/code&gt; from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt;. Allocating a brand-new storage slot from zero to a non-zero number costs &lt;strong&gt;20,000 gas&lt;/strong&gt;. The next call updates count from &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;--and overwriting an already non-zero storage slot costs &lt;strong&gt;2,900 gas&lt;/strong&gt;. Even though both users called &lt;code&gt;increment()&lt;/code&gt;, the first call performed heavier database work under the network's rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the Two Receipts: Are Fees on Monad Expensive?
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2oaaudb9nvxktzhfirf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq2oaaudb9nvxktzhfirf.png" alt="Monad Transaction hash receipt" width="799" height="427"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frwdvxbyxaxymfob5lg7h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frwdvxbyxaxymfob5lg7h.png" alt="Ethereum Transaction hash receipt" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's put our Monad Testnet interaction and the Sepolia Testnet receipt side by side&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Monad Testnet: &lt;code&gt;increment()&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Sepolia Testnet: &lt;code&gt;close()&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gas Limit&lt;/td&gt;
&lt;td&gt;49,785&lt;/td&gt;
&lt;td&gt;156,059&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Used&lt;/td&gt;
&lt;td&gt;49,785&lt;/td&gt;
&lt;td&gt;152,492&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas Usage&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;97.71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective Gas Price (as reported)&lt;/td&gt;
&lt;td&gt;102.82 Gwei&lt;/td&gt;
&lt;td&gt;0.000153611 Gwei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction Fee&lt;/td&gt;
&lt;td&gt;0.0051188937 MON&lt;/td&gt;
&lt;td&gt;0.000000023424448612 ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fee Calculation&lt;/td&gt;
&lt;td&gt;Gas Used x Gas Price&lt;/td&gt;
&lt;td&gt;Gas Used x Effective Gas Price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you look at this table, you might notice that &lt;code&gt;102.82 Gwei&lt;/code&gt; (&lt;code&gt;0.0051 MON&lt;/code&gt;) looks numerically much larger than &lt;code&gt;0.000153611 Gwei&lt;/code&gt; (&lt;code&gt;0.0000000234 ETH&lt;/code&gt;). Does that mean gas fees on Monad are expensive?&lt;/p&gt;

&lt;p&gt;Not at all. In real-world dollar terms, Monad is designed so transactions cost a fraction of a cent. Those raw numbers cannot be compared directly for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Different Native Tokens (&lt;code&gt;MON-Gwei&lt;/code&gt; vs. &lt;code&gt;ETH-Gwei&lt;/code&gt;)&lt;/strong&gt;: &lt;code&gt;1 Gwei&lt;/code&gt; on Ethereum is one-billionth of &lt;code&gt;1 ETH&lt;/code&gt; (an asset priced in thousands of dollars), whereas &lt;code&gt;1 Gwei&lt;/code&gt; on Monad is one-billionth of &lt;code&gt;1 MON&lt;/code&gt;. Comparing &lt;code&gt;0.005 MON&lt;/code&gt; to &lt;code&gt;0.00000002 ETH&lt;/code&gt; is like comparing &lt;code&gt;50 Naira&lt;/code&gt; (or 5 Japanese Yen) to &lt;code&gt;0.03 US Dollars&lt;/code&gt;--a bigger number in one currency doesn't mean a higher cost!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monad's &lt;code&gt;100 Gwei&lt;/code&gt; Minimum Base Fee Floor&lt;/strong&gt;: To prevent spam when blocks aren't full, Monad enforces a minimum base fee of &lt;code&gt;100 MON-gwei&lt;/code&gt; (which is why our transaction was &lt;code&gt;102.82 Gwei&lt;/code&gt;: &lt;code&gt;100 Gwei&lt;/code&gt; base fee + &lt;code&gt;2.82 Gwei&lt;/code&gt; priority tip). Sepolia Testnet has no 100 Gwei floor, so its testnet base fee had drifted down to nearly zero (&lt;code&gt;0.000000015 Gwei&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Misunderstandings About Gas (Quick Checklist)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gas and cryptocurrency are the same thing&lt;/strong&gt;. They are not. Gas is a measurement of computational work; ETH and MON are the native assets used to pay for that work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gas limit is the exact amount of gas a transaction will use&lt;/strong&gt;. Not necessarily. The gas limit is the maximum allowance; actual execution often consumes less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A transaction with &lt;code&gt;0&lt;/code&gt; value is free&lt;/strong&gt;. Incorrect. Both of our transactions had a value of &lt;code&gt;0&lt;/code&gt;, and both required fees to pay for smart contract execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A higher gas limit always means a higher fee&lt;/strong&gt;. It depends on the network's architecture! On Ethereum, you are only billed for &lt;code&gt;Gas Used&lt;/code&gt;, and unused gas is refunded. On Monad, because blocks are packed asynchronously before execution, you are billed for the entire &lt;code&gt;Gas Limit&lt;/code&gt; you reserve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enough gas guarantees a transaction will succeed&lt;/strong&gt;. Incorrect. Even with plenty of gas, a transaction will revert if it hits a failed contract condition or permission check. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Explain Gas to Someone Else
&lt;/h2&gt;

&lt;p&gt;If someone asks you, &lt;em&gt;"What is gas on the blockchain, and how do fees work?"&lt;/em&gt;, here is how you can break it down cleanly&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gas is the unit that measures how much computational work a transaction requires--just like liters of fuel needed for a delivery truck to complete a trip.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every transaction sets a &lt;strong&gt;Gas Limit&lt;/strong&gt; (calculated automatically by your wallet simulating the transaction), which acts as a maximum fuel cap so a bad loop can't run forever. As the transaction runs, it tallies up the &lt;strong&gt;Gas Used&lt;/strong&gt; based on the exact instructions executed.&lt;/p&gt;

&lt;p&gt;To find the &lt;strong&gt;Transaction Fee&lt;/strong&gt;, you multiply the gas quantity by the Gas Price (measured in &lt;strong&gt;Gwei&lt;/strong&gt;). On Ethereum, you only pay for the fuel your code actually burned (&lt;code&gt;Gas Used&lt;/code&gt;), and any unused gas is refunded immediately. On high-speed asynchronous networks like Monad, where block space is reserved before running the code, you pay for the entire fuel limit you reserved upfront (&lt;code&gt;Gas Limit&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Once you separate the unit of work, the maximum allowance, the actual consumption, and the price per unit, any transaction receipt on any EVM block explorer becomes intuitive to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Figure Out the Rest Together
&lt;/h2&gt;

&lt;p&gt;Writing this guide started with a simple realization: it is completely normal to use blockchains, deploy contracts, and still feel a bit fuzzy on what every single line of a transaction receipt is actually telling you. Moving from memorizing definitions to tracing the exact numbers is how the mental model finally clicks.&lt;/p&gt;

&lt;p&gt;Now I'd love to hear from you: &lt;strong&gt;What part of Gas still feels unclear, confusing, or tricky to explain?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is there a field on a block explorer receipt that you've always wondered about?&lt;/li&gt;
&lt;li&gt;Have you ever run into an unexpected &lt;code&gt;Out of Gas&lt;/code&gt; error or a strange gas estimation spike while testing a contract?&lt;/li&gt;
&lt;li&gt;Or do you have a favorite real-world analogy of your own for explaining gas, &lt;code&gt;Gwei&lt;/code&gt;, or &lt;code&gt;EIP-1559&lt;/code&gt; to someone new?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop your questions, edge cases, or thoughts in the comments below--no question is too basic or too far down the rabbit hole. Let's unpack them together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.binnadev.com" rel="noopener noreferrer"&gt;BinnaDevLab&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Reasoning Precedes Coding.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>solidity</category>
      <category>web3</category>
    </item>
    <item>
      <title>What the DAO Hack, Parity Freeze and Ronin Exploit Teach Us About Immutability</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:09:01 +0000</pubDate>
      <link>https://dev.to/binnadev/what-the-dao-hack-parity-freeze-and-ronin-exploit-teach-us-about-immutability-4p9e</link>
      <guid>https://dev.to/binnadev/what-the-dao-hack-parity-freeze-and-ronin-exploit-teach-us-about-immutability-4p9e</guid>
      <description>&lt;p&gt;In 2016, an attacker discovered a bug in a smart contract called the DAO.&lt;/p&gt;

&lt;p&gt;He didn't need to compromise Ethereum. He didn't need a zero-day exploit. He found one bad assumption in the withdrawal logic, wrote a contract that exploited it recursively, and drained $60 million. Every node on the Ethereum network confirmed every transaction. The blockchain did its job perfectly.&lt;/p&gt;

&lt;p&gt;That's the thing about immutable code. It can't lie. But it also means it can't be fixed.&lt;/p&gt;

&lt;p&gt;I've been sitting with this idea since I hosted Session 3 of BinnaDevLab Cohort 1. I invited Uchenna Ofoma, a software engineer of 3+ years currently focused on the distributed systems that underpin blockchain technology, to walk us through the full engineering reality of deploying code you can never take back. What followed was one of the most honest breakdowns of Web3 security I've heard.&lt;/p&gt;

&lt;p&gt;If you'd rather watch than read, here's the full 42-minute session:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/UWpmbW5uU0w" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Deploying" Actually Means
&lt;/h2&gt;

&lt;p&gt;I opened the session by framing the core question: what does it actually mean that a smart contract is immutable?&lt;/p&gt;

&lt;p&gt;Most developers have an intuition for deployment -- you push code, it runs on a server, the server can be updated or rolled back. Uchenna immediately challenged that mental model.&lt;/p&gt;

&lt;p&gt;When you deploy a smart contract, your code gets compiled to EVM bytecode -- an intermediate representation designed specifically to run on the &lt;a href="https://ethereum.org/developers/docs/evm" rel="noopener noreferrer"&gt;Ethereum Virtual Machine&lt;/a&gt;. From that moment, your code gets a permanent address. Every node on the network holds a copy and can execute it. Nobody owns the server. There is no centralized server.&lt;/p&gt;

&lt;p&gt;As Uchenna put it: "Once you deploy your smart contract, just take it that it's online forever or until the blockchain inevitably goes down someday, somehow."&lt;/p&gt;

&lt;p&gt;The reason for this is deterministic execution. The EVM guarantees that given the same inputs, every node produces the same output. This is what makes decentralization possible: no one node needs to be trusted, because they all agree on the math. But it also means your bug is their bug permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Kinds of Trust
&lt;/h2&gt;

&lt;p&gt;Before Uchenna walked us through what goes wrong, he established something I thought was fundamental and it connects directly back to what we covered in Session 1.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If the owner can go to edit the rules, you're back to trusting a company, which is what we're trying to avoid."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The whole promise of a smart contract is that you're replacing trust in a person with trust in code. Anyone can audit it. Anyone can verify it. It runs without asking permission. But the moment you give an admin the ability to change the code, you've reintroduced a human and with it, all of human failure.&lt;/p&gt;

&lt;p&gt;That's the foundation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A contract you can edit is not an agreement -- it's a promise."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When Uchenna said that, I wrote it down immediately. It cuts straight to why any of this matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Forever Goes Wrong
&lt;/h2&gt;

&lt;p&gt;This is the section of the session I wasn't fully prepared for. I knew Uchenna was going to cover the major hacks. I didn't expect the specific engineering detail he brought to each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DAO $60 million Hack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The DAO was a decentralised fund on Ethereum. Members could pool ETH and vote on how to allocate it. A smart contract managed all the funds.&lt;/p&gt;

&lt;p&gt;There was a bug, a re-entrancy bug. Uchenna explained it in a way that made it immediately understandable:&lt;/p&gt;

&lt;p&gt;The contract let you withdraw your balance. But before it updated your balance, it made an external call to send you the ETH. An attacker wrote a contract that, the moment it received ETH from the withdrawal, immediately called the withdrawal function again before the balance had been marked as zero. Then again. And again.&lt;/p&gt;

&lt;p&gt;He kept entering the vault before it could record that he'd left.&lt;/p&gt;

&lt;p&gt;By the end, he had drained approximately $60 million. The code didn't fail. No exception was thrown. The EVM executed every call exactly as instructed.&lt;/p&gt;

&lt;p&gt;The fix for this pattern is called &lt;strong&gt;Checks-Effects-Interactions&lt;/strong&gt;: update all your state variables before you make any external calls. Mark the balance as zero first. Then send the money. If someone tries to re-enter, they find an empty account.&lt;/p&gt;

&lt;p&gt;The aftermath is one of the most significant moments in blockchain history. The Ethereum community forked the chain -- creating a copy up to just before the hack -- so that affected users could get their money back. The old chain, where the attacker kept his funds, became what we now know as &lt;a href="https://en.wikipedia.org/wiki/Ethereum_Classic" rel="noopener noreferrer"&gt;Ethereum Classic&lt;/a&gt;. The blockchain literally split in two over one bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Parity Wallet Freeze -- $280 million&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In 2017, Parity Technologies built a popular multi-signature wallet. Multiple signatures were required to move funds, no one person could act alone.&lt;/p&gt;

&lt;p&gt;The problem was architectural. Their wallets shared a single library contract. That library had an unguarded initialization function, no access control, no guard on who could call it.&lt;/p&gt;

&lt;p&gt;A user exploring the code called it. Then, exploring further, called the function to delete the library.&lt;/p&gt;

&lt;p&gt;$280 million was locked. Not stolen. Just permanently inaccessible, because the code that would authorise withdrawals no longer existed.&lt;/p&gt;

&lt;p&gt;The fix is access control: every state-changing function needs to verify who is calling it before allowing the action to proceed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ronin Sidechain Exploit -- $625 million&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ronin was the sidechain powering Axie Infinity. To validate transactions, it used nine validator keys. A majority, five of nine was required to approve any action.&lt;/p&gt;

&lt;p&gt;Attackers compromised five of them.&lt;/p&gt;

&lt;p&gt;Uchenna's analogy during the session stuck with me: "Half of nine is 4.5. They've compromised five. Five is more than 4.5."&lt;/p&gt;

&lt;p&gt;With majority control, the attackers could authorize anything. $625 million left. The code worked exactly as designed. It was just the wrong people holding the keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Engineers Live With It
&lt;/h2&gt;

&lt;p&gt;After walking through what failed, Uchenna shifted to what engineers do in response. The honest answer is that &lt;strong&gt;there are no perfect solutions, only trade-offs.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proxy Contracts:&lt;/strong&gt; You can deploy a "proxy" that users interact with, which delegates logic to an implementation contract behind the scenes. When you need to patch a bug, you deploy a new implementation and point the proxy at it. The address never changes. The logic does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Locks:&lt;/strong&gt; Any upgrade or large fund movement gets queued with a delay like 72 hours. This gives the community time to notice if something suspicious is being pushed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-sig wallets:&lt;/strong&gt; No single person can push the button. A 3-of-5 multi-sig requires three separate private keys to sign any transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party audits and bug bounties:&lt;/strong&gt; Independent firms review your code. Or you open a bounty for anyone to report a vulnerability, get paid. The goal is to maximize the number of friendly eyes before deployment, because after deployment, every eye is potentially hostile.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Every Escape Hatch Is Also an Attack Hatch
&lt;/h2&gt;

&lt;p&gt;This was the moment of the session that landed hardest.&lt;/p&gt;

&lt;p&gt;Every mechanism you build to survive a bug is also a mechanism an attacker can use against you.&lt;/p&gt;

&lt;p&gt;Proxy contracts? An attacker who controls the admin key can push a malicious implementation.&lt;/p&gt;

&lt;p&gt;Time locks? The attacker now has 72 hours to prepare not just a few seconds.&lt;/p&gt;

&lt;p&gt;Uchenna's framing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"An immutable bug is honest. An upgradeable protocol is a promise with a delay."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An immutable contract is what it is. Its failure mode is fixed and visible. An upgradeable contract introduces ongoing risk: the humans holding the upgrade keys, forever. If those humans are ever compromised or ever act against users' interests, the upgrade path becomes the exploit.&lt;/p&gt;

&lt;p&gt;The engineer's job, as Uchenna put it, is to decide which failure they can live with.&lt;/p&gt;

&lt;h2&gt;
  
  
  DYOR Is Not Just a Meme
&lt;/h2&gt;

&lt;p&gt;I closed the session with something I feel strongly about, and I want to say it directly here too.&lt;/p&gt;

&lt;p&gt;DYOR (Do Your Own Research), gets thrown around in crypto circles as a joke or a liability disclaimer. But after what Uchenna walked us through, it's the only honest answer to the immutability problem.&lt;/p&gt;

&lt;p&gt;If you're building: audit your code, run open-source tools against it, get third-party reviews, and be honest about what your protocol assumes. Know what your upgrade path actually exposes.&lt;/p&gt;

&lt;p&gt;If you're using: don't trust a protocol because the team told you to. Find the audit report. Look at who holds the keys. Understand whether the thing you're interacting with is truly immutable or whether there's an admin somewhere with a button.&lt;/p&gt;

&lt;p&gt;The code can't lie. But you need to be able to read it.&lt;/p&gt;

&lt;p&gt;We're running Cohort 1 of BinnaDevLab: a structured developer program built on reasoning from first principles. Session 3 was the theory. Session 4 is the practical: actually writing smart contracts and experiencing immutability firsthand.&lt;/p&gt;

&lt;p&gt;Missed Session 1 on trust and decentralization? Watch it here.&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/MTpR9-JL1ig" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Follow along on &lt;a href="https://www.youtube.com/@BinnaDevLab" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; or join the community at &lt;a href="https://discord.gg/KFxVtAQpA" rel="noopener noreferrer"&gt;binnadev.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>smartcontract</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Blockchain Isn't About Crypto. It's About a Trust Problem You Didn't Know You Had.</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Wed, 07 Oct 2026 10:28:13 +0000</pubDate>
      <link>https://dev.to/binnadev/blockchain-isnt-about-crypto-its-about-a-trust-problem-you-didnt-know-you-had-3m81</link>
      <guid>https://dev.to/binnadev/blockchain-isnt-about-crypto-its-about-a-trust-problem-you-didnt-know-you-had-3m81</guid>
      <description>&lt;p&gt;Before we started the first session of BinnaDevLab Cohorts, I knew one thing with certainty:&lt;/p&gt;

&lt;p&gt;If we opened with the word "blockchain," we'd lose half of the room.&lt;/p&gt;

&lt;p&gt;Not because the topic is hard. Because the word carries baggage. Crypto speculation. NFT mania. Headlines about scams and volatility. By the time most developers hear "blockchain," they've already formed an opinion and it's usually dismissive.&lt;/p&gt;

&lt;p&gt;So we didn't start there.&lt;/p&gt;

&lt;p&gt;We started with a question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Think of an app you used this morning. Name it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the warm-up exercise Uchenna Ofoma, our facilitator for Session 1 put to the cohort.&lt;/p&gt;

&lt;p&gt;The answers came quickly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Opay. WhatsApp. Gmail. Facebook. Sporty.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Uchenna nodded at each one. Then he asked the follow-up question. The one that changed everything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who were you actually trusting?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The room went quiet.&lt;/p&gt;

&lt;p&gt;Not the app. Not the company. When you send ₦5,000 on Opay, you're trusting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opay's servers to record the transaction accurately&lt;/li&gt;
&lt;li&gt;The commercial bank Opay connects to&lt;/li&gt;
&lt;li&gt;NIBSS to clear the interbank transfer&lt;/li&gt;
&lt;li&gt;The KYC provider that verified both account holders&lt;/li&gt;
&lt;li&gt;The CBN regulatory framework sitting beneath all of it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You weren't trusting one party. You were trusting a &lt;strong&gt;chain&lt;/strong&gt; and you had zero visibility into any link of it.&lt;/p&gt;

&lt;p&gt;That's not a crypto problem. That's not a web3 problem. That's a software engineering problem that exists in every application you've ever built or used.&lt;/p&gt;

&lt;p&gt;And that is the real reason blockchain was invented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Built BinnaDevLab Around This
&lt;/h2&gt;

&lt;p&gt;When I started BinnaDevLab Cohorts, the goal was simple but specific: train developers to reason at a global level not just to write code, but to understand why systems are designed the way they are.&lt;/p&gt;

&lt;p&gt;The industry is full of bootcamps that teach you to ship fast. We wanted to build something different. A program built on the principle that &lt;strong&gt;reasoning comes before coding&lt;/strong&gt;. That understanding the problem deeply is the actual skill, the code is just the output.&lt;/p&gt;

&lt;p&gt;Blockchain was the perfect first topic. Not because it's trendy. Because it sits at the intersection of distributed systems, game theory, cryptography, and trust and most introductions to it skip all of that in favour of price charts.&lt;/p&gt;

&lt;p&gt;We wanted to go the other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happens When Trust Breaks
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;🎬 &lt;strong&gt;Prefer to watch?&lt;/strong&gt; The full 64-minute session is on YouTube&lt;br&gt;
&lt;a href="https://youtu.be/MTpR9-JL1ig" rel="noopener noreferrer"&gt;Blockchain Fundamentals Explained | BinnaDevLab Cohort 1, Session 1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Uchenna spent the core of Session 1 on a deceptively simple question: where does the trust network fail?&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;single points of failure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every link in that chain we described: Opay, the bank, NIBSS, the KYC provider is a centralised entity. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One server goes down -&amp;gt; transactions fail&lt;/li&gt;
&lt;li&gt;One database is compromised -&amp;gt; your data is leaked&lt;/li&gt;
&lt;li&gt;One regulatory decision -&amp;gt; your account is frozen without warning&lt;/li&gt;
&lt;li&gt;One bad actor inside the organisation -&amp;gt; the records are altered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You have no way to verify what's happening. You're trusting the institution to be honest, competent, and well-intentioned. Usually they are. But the system requires you to trust them, you have no alternative.&lt;/p&gt;

&lt;p&gt;Blockchain's argument is not that institutions are corrupt. It's that you shouldn't have to assume they're honest. The system should be designed so that anyone can verify the truth not just the people running the servers.&lt;/p&gt;

&lt;p&gt;In practice, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of rules locked on a private server -&amp;gt; rules deployed as code, in public&lt;/li&gt;
&lt;li&gt;Instead of promises -&amp;gt; anyone can read the code and check exactly what it does&lt;/li&gt;
&lt;li&gt;Instead of one keeper -&amp;gt; everyone keeps a copy, and no single party can rewrite history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not magic. That's engineering. And it comes with real costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Honest Trade-offs (Most Courses Skip This Part)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the things I'm proudest of in how Uchenna ran Session 1 is that he didn't sell blockchain. He explained it including the parts that hurt.&lt;/p&gt;

&lt;p&gt;Distributed verification is slower than a centralised server. Always. If you need instant transaction finality, a blockchain is the wrong tool.&lt;/p&gt;

&lt;p&gt;It's more expensive to run. Every node in the network has to process every transaction. That cost is real and it compounds at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finality works differently&lt;/strong&gt;. On your bank app, if you send money to the wrong person, you call customer service. On a public blockchain, the transaction is final. There is no customer service. The code runs.&lt;/p&gt;

&lt;p&gt;And perhaps the sharpest point: &lt;strong&gt;blockchain doesn't remove trust&lt;/strong&gt;. It relocates it. You stop trusting institutions and start trusting code, mathematics, and the incentive structures of a decentralised network. That's a different kind of trust, more auditable, more transparent, but trust nonetheless.&lt;/p&gt;

&lt;p&gt;Understanding those trade-offs is what separates engineers who can reason about distributed systems from developers who just know how to call a Web3 library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the Full Session&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything I've described above is the skeleton of a 64-minute live session with audience interaction, Q&amp;amp;A, and a cohort working through these ideas in real time.&lt;/p&gt;

&lt;p&gt;Uchenna also covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A brief history of blockchain (Bitcoin 2008 -&amp;gt; Ethereum 2015 -&amp;gt; today)&lt;/li&gt;
&lt;li&gt;Public vs. private blockchains and why each exists&lt;/li&gt;
&lt;li&gt;What Ethereum and the EVM actually are&lt;/li&gt;
&lt;li&gt;What people are building on Ethereum right now&lt;/li&gt;
&lt;li&gt;Consensus mechanisms: Proof-of-Work, Proof-of-Stake, Proof-of-History&lt;/li&gt;
&lt;li&gt;A live Q&amp;amp;A: does blockchain make systems trustless, or does it just shift the trust?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this article gave you the argument, the video gives you the full picture including the moments where the cohort pushed back and Uchenna had to defend his reasoning in real time.&lt;/p&gt;

&lt;p&gt;That's the kind of session we wanted to build. Not a lecture. A thinking session.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/MTpR9-JL1ig" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/MTpR9-JL1ig" rel="noopener noreferrer"&gt;Watch here 👉 Blockchain Fundamentals Explained | BinnaDevLab Cohort 1, Session 1&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Session 2 is hands-on. Obinna takes the cohort from theory into code writing and deploying a smart contract in Solidity, live.&lt;/p&gt;

&lt;p&gt;If Session 1 answers why blockchain exists, Session 2 answers how you build on it.&lt;/p&gt;

&lt;p&gt;Follow BinnaDevLab to catch it when it drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  About BinnaDevLab
&lt;/h2&gt;

&lt;p&gt;BinnaDevLab is a developer training community built around one idea: reason before you code.&lt;/p&gt;

&lt;p&gt;We run structured cohorts for developers who want to build with depth not just ship fast.&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://www.binnadev.com/cohorts" rel="noopener noreferrer"&gt;binnadev.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
      <category>explainlikeimfive</category>
    </item>
    <item>
      <title>ERC20 Edge Cases Every Smart Contract Engineer Should Know</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:49:00 +0000</pubDate>
      <link>https://dev.to/binnadev/erc20-edge-cases-every-smart-contract-engineer-should-know-2o8e</link>
      <guid>https://dev.to/binnadev/erc20-edge-cases-every-smart-contract-engineer-should-know-2o8e</guid>
      <description>&lt;p&gt;Your protocol launches. Everything passes the test suite with 100% coverage. Your audits are clean.&lt;/p&gt;

&lt;p&gt;Then, an unexpected token enters your liquidity pool. The accounting drifts by a fraction of a percent. Within hours, the discrepancy compounds. Users attempt to withdraw, and the transactions revert. The vault is permanently bricked, and funds are stuck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most ERC20 exploits are not actually bugs in the protocol's core logic. They happen because protocol engineers assume every token behaves exactly like a vanilla OpenZeppelin implementation.&lt;/p&gt;

&lt;p&gt;The ERC20 standard is an interface, not a guarantee. It defines function signatures (&lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;, &lt;code&gt;balanceOf&lt;/code&gt;), but it dictates absolutely nothing about the internal execution logic behind those signatures.&lt;/p&gt;

&lt;p&gt;If your protocol integrates arbitrary tokens, your threat model immediately includes the behavior of those tokens. A single flawed assumption about how a token transfers, reverts, or accounts for balances can lead to systemic insolvency.&lt;/p&gt;

&lt;p&gt;Let's look under the hood of the most common ERC20 edge cases that break production systems, and how to engineer defensive architectures to mitigate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Four Assumptions That Break Integrations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we look at specific edge cases, it helps to build a mental model of why these failures happen. Almost every ERC20 integration bug stems from one of four flawed assumptions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Amount Assumptions:&lt;/strong&gt; Believing the amount you request to transfer is the exact amount that will arrive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution Assumptions:&lt;/strong&gt; Assuming token transfers are passive state updates, rather than active execution environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Assumptions:&lt;/strong&gt; Believing that having a sufficient balance and allowance guarantees a transfer will succeed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting Assumptions:&lt;/strong&gt; Hardcoding precision logic or assuming token behavior is immutable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's break down how these assumptions fail in production.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fowwo7jcgncmlvp629298.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fowwo7jcgncmlvp629298.png" alt="A clean, technical 3D isometric diagram of a glowing smart contract "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #1: Fee-on-Transfer &amp;amp; Rebasing Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If I call &lt;code&gt;token.transferFrom(alice, vault, 100)&lt;/code&gt;, the vault's balance increases by exactly 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Tokens can deduct a tax on transfer (e.g., a 5% protocol fee) or continuously alter user balances algorithmically (rebasing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A user deposits 100 fee-on-transfer tokens. The token takes a 5% fee.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User calls deposit(100)
👇
Token executes transfer, deducts 5% tax
👇
Vault receives 95 tokens

❌ Vault internally mints 100 shares based on 'amount' parameter
✅ Vault should mint 95 shares based on actual received funds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the vault blindly minted 100 shares, the user can immediately withdraw 100 tokens, successfully stealing 5 tokens from previous depositors. The accounting drifts, and the last users to withdraw will be left with nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Never trust the &lt;code&gt;amount&lt;/code&gt; parameter for internal accounting. Always measure the actual balance delta before and after the transfer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceReceived = token.balanceOf(address(this)) - balanceBefore;

// Use balanceReceived for all subsequent logic, never 'amount'
_mintShares(msg.sender, balanceReceived);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Balance-delta accounting fixes this assumption. Unfortunately, the next edge case breaks protocols even when the balances match perfectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #2: Tokens with Callbacks (ERC-777 &amp;amp; Transfer Hooks)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Calling transfer only updates storage balances and returns execution flow directly back to my contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Standards like ERC-777, or custom tokens with transfer hooks, actively hand execution control back to the sender or receiver via external calls during the transfer process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol updates the user's internal debt balance after pushing tokens to them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User calls withdraw()
👇
Protocol calls token.transfer(user)
👇
Token calls back to User's smart contract
👇
User's contract calls withdraw() AGAIN (before protocol debt is updated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a classic reentrancy attack, facilitated entirely by the token's internal hook.&lt;/p&gt;

&lt;p&gt;Why This Surprises Engineers: We naturally view tokens as passive data ledgers. Callbacks turn tokens into active, hostile execution environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Follow the Checks-Effects-Interactions (CEI) pattern religiously. Update all internal state before interacting with external tokens. Additionally, apply OpenZeppelin's &lt;code&gt;ReentrancyGuard&lt;/code&gt; (&lt;code&gt;nonReentrant&lt;/code&gt; modifier) to any function interacting with external tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Reentrancy guards handle unexpected execution paths. But what happens when a token quietly fails without telling you?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #3: Returning &lt;code&gt;false&lt;/code&gt; vs. Reverting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If a token transfer fails, the transaction will revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some older tokens (like ZRX) do not revert on failure. Instead, they gracefully return a false boolean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol calls &lt;code&gt;token.transfer(user, amount)&lt;/code&gt;and doesn't check the return value. The transfer fails (returns &lt;code&gt;false&lt;/code&gt;), but your code continues executing. The protocol marks the user's debt as paid, even though no tokens actually moved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Never use bare &lt;code&gt;transfer&lt;/code&gt; or &lt;code&gt;transferFrom&lt;/code&gt; calls. Wrap them in logic that asserts the boolean return value is &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenZeppelin / best practice&lt;/strong&gt;&lt;br&gt;
Always use OpenZeppelin's &lt;code&gt;SafeERC20&lt;/code&gt; library (&lt;code&gt;safeTransfer&lt;/code&gt;, &lt;code&gt;safeTransferFrom&lt;/code&gt;). It normalizes behavior by forcing a silent &lt;code&gt;false&lt;/code&gt; failure to revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; SafeERC20 forces silent failures to revert. But occasionally, you actually want certain transfers to bypass reverts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #4: Zero Address &amp;amp; Zero Amount Reverts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Transferring an amount of &lt;code&gt;0&lt;/code&gt; is a harmless No-Op (no operation) that costs a little gas but executes successfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Different token implementations handle zeros differently. Some explicitly revert if the transferred amount is &lt;code&gt;0&lt;/code&gt;, or if the target is the zero address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imagine this scenario:&lt;/strong&gt;&lt;br&gt;
Your protocol distributes daily yields in a batch loop to 100 users. One user earned &lt;code&gt;0&lt;/code&gt; yield today. The loop attempts to &lt;code&gt;transfer(user, 0)&lt;/code&gt;. The token reverts, crashing the entire transaction and preventing the other 99 users from receiving their yields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Standardize behavior by wrapping transfers in a simple defensive check to bypass zero-amount logic.&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Standardizing behavior protects your loops. But what if the token completely breaks the standard interface?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #5: Missing Return Values (The USDT Problem)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
All ERC20 tokens return a boolean, as dictated by the standard interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production Reality:&lt;/strong&gt; Tether (USDT) on Ethereum Mainnet is non-standard. Its &lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;transferFrom&lt;/code&gt;, and approve functions do not return a boolean; they return &lt;code&gt;void&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
If your contract compiles against the standard &lt;code&gt;IERC20&lt;/code&gt; interface, the EVM expects boolean return data to decode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standard: function transfer(...) returns (bool)
USDT:     function transfer(...) [returns void]
EVM:      "Wait, where is the boolean to decode? Revert."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your protocol completely fails to integrate with the largest stablecoin on the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
OpenZeppelin's &lt;code&gt;SafeERC20&lt;/code&gt; handles this under the hood. By using &lt;code&gt;safeTransfer&lt;/code&gt;, you safely abstract away the missing return value issue at the assembly level.&lt;/p&gt;

&lt;p&gt;Takeaway: SafeERC20 handles legacy interfaces. But interface compatibility won't save you from centralized control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #6: Blacklisted Addresses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If an address has a sufficient balance and allowance, a transfer will always succeed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Fiat-backed stablecoins like USDC and USDT maintain centralized blacklists. If an address is blacklisted, all transfers involving that address will revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol liquidates undercollateralized debt. To close a position, it must send leftover USDC collateral to the user. The user gets blacklisted by Circle. The liquidation transaction reverts when attempting to send the leftover funds, preventing the protocol from clearing bad debt. The systemic risk cascades, threatening protocol solvency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Rule: Pull-over-Push&lt;/strong&gt;&lt;br&gt;
Decouple protocol logic from token transfers. Instead of pushing funds to a user during a critical state change, record their balance internally and force them to pull (claim) it in a separate transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Decoupling logic prevents centralized blacklists from freezing your protocol. Speaking of centralized quirks, let's look at approvals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #7: Non-Zero Approval Restrictions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
You can update an allowance from any number to any other number using &lt;code&gt;approve(spender, newAmount)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
To prevent approval front-running (where an attacker exploits an old allowance while a new one is pending), tokens like USDT require that allowances be reset to &lt;code&gt;0&lt;/code&gt; before being updated to a new non-zero value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A router contract tries to update its USDT approval to a DEX from 50 to 100. It calls &lt;code&gt;approve(dex, 100)&lt;/code&gt;. Because the current allowance is &lt;code&gt;50&lt;/code&gt; (non-zero), USDT reverts the transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
You must approve &lt;code&gt;0&lt;/code&gt; before approving the target amount. In OpenZeppelin v5+, &lt;code&gt;safeApprove&lt;/code&gt; was removed. Use &lt;code&gt;forceApprove()&lt;/code&gt;, which natively handles the &lt;code&gt;0&lt;/code&gt; reset under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Approval races are tricky, but at least the token is always the token... right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #8: Multiple Addresses for the Same Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; Think of an ERC20 token as a ledger. Usually, there is one door to access that ledger. But sometimes, developers build multiple doors (proxies, wrappers, aliases) that lead to the exact same ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some tokens (like older TrueUSD implementations) use multiple entry point addresses that point to the same underlying state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A lending protocol relies on the token's contract address as a unique identifier for price feeds. An attacker uses a wrapper contract that proxies to the same underlying token. The protocol reads the proxy as an unverified token, misprices the collateral, and allows the attacker to borrow against manipulated values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Identify tokens strictly by exact, whitelisted addresses. Never rely blindly on &lt;code&gt;msg.sender&lt;/code&gt; to dynamically determine asset identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Identity is fluid. And worse, token logic is mutable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #9: Upgradeable Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
The token behavior I test today will be the token behavior in production tomorrow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Tokens like USDC are upgradeable behind a proxy. The controlling entity can push an upgrade at any time, adding transfer fees, callbacks, or entirely new operational restrictions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
You build a protocol assuming USDC has no fee-on-transfer. Circle upgrades USDC to enforce a 1 basis point transaction fee. Your static accounting breaks, and protocol funds become permanently locked because the math no longer balances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Code defensively. Use balance-delta accounting (Edge Case #1) even if the token currently doesn't require it. Build pause functionality into your own protocol so you can halt operations if an upgrade breaks your integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; You can't control token upgrades, but you can control how you calculate value. Which brings us to spot balances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #10: Flash-Mintable Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Massive token balances represent real, scarce capital.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some tokens, like DAI, natively support flash-minting. A user can instantly mint millions of tokens in a single transaction, provided they are burned (with a fee) by the end of execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol calculates governance voting power based on spot balances within a liquidity pool. An attacker flash-mints 100 million DAI, executes a massive governance manipulation, and repays the DAI in the same block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Never rely on spot balances for price discovery or voting power. Use Time-Weighted Average Price (TWAP) oracles and historical checkpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Spot balances lie. And sometimes, tokens lie about how much they transferred, too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #11: Transferring Less Than Requested&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If I request to transfer &lt;code&gt;type(uint256).max&lt;/code&gt;, the token will either transfer that exact enormous amount or revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some obscure tokens treat an amount parameter of &lt;code&gt;type(uint256).max&lt;/code&gt; as a custom flag meaning "transfer all of the user's available balance."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A protocol requests a &lt;code&gt;uint256.max&lt;/code&gt; sweep. The token successfully transfers the user's actual balance of 50 tokens. The protocol incorrectly records that &lt;code&gt;type(uint256).max&lt;/code&gt; tokens were received, creating massive artificial inflation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Once again, balance-delta accounting saves the day. Never assume the requested amount is the executed amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #12: High-Decimal Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Tokens either have 18 decimals (like ETH) or 6 decimals (like USDC).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Tokens like YAMv2 use 24 decimals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol multiplies a token balance by a high-precision constant before dividing it. Because the token already has 24 decimals, the multiplication exceeds &lt;code&gt;type(uint256).max&lt;/code&gt; and triggers an unexpected math overflow, bricking the transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Always dynamically read the &lt;code&gt;decimals()&lt;/code&gt; function and normalize balances to a standard precision internally before performing complex math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus Edge Cases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #13. Pausable Tokens:&lt;/strong&gt;&lt;br&gt;
Tokens can be globally paused by their admins. Ensure your system degrades gracefully and uses Pull-over-Push accounting so a globally paused token doesn't brick your entire transaction loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #14. Large Approval/Transfer Reverts:&lt;/strong&gt;&lt;br&gt;
Tokens like UNI and COMP use &lt;code&gt;uint96&lt;/code&gt; for their internal accounting limits. Attempting to approve or transfer &lt;code&gt;uint256.max&lt;/code&gt; will unexpectedly revert. Always approve exact required amounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defensive ERC20 Integration Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before deploying any contract that touches external ERC20 tokens, verify your architecture against these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Are you using SafeERC20?&lt;/strong&gt; Never use bare &lt;code&gt;.transfer()&lt;/code&gt; or &lt;code&gt;.transferFrom()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you measuring balance deltas?&lt;/strong&gt; Never assume &lt;code&gt;amount sent == amount received&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you safe from reentrancy?&lt;/strong&gt; Assume every token transfer is an external callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you using Pull-over-Push?&lt;/strong&gt; Ensure a failing transfer only affects the specific user, not the broader system loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have you normalized decimals?&lt;/strong&gt; Do not assume 6 or 18 decimals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you rely on spot balances?&lt;/strong&gt; Ensure oracles and voting mechanisms cannot be manipulated via flash-mints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Closing Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smart contract engineering is deeply unforgiving because you are not just building software; you are building immutable financial infrastructure.&lt;/p&gt;

&lt;p&gt;When you integrate an external token, you do not just integrate an asset. You inherit its execution models, its upgrade paths, its governance assumptions, and its operational constraints. If the token's logic changes, or if it behaves in a way you didn't model for, your protocol pays the price.&lt;/p&gt;

&lt;p&gt;Treat every external token as fundamentally hostile. Rely on pure math, stateful invariants, and defensive accounting.&lt;/p&gt;

&lt;p&gt;Reliable systems emerge from reducing assumptions. Let's build something we can trust.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What edge cases have caused problems in your own deployments? Let's chat about it in the comments below. Follow for more deep dives into smart contract architecture and onchain security.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>smartcontract</category>
      <category>web3</category>
      <category>code</category>
    </item>
    <item>
      <title>The Internet Hasn't Changed</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 07 Sep 2026 12:11:38 +0000</pubDate>
      <link>https://dev.to/binnadev/the-internet-hasnt-changed-3hgg</link>
      <guid>https://dev.to/binnadev/the-internet-hasnt-changed-3hgg</guid>
      <description>&lt;p&gt;Before we argue about the future of the web, we need to understand what it has always been: one continuous system of innovation built on the exact same foundation.&lt;/p&gt;

&lt;p&gt;Let's start with the Web3 ecosystem.&lt;/p&gt;

&lt;p&gt;When people talk about Web3, the conversation usually gets tangled up in tokens, hype, and technical buzzwords. But at &lt;strong&gt;&lt;a href="https://binnadev.com" rel="noopener noreferrer"&gt;BinnaDevLab&lt;/a&gt;&lt;/strong&gt;, when we talk about the Web3 ecosystem, we are not talking about a completely different technology. We are referring to the internet as a whole, viewed from Version 3.&lt;/p&gt;

&lt;p&gt;Because the truth is, the internet hasn't fundamentally changed.&lt;/p&gt;

&lt;p&gt;The internet has always been a means of connection. The core idea is simply to bring people together, share resources, and bridge communication. If you Google the word "Internet," you'll get a definition something like this: &lt;em&gt;a global system of connected computer networks that people use to share information and communicate worldwide.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over the years, we have experienced completely different ways of doing this. We went from writing letters and painting on walls, to static web pages, social media apps, and streaming reels. It feels like a different world, but it is just a system of innovation built on the exact same block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Water Analogy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about drinking water for a second.&lt;/p&gt;

&lt;p&gt;In ancient times, people used their hands, clay pots, or carved artifacts to draw water. Today? We use modern dispensers, insulated smart cups, and bottled cans. The method changed entirely, but the fundamental action remained exactly the same: we are still drinking water.&lt;/p&gt;

&lt;p&gt;The point of innovation is not to change the fundamental purpose. It is to improve the vessel that delivers it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v9tyrqz38cser47kt1i.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v9tyrqz38cser47kt1i.png" alt="A split-screen conceptual illustration showing an ancient clay pot on one side and a modern smart cup on the other. Both vessels contain the same water, connected by a subtle glowing line. The visual communicates that while technology, interfaces, and infrastructure evolve, the fundamental purpose remains unchanged. The vessel improves; the underlying function does not." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bring this to the internet. When you strip away the buzzwords, the internet is simply our vessel for communication. The changes that happen over time are just iterations. We attach numbers to them, but if you look closely, they all mean the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Season 1: The "Read-Only" Web
&lt;/h2&gt;

&lt;p&gt;When you see the word "Web1," what is the first thing that comes to your mind?&lt;/p&gt;

&lt;p&gt;We all know the "web" is the internet. But when we attach a "1" to it, it triggers curiosity. Why call it 1?&lt;/p&gt;

&lt;p&gt;It's just like a television series. You have Season 1, Season 2, and Season 3. The numbers are simply there to show progression. Innovation is a progression; it has different iterations, and we need to keep track of it. Web1 just means the first version of the show.&lt;/p&gt;

&lt;p&gt;So, what did Season 1 of the internet look like? Take a pause and think about it for a minute.&lt;/p&gt;

&lt;p&gt;Web1 was the "read-only" web.&lt;/p&gt;

&lt;p&gt;Companies published, and you consumed. You could visit a website and read what was there, but you couldn't easily comment, post, or interact. The information was mostly static text and typed documents.&lt;/p&gt;

&lt;p&gt;But at the time, it solved a massive problem: it removed physical barriers to information. The core promise was, &lt;em&gt;"Anyone with a browser can access information from anywhere."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We haven't stopped chasing that goal. We just wanted a level up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human Ambition and Season 2
&lt;/h2&gt;

&lt;p&gt;Humans are never satisfied.&lt;/p&gt;

&lt;p&gt;Men sat and thought: "If birds can fly, shouldn't we build a vehicle to do that?" "If fish can swim, shouldn't we build a vehicle to do that too?"&lt;/p&gt;

&lt;p&gt;That exact mindset pushed the internet from Season 1 to Season 2. Web1 gave us the protocols and the connection, but human ambition always wants to go further. We wanted to move from just "publish and read” to “interact, share, and build together."&lt;/p&gt;

&lt;p&gt;This birthed Web2: the "read-and-write" web.&lt;/p&gt;

&lt;p&gt;Web2 didn't change the fundamental internet. It just brought our pictures, our videos, and our voices into the mix. But this caused a massive cultural shift. Why? Because suddenly, you weren't just consuming the information anymore. You became the information that other people consume.&lt;/p&gt;

&lt;p&gt;The internet became highly interactive and social. But making ourselves the product came with a hidden cost we didn't fully understand at the time.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxb93xhn7l5otgcdnjo72.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxb93xhn7l5otgcdnjo72.png" alt="A technical comparison showing how the user's role changed between Web1 and Web2. In Web1, information flows primarily in one direction from servers to users, making users consumers of information. In Web2, users interact bidirectionally with platforms, creating, uploading, sharing, and distributing content such as images, videos, and text." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost and the Web3 Fix
&lt;/h2&gt;

&lt;p&gt;Web2 is amazing, but it is dominated by a few giant platforms. They store your data. They decide what gets shown or removed. They monetize your activity, keeping the profits while you create the value.&lt;/p&gt;

&lt;p&gt;That is why people started pushing for Web3. We saw central gatekeepers raising issues of censorship. We saw privacy slipping away. People wanted more say over their digital identity.&lt;/p&gt;

&lt;p&gt;For some reason, people try to detach Web3 from the internet, as if it's some completely alien technology. It's not. It is simply the next season of the exact same show, designed to fix a glaring structural problem.&lt;/p&gt;

&lt;p&gt;Web3 uses new tech enablers like blockchain, decentralized storage, and peer-to-peer protocols, to introduce the "read-write-own" web.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web 1.0 (Read-only):&lt;/strong&gt; Companies publish, you consume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web 2.0 (Read-write):&lt;/strong&gt; You interact, but platforms own the space and the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web 3.0 (Read-write-own):&lt;/strong&gt; You interact, but you own your data, your identity, and the value you create without a central intermediary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of data living on a few company servers, it's distributed. It's not a buzzword. It's a structural fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Are An Internet Builder
&lt;/h2&gt;

&lt;p&gt;I need to clear up a major misconception: Web3 is not at war with Web2 or Web1.&lt;/p&gt;

&lt;p&gt;Do not see this as a competition. Web1 hasn't even faded away; every time you read a static webpage, you are using the fundamental internet. Web3 was birthed from the others. They all share the exact same DNA.&lt;/p&gt;

&lt;p&gt;If Web3 promises to remove centralized gatekeepers, it means eventually, someone will find the gaps in Web3 and introduce the next innovation. Maybe Web4. You will find a pattern that they are just continuously filling the gaps the previous version left behind. It is one whole, continuous system.&lt;/p&gt;

&lt;p&gt;This means if you can build in Web1, you can build in Web2, Web3, and whatever comes next.&lt;/p&gt;

&lt;p&gt;There are endless possibilities. You don't have to box yourself in as just a "Web2 developer" or a "Web3 developer."&lt;/p&gt;

&lt;p&gt;You are an internet builder.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu15og0t00h52j4l71d29.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu15og0t00h52j4l71d29.png" alt="A horizontal evolution diagram showing Web1, Web2, and Web3 as overlapping stages built on the same underlying internet rather than as isolated technologies. A continuous baseline represents “The Internet, Connection &amp;amp; Sharing,” while successive waves represent the evolving capabilities of the web. Web1 introduces reading, Web2 expands into reading and writing, and Web3 introduces concepts around ownership and governance. The overlapping waves emphasize continuity and shared infrastructure rather than technological separation." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Method
&lt;/h2&gt;

&lt;p&gt;Because the web shares the same DNA, you need to have a foundational knowledge of versions 1 and 2 to actually grasp what we are building in version 3. You can't build the future if you don't understand the past.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;&lt;a href="https://www.binnadev.com" rel="noopener noreferrer"&gt;BinnaDevLab&lt;/a&gt;&lt;/strong&gt;, we see the endless possibilities of these new iterations. We train developers to understand this entire ecosystem strictly from an engineering method.&lt;/p&gt;

&lt;p&gt;The internet is here to stay. It may have different versions, but do not confuse it. It is still the same internet. We are still connecting. We are still sharing resources. We are still processing data. We are just using better vessels.&lt;/p&gt;

&lt;p&gt;The Internet is Here. Come to &lt;strong&gt;&lt;a href="https://www.binnadev.com" rel="noopener noreferrer"&gt;BinnaDevLab&lt;/a&gt;&lt;/strong&gt; and learn how to grow this knowledge.&lt;/p&gt;

&lt;p&gt;Before you go, I have a quick task for you: Google what "engineering method" is, and drop your findings in the comments below. Let's discuss it.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>software</category>
      <category>programming</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>ERC20 Edge Cases Every Smart Contract Engineer Should Know</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:47:40 +0000</pubDate>
      <link>https://dev.to/binnadev/erc20-edge-cases-every-smart-contract-engineer-should-know-3dhb</link>
      <guid>https://dev.to/binnadev/erc20-edge-cases-every-smart-contract-engineer-should-know-3dhb</guid>
      <description>&lt;p&gt;Your protocol launches. Everything passes the test suite with 100% coverage. Your audits are clean.&lt;/p&gt;

&lt;p&gt;Then, an unexpected token enters your liquidity pool. The accounting drifts by a fraction of a percent. Within hours, the discrepancy compounds. Users attempt to withdraw, and the transactions revert. The vault is permanently bricked, and funds are stuck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most ERC20 exploits are not actually bugs in the protocol's core logic. They happen because protocol engineers assume every token behaves exactly like a vanilla OpenZeppelin implementation.&lt;/p&gt;

&lt;p&gt;The ERC20 standard is an interface, not a guarantee. It defines function signatures (&lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;, &lt;code&gt;balanceOf&lt;/code&gt;), but it dictates absolutely nothing about the internal execution logic behind those signatures.&lt;/p&gt;

&lt;p&gt;If your protocol integrates arbitrary tokens, your threat model immediately includes the behavior of those tokens. A single flawed assumption about how a token transfers, reverts, or accounts for balances can lead to systemic insolvency.&lt;/p&gt;

&lt;p&gt;Let's look under the hood of the most common ERC20 edge cases that break production systems, and how to engineer defensive architectures to mitigate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Four Assumptions That Break Integrations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we look at specific edge cases, it helps to build a mental model of why these failures happen. Almost every ERC20 integration bug stems from one of four flawed assumptions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Amount Assumptions:&lt;/strong&gt; Believing the amount you request to transfer is the exact amount that will arrive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution Assumptions:&lt;/strong&gt; Assuming token transfers are passive state updates, rather than active execution environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Assumptions:&lt;/strong&gt; Believing that having a sufficient balance and allowance guarantees a transfer will succeed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting Assumptions:&lt;/strong&gt; Hardcoding precision logic or assuming token behavior is immutable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's break down how these assumptions fail in production.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fowwo7jcgncmlvp629298.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fowwo7jcgncmlvp629298.png" alt="A clean, technical 3D isometric diagram of a glowing smart contract " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #1: Fee-on-Transfer &amp;amp; Rebasing Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If I call &lt;code&gt;token.transferFrom(alice, vault, 100)&lt;/code&gt;, the vault's balance increases by exactly 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Tokens can deduct a tax on transfer (e.g., a 5% protocol fee) or continuously alter user balances algorithmically (rebasing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A user deposits 100 fee-on-transfer tokens. The token takes a 5% fee.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User calls deposit(100)
👇
Token executes transfer, deducts 5% tax
👇
Vault receives 95 tokens

❌ Vault internally mints 100 shares based on 'amount' parameter
✅ Vault should mint 95 shares based on actual received funds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the vault blindly minted 100 shares, the user can immediately withdraw 100 tokens, successfully stealing 5 tokens from previous depositors. The accounting drifts, and the last users to withdraw will be left with nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Never trust the &lt;code&gt;amount&lt;/code&gt; parameter for internal accounting. Always measure the actual balance delta before and after the transfer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceReceived = token.balanceOf(address(this)) - balanceBefore;

// Use balanceReceived for all subsequent logic, never 'amount'
_mintShares(msg.sender, balanceReceived);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Balance-delta accounting fixes this assumption. Unfortunately, the next edge case breaks protocols even when the balances match perfectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #2: Tokens with Callbacks (ERC-777 &amp;amp; Transfer Hooks)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Calling transfer only updates storage balances and returns execution flow directly back to my contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Standards like ERC-777, or custom tokens with transfer hooks, actively hand execution control back to the sender or receiver via external calls during the transfer process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol updates the user's internal debt balance after pushing tokens to them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User calls withdraw()
👇
Protocol calls token.transfer(user)
👇
Token calls back to User's smart contract
👇
User's contract calls withdraw() AGAIN (before protocol debt is updated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a classic reentrancy attack, facilitated entirely by the token's internal hook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Surprises Engineers:&lt;/strong&gt; We naturally view tokens as passive data ledgers. Callbacks turn tokens into active, hostile execution environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Follow the Checks-Effects-Interactions (CEI) pattern religiously. Update all internal state before interacting with external tokens. Additionally, apply OpenZeppelin's &lt;code&gt;ReentrancyGuard&lt;/code&gt; (&lt;code&gt;nonReentrant&lt;/code&gt; modifier) to any function interacting with external tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Reentrancy guards handle unexpected execution paths. But what happens when a token quietly fails without telling you?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #3: Returning &lt;code&gt;false&lt;/code&gt; vs. Reverting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If a token transfer fails, the transaction will revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some older tokens (like ZRX) do not revert on failure. Instead, they gracefully return a false boolean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol calls &lt;code&gt;token.transfer(user, amount)&lt;/code&gt;and doesn't check the return value. The transfer fails (returns &lt;code&gt;false&lt;/code&gt;), but your code continues executing. The protocol marks the user's debt as paid, even though no tokens actually moved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Never use bare &lt;code&gt;transfer&lt;/code&gt; or &lt;code&gt;transferFrom&lt;/code&gt; calls. Wrap them in logic that asserts the boolean return value is &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenZeppelin / best practice&lt;/strong&gt;&lt;br&gt;
Always use OpenZeppelin's &lt;code&gt;SafeERC20&lt;/code&gt; library (&lt;code&gt;safeTransfer&lt;/code&gt;, &lt;code&gt;safeTransferFrom&lt;/code&gt;). It normalizes behavior by forcing a silent &lt;code&gt;false&lt;/code&gt; failure to revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; SafeERC20 forces silent failures to revert. But occasionally, you actually want certain transfers to bypass reverts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #4: Zero Address &amp;amp; Zero Amount Reverts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Transferring an amount of &lt;code&gt;0&lt;/code&gt; is a harmless No-Op (no operation) that costs a little gas but executes successfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Different token implementations handle zeros differently. Some explicitly revert if the transferred amount is &lt;code&gt;0&lt;/code&gt;, or if the target is the zero address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imagine this scenario:&lt;/strong&gt;&lt;br&gt;
Your protocol distributes daily yields in a batch loop to 100 users. One user earned &lt;code&gt;0&lt;/code&gt; yield today. The loop attempts to &lt;code&gt;transfer(user, 0)&lt;/code&gt;. The token reverts, crashing the entire transaction and preventing the other 99 users from receiving their yields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Standardize behavior by wrapping transfers in a simple defensive check to bypass zero-amount logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (amount &amp;gt; 0) {
    token.safeTransfer(user, amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Standardizing behavior protects your loops. But what if the token completely breaks the standard interface?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #5: Missing Return Values (The USDT Problem)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
All ERC20 tokens return a boolean, as dictated by the standard interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production Reality:&lt;/strong&gt; Tether (USDT) on Ethereum Mainnet is non-standard. Its &lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;transferFrom&lt;/code&gt;, and approve functions do not return a boolean; they return &lt;code&gt;void&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
If your contract compiles against the standard &lt;code&gt;IERC20&lt;/code&gt; interface, the EVM expects boolean return data to decode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standard: function transfer(...) returns (bool)
USDT:     function transfer(...) [returns void]
EVM:      "Wait, where is the boolean to decode? Revert."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your protocol completely fails to integrate with the largest stablecoin on the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
OpenZeppelin's &lt;code&gt;SafeERC20&lt;/code&gt; handles this under the hood. By using &lt;code&gt;safeTransfer&lt;/code&gt;, you safely abstract away the missing return value issue at the assembly level.&lt;/p&gt;

&lt;p&gt;Takeaway: SafeERC20 handles legacy interfaces. But interface compatibility won't save you from centralized control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #6: Blacklisted Addresses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If an address has a sufficient balance and allowance, a transfer will always succeed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Fiat-backed stablecoins like USDC and USDT maintain centralized blacklists. If an address is blacklisted, all transfers involving that address will revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol liquidates undercollateralized debt. To close a position, it must send leftover USDC collateral to the user. The user gets blacklisted by Circle. The liquidation transaction reverts when attempting to send the leftover funds, preventing the protocol from clearing bad debt. The systemic risk cascades, threatening protocol solvency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Rule: Pull-over-Push&lt;/strong&gt;&lt;br&gt;
Decouple protocol logic from token transfers. Instead of pushing funds to a user during a critical state change, record their balance internally and force them to pull (claim) it in a separate transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Decoupling logic prevents centralized blacklists from freezing your protocol. Speaking of centralized quirks, let's look at approvals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #7: Non-Zero Approval Restrictions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
You can update an allowance from any number to any other number using &lt;code&gt;approve(spender, newAmount)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
To prevent approval front-running (where an attacker exploits an old allowance while a new one is pending), tokens like USDT require that allowances be reset to &lt;code&gt;0&lt;/code&gt; before being updated to a new non-zero value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A router contract tries to update its USDT approval to a DEX from 50 to 100. It calls &lt;code&gt;approve(dex, 100)&lt;/code&gt;. Because the current allowance is &lt;code&gt;50&lt;/code&gt; (non-zero), USDT reverts the transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
You must approve &lt;code&gt;0&lt;/code&gt; before approving the target amount. In OpenZeppelin v5+, &lt;code&gt;safeApprove&lt;/code&gt; was removed. Use &lt;code&gt;forceApprove()&lt;/code&gt;, which natively handles the &lt;code&gt;0&lt;/code&gt; reset under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Approval races are tricky, but at least the token is always the token... right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #8: Multiple Addresses for the Same Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mental Model:&lt;/strong&gt; Think of an ERC20 token as a ledger. Usually, there is one door to access that ledger. But sometimes, developers build multiple doors (proxies, wrappers, aliases) that lead to the exact same ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some tokens (like older TrueUSD implementations) use multiple entry point addresses that point to the same underlying state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A lending protocol relies on the token's contract address as a unique identifier for price feeds. An attacker uses a wrapper contract that proxies to the same underlying token. The protocol reads the proxy as an unverified token, misprices the collateral, and allows the attacker to borrow against manipulated values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Identify tokens strictly by exact, whitelisted addresses. Never rely blindly on &lt;code&gt;msg.sender&lt;/code&gt; to dynamically determine asset identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Identity is fluid. And worse, token logic is mutable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #9: Upgradeable Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
The token behavior I test today will be the token behavior in production tomorrow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Tokens like USDC are upgradeable behind a proxy. The controlling entity can push an upgrade at any time, adding transfer fees, callbacks, or entirely new operational restrictions.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xcg5m3b327dh1p71cvy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xcg5m3b327dh1p71cvy.png" alt="USDC are upgradeable behind a proxy." width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
You build a protocol assuming USDC has no fee-on-transfer. Circle upgrades USDC to enforce a 1 basis point transaction fee. Your static accounting breaks, and protocol funds become permanently locked because the math no longer balances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Code defensively. Use balance-delta accounting (Edge Case #1) even if the token currently doesn't require it. Build pause functionality into your own protocol so you can halt operations if an upgrade breaks your integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; You can't control token upgrades, but you can control how you calculate value. Which brings us to spot balances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #10: Flash-Mintable Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Massive token balances represent real, scarce capital.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some tokens, like DAI, natively support flash-minting. A user can instantly mint millions of tokens in a single transaction, provided they are burned (with a fee) by the end of execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol calculates governance voting power based on spot balances within a liquidity pool. An attacker flash-mints 100 million DAI, executes a massive governance manipulation, and repays the DAI in the same block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Never rely on spot balances for price discovery or voting power. Use Time-Weighted Average Price (TWAP) oracles and historical checkpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Spot balances lie. And sometimes, tokens lie about how much they transferred, too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #11: Transferring Less Than Requested&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
If I request to transfer &lt;code&gt;type(uint256).max&lt;/code&gt;, the token will either transfer that exact enormous amount or revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Some obscure tokens treat an amount parameter of &lt;code&gt;type(uint256).max&lt;/code&gt; as a custom flag meaning "transfer all of the user's available balance."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
A protocol requests a &lt;code&gt;uint256.max&lt;/code&gt; sweep. The token successfully transfers the user's actual balance of 50 tokens. The protocol incorrectly records that &lt;code&gt;type(uint256).max&lt;/code&gt; tokens were received, creating massive artificial inflation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Once again, balance-delta accounting saves the day. Never assume the requested amount is the executed amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #12: High-Decimal Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The assumption&lt;/strong&gt;&lt;br&gt;
Tokens either have 18 decimals (like ETH) or 6 decimals (like USDC).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens&lt;/strong&gt;&lt;br&gt;
Tokens like YAMv2 use 24 decimals.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv59v8szp884sw6eeocq6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv59v8szp884sw6eeocq6.png" alt="Tokens like YAMv2 use 24 decimals." width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example failure&lt;/strong&gt;&lt;br&gt;
Your protocol multiplies a token balance by a high-precision constant before dividing it. Because the token already has 24 decimals, the multiplication exceeds &lt;code&gt;type(uint256).max&lt;/code&gt; and triggers an unexpected math overflow, bricking the transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;br&gt;
Always dynamically read the &lt;code&gt;decimals()&lt;/code&gt; function and normalize balances to a standard precision internally before performing complex math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus Edge Cases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #13. Pausable Tokens:&lt;/strong&gt;&lt;br&gt;
Tokens can be globally paused by their admins. Ensure your system degrades gracefully and uses Pull-over-Push accounting so a globally paused token doesn't brick your entire transaction loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge Case #14. Large Approval/Transfer Reverts:&lt;/strong&gt;&lt;br&gt;
Tokens like UNI and COMP use &lt;code&gt;uint96&lt;/code&gt; for their internal accounting limits. Attempting to approve or transfer &lt;code&gt;uint256.max&lt;/code&gt; will unexpectedly revert. Always approve exact required amounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defensive ERC20 Integration Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before deploying any contract that touches external ERC20 tokens, verify your architecture against these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Are you using SafeERC20?&lt;/strong&gt; Never use bare &lt;code&gt;.transfer()&lt;/code&gt; or &lt;code&gt;.transferFrom()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you measuring balance deltas?&lt;/strong&gt; Never assume &lt;code&gt;amount sent == amount received&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you safe from reentrancy?&lt;/strong&gt; Assume every token transfer is an external callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you using Pull-over-Push?&lt;/strong&gt; Ensure a failing transfer only affects the specific user, not the broader system loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have you normalized decimals?&lt;/strong&gt; Do not assume 6 or 18 decimals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you rely on spot balances?&lt;/strong&gt; Ensure oracles and voting mechanisms cannot be manipulated via flash-mints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Closing Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smart contract engineering is deeply unforgiving because you are not just building software; you are building immutable financial infrastructure.&lt;/p&gt;

&lt;p&gt;When you integrate an external token, you do not just integrate an asset. You inherit its execution models, its upgrade paths, its governance assumptions, and its operational constraints. If the token's logic changes, or if it behaves in a way you didn't model for, your protocol pays the price.&lt;/p&gt;

&lt;p&gt;Treat every external token as fundamentally hostile. Rely on pure math, stateful invariants, and defensive accounting.&lt;/p&gt;

&lt;p&gt;Reliable systems emerge from reducing assumptions. Let's build something we can trust.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What edge cases have caused problems in your own deployments? Let's chat about it in the comments below. Follow for more deep dives into smart contract architecture and onchain security.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>smartcontract</category>
      <category>web3</category>
      <category>code</category>
    </item>
    <item>
      <title>How AMMs Work: The Simple Math Behind Decentralized Trading</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Sat, 13 Jun 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/binnadev/how-amms-work-the-simple-math-behind-decentralized-trading-9c7</link>
      <guid>https://dev.to/binnadev/how-amms-work-the-simple-math-behind-decentralized-trading-9c7</guid>
      <description>&lt;p&gt;One of the hardest problems in decentralized systems is this: how do two strangers trade without trusting each other or paying a middleman to coordinate?&lt;/p&gt;

&lt;p&gt;In traditional finance, this is solved by centralized exchanges. On the blockchain, we solve it with an &lt;strong&gt;Automated Market Maker (AMM).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you spend any time looking at decentralized finance (DeFi), you have probably seen the acronym floating around. Today, we are going to look under the hood to see exactly what an &lt;strong&gt;AMM&lt;/strong&gt; is, why the Ethereum ecosystem desperately needed it, and how it translates market rules into unstoppable code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Constraint: Why Order Books Fail Onchain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we can appreciate the solution, we have to understand the architectural constraint.&lt;/p&gt;

&lt;p&gt;In traditional finance (like the stock market or centralized crypto exchanges like Binance or Coinbase), trading runs on an &lt;strong&gt;Order Book.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An order book is a giant digital matchmaking list. If I want to buy 1 ETH for $2,000, I write my name on the list. The exchange then searches for someone willing to sell 1 ETH for exactly $2,000. When it finds a match, the trade executes.&lt;/p&gt;

&lt;p&gt;This works flawlessly on centralized servers. But on a blockchain, order books have a fatal flaw: &lt;strong&gt;Cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every single time you place, cancel, or match an order, it requires a state change on the network. State changes cost "gas" (transaction fees). If you had to pay a $20 gas fee just to list your trade on a decentralized order book whether someone buys it or not, the system would instantly become too slow and prohibitively expensive to use.&lt;/p&gt;

&lt;p&gt;We needed a way to trade assets without a matchmaking middleman. We needed to replace human coordination with reliable computation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Analogy: The Potato &amp;amp; Apple Farmers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To understand how an AMM solves this coordination problem, let's use a simple system model.&lt;/p&gt;

&lt;p&gt;Imagine you are a potato farmer. You have hundreds of potatoes. Eventually, you get sick of them and just want a crisp, sweet, red apple.&lt;/p&gt;

&lt;p&gt;There are no grocery stores. To get an apple, you have to find an apple farmer who is tired of apples and wants potatoes, and trade with them directly. This is the exact equivalent of an &lt;strong&gt;Order Book&lt;/strong&gt;-finding a direct match. It is exhausting.&lt;/p&gt;

&lt;p&gt;Now, imagine an automated bin appears in the middle of your village.&lt;/p&gt;

&lt;p&gt;A wealthy merchant came by and filled this bin with 100 apples and 100 potatoes. The bin has no human cashier. Instead, it is run by a simple mathematical formula. The formula's only rule is to balance the supply.&lt;/p&gt;

&lt;p&gt;When you walk up to the bin and take out 20 apples, apples suddenly become scarcer. Because there are fewer apples left in the bin, the formula automatically raises the price of apples. To take those 20 apples, the bin demands you put in 25 potatoes.&lt;/p&gt;

&lt;p&gt;Next time someone wants an apple, it will cost even more potatoes, because apples are getting rarer. But if an apple farmer comes along and dumps 50 apples into the bin to take out some potatoes, the price of apples drops back down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That bin isn't intelligent. It doesn't predict markets. It simply enforces rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This automated, math-driven bin is the exact conceptual framework of an Automated Market Maker.&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%2Foocelus8tck6s45y75qv.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%2Foocelus8tck6s45y75qv.png" alt="A high-quality, modern, 3D abstract illustration. On the left, a rustic wooden basket containing glowing, stylized 3D potatoes and apples. On the right, this seamlessly morphs into a glowing, futuristic computer server running clean smart contract code. The color palette must strictly feature Royal Purple (#6B3FA0) and Soft Gold (#D6AF36) to match the BinnaDev visual identity. The vibe is premium, technical, and reliable." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mechanism: The Constant Product Formula&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In decentralized systems, that automated bin is called a &lt;strong&gt;Liquidity Pool.&lt;/strong&gt; It is simply a smart contract holding a pool of two different tokens (like ETH and USDC).&lt;/p&gt;

&lt;p&gt;Instead of waiting for an order book to match your trade, you trade &lt;em&gt;directly against the smart contract.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But how does the contract know what price to charge you? It uses an algorithmic invariant. The most common one is the &lt;strong&gt;Constant Product Formula&lt;/strong&gt;, which looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;x * y = k&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This math is beautifully simple and completely deterministic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x&lt;/code&gt; is the amount of Token A (e.g., Apples).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;y&lt;/code&gt; is the amount of Token B (e.g., Potatoes).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;k&lt;/code&gt; is a constant number that must always stay exactly the same after a trade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's use our 100 apples and 100 potatoes example.&lt;br&gt;
&lt;code&gt;100 (apples) * 100 (potatoes) = 10,000 (our constant 'k')&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you take 20 apples out of the pool, there are only 80 apples left. But the smart contract strictly enforces that the total multiplied together must still equal 10,000.&lt;/p&gt;

&lt;p&gt;So, the contract calculates the new required state: &lt;code&gt;10,000 / 80 apples = 125 potatoes.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For the pool to satisfy the invariant, it must now contain 125 potatoes. Since it originally had 100, you have to deposit 25 potatoes (the difference between 125 and 100) to execute your trade.&lt;/p&gt;

&lt;p&gt;That is it. Supply and demand, governed purely by unbreakable math. As one asset is bought out of the pool, it mathematically becomes more expensive. As an asset is sold into the pool, it becomes cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Infrastructure: Liquidity Providers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This raises an engineering question: Where do the millions of dollars worth of tokens sitting in these smart contracts come from?&lt;/p&gt;

&lt;p&gt;They come from &lt;strong&gt;Liquidity Providers (LPs).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LPs are everyday users, investors, or protocols who lock their own tokens inside the smart contract to fund the pool. In exchange for supplying this infrastructure, they receive an &lt;strong&gt;LP Token:&lt;/strong&gt; a digital receipt proving they own a percentage of that specific pool.&lt;/p&gt;

&lt;p&gt;Why would anyone risk their capital to let strangers trade against it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fees.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time a user makes a swap on an AMM, the protocol charges a tiny fee (usually around 0.3%). That fee does not go to a CEO or a centralized exchange. It goes directly back into the liquidity pool.&lt;/p&gt;

&lt;p&gt;Because the LP tokens represent a percentage claim over the pool, as those trading fees pile up, the underlying value of the LP tokens increases. The Liquidity Providers earn a yield simply for providing the capital that makes decentralized trading possible.&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%2Fj3jsswjhw4tcqh2vci0e.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%2Fj3jsswjhw4tcqh2vci0e.png" alt="A clean, modern 3D infographic diagram on a dark background. In the center is a glowing pool of digital tokens representing a Liquidity Pool. Arrows show 'Traders' swapping tokens and paying a small fee into the pool. Other arrows show 'Liquidity Providers' depositing assets into the pool and receiving glowing 'LP Tokens' and 'Yield' in return. Design should be elegant and precise, using Royal Purple and Soft Gold." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's quickly recap the system architecture we just explored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order Books are inefficient onchain.&lt;/strong&gt; Requiring a state change for every order creation and cancellation costs too much gas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AMMs replace middlemen with math.&lt;/strong&gt; You trade directly against a smart contract (a Liquidity Pool) rather than waiting for a human counterpart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices are driven by deterministic algorithms.&lt;/strong&gt; Using formulas like &lt;code&gt;x * y = k&lt;/code&gt;, the AMM automatically raises the price of an asset as it becomes scarcer in the pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liquidity Providers are the backbone.&lt;/strong&gt; They supply the underlying tokens that make the system liquid, earning trading fees as a reward for their capital.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AMMs changed trading by replacing coordination with computation, turning market rules into software anyone can verify.&lt;/p&gt;

&lt;p&gt;They took a complex, middleman-heavy financial process and reduced it to a few lines of self-executing, reliable code. They are a perfect example of how thoughtful engineering can create systems that are not only highly functional, but entirely trustless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are your thoughts on how AMMs are designed? Have you ever provided liquidity to a protocol yourself?&lt;/strong&gt; Let's chat about it in the comments below.&lt;/p&gt;

&lt;p&gt;Let's build something you can trust. Follow for more deep dives into smart contract architecture and onchain fundamentals.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>defi</category>
      <category>amm</category>
    </item>
    <item>
      <title>Demystifying DevRel: What It Actually Is (And Why Should You Become One?)</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Mon, 25 May 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/binnadev/demystifying-devrel-what-it-actually-is-and-why-should-you-become-one-26oe</link>
      <guid>https://dev.to/binnadev/demystifying-devrel-what-it-actually-is-and-why-should-you-become-one-26oe</guid>
      <description>&lt;p&gt;If you spend enough time in the tech space, you will eventually hear the word "&lt;strong&gt;DevRel&lt;/strong&gt;". You will see people with titles like &lt;em&gt;Developer Advocate&lt;/em&gt;, &lt;em&gt;Community Manager&lt;/em&gt;, or &lt;em&gt;Developer Relations Engineer&lt;/em&gt;. They speak at conferences, write tutorials, hang out in Discord servers, and the community usually seems to love them.&lt;/p&gt;

&lt;p&gt;But what exactly do they do? Is it just marketing for coders? Is it just customer support?&lt;/p&gt;

&lt;p&gt;For me, DevRel is simply the combination of two words: &lt;strong&gt;Developer&lt;/strong&gt; and &lt;strong&gt;Relationship&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you have little to no knowledge of what Developer Relations is, this article will break down exactly what it means, what the day-to-day looks like, and help you decide if it is a career path you might want to pursue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Developer" (The Builder)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A developer is a builder.&lt;/p&gt;

&lt;p&gt;Someone who creates things from scratch or builds on top of existing infrastructure. Their goal is always to create something meaningful.&lt;/p&gt;

&lt;p&gt;As humans, we naturally love creativity because it gives us variety, and variety is the spice of life.&lt;/p&gt;

&lt;p&gt;Imagine I hand you a bag of flour. With creativity, you don't just see white powder. You see the potential to create bread, cake, doughnuts, and countless other pastries. Even with cake alone, there are endless flavors and styles.&lt;/p&gt;

&lt;p&gt;That is the essence of creativity.&lt;/p&gt;

&lt;p&gt;And that exact same creative spark lives in the heart of a builder. In software development, the "flour" is code, APIs, and infrastructure. They take raw technical ingredients and bake them into applications that change the world.&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%2F2u4yxjymdui9ep3jpofi.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%2F2u4yxjymdui9ep3jpofi.png" alt="A visually appealing split-screen graphic. On the left, raw ingredients (flour, eggs) morphing into a cake. On the right, raw code/terminal morphing into a beautiful web interface" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Relationship" (The Connection)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, let's talk about relationship.&lt;/p&gt;

&lt;p&gt;A relationship is simply how people connect. And how do human beings connect? Through &lt;strong&gt;similarities&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;We connect over shared interests, backgrounds, mindsets, experiences, or even just sharing the same birthday or hometown. If you meet a stranger today and find out they went to your high school, you naturally connect faster because you share common ground.&lt;/p&gt;

&lt;p&gt;Bring that concept into DevRel.&lt;/p&gt;

&lt;p&gt;A DevRel is someone who can genuinely relate to builders because they understand the realities of software development. They share common ground with them.&lt;/p&gt;

&lt;p&gt;And what does that shared ground look like?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designing and building systems&lt;/li&gt;
&lt;li&gt;Dealing with broken deployments&lt;/li&gt;
&lt;li&gt;Staring at a screen at 2 AM trying to debug an issue&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%2F0z1jukepvp7qstp3i555.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%2F0z1jukepvp7qstp3i555.png" alt="A funny, relatable developer meme about code working locally but breaking in production, illustrating the " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Elephant in the Room: "Do I need to code to be a DevRel?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because of this shared reality, people constantly ask: &lt;em&gt;"Do I need to know how to code before becoming a DevRel?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From my perspective, the answer is &lt;strong&gt;yes-at least to some level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers can quickly detect inauthenticity. If you try to relate to a developer's struggle with a broken API, but you have never actually written a line of code or consumed an API yourself, the interaction falls flat.&lt;/p&gt;

&lt;p&gt;You need to understand the language developers speak so you can communicate naturally and actually help them solve their problems. You don't need to be the absolute best 10x senior engineer in the room, but you do need to have built things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Does a DevRel Actually Do Daily?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have you ever wondered what a Developer Advocate does when they log into their computer every morning? Typically, their daily tasks involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writing Code &amp;amp; Content:&lt;/strong&gt; Building demo applications, writing technical tutorials, and creating open-source templates so developers don't have to start from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Management:&lt;/strong&gt; Hanging out in Discord or Slack, answering technical questions, and helping developers get unblocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public Speaking:&lt;/strong&gt; Giving technical talks at conferences, hosting webinars, or running hackathons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product Feedback:&lt;/strong&gt; Listening to what frustrates the community and carrying that feedback directly to the internal engineering team to improve the product.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Developer Relations (DevRel) is a function that bridges the gap between developers and a product or platform. DevRels help developers succeed by providing education, tools, and support, while also feeding real-world developer feedback back into the product and engineering teams.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why Does DevRel Exist? (The Bridge)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You might be wondering, if a company builds a great technical product, won't developers just use it?&lt;/p&gt;

&lt;p&gt;Not necessarily. There are many reasons companies hire DevRel teams, but the major responsibility boils down to one word: &lt;strong&gt;Onboarding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Whenever a new system, protocol, or infrastructure is created, developers need to understand four critical things before they will dedicate their time to it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What it does.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it works.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it matters&lt;/strong&gt; (how it makes their life easier).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How they can use it&lt;/strong&gt; (the actual code and implementation).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a company has a brilliant product but terrible documentation and no one to answer questions, developers will leave.&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%2Frmfcutdiwslyjg0gjo68.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%2Frmfcutdiwslyjg0gjo68.png" alt="A clean diagram showing " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That bridge between the raw technology and the human community is where DevRel lives.&lt;/p&gt;

&lt;p&gt;They write the tutorials. They build the sample projects. They answer questions in the Discord server. And just as importantly, they listen to the developers' frustrations and carry that feedback back to the company's internal product team to make the tool better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is DevRel the Right Career For You?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DevRel is an incredibly rewarding path, but it requires a very specific hybrid of skills. It might be perfect for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You love coding, but you also love talking about code.&lt;/strong&gt; You enjoy building, but you enjoy teaching someone else how to build just as much.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have high empathy.&lt;/strong&gt; When someone is stuck on an error message, you don't feel annoyed; you feel a genuine desire to help them figure it out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are a translator.&lt;/strong&gt; You can take a complex, intimidating technical concept and explain it simply to a beginner without making them feel stupid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You love community.&lt;/strong&gt; You actually enjoy the human side of tech.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the end of the day, a good DevRel does not just market products. Marketing is about telling someone to buy something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A good DevRel helps developers feel understood.&lt;/strong&gt; They provide the ingredients, teach the community how to bake, and celebrate the amazing things the builders create. If you love technology and you love people, DevRel might just be the perfect place for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Are you currently thinking about transitioning into DevRel? What is your biggest question about the role? Let's chat in the comments below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>community</category>
      <category>devrel</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Smart Contracts Demand Better Infrastructure: Building on contract.dev</title>
      <dc:creator>Obinna Duru</dc:creator>
      <pubDate>Sat, 23 May 2026 06:06:27 +0000</pubDate>
      <link>https://dev.to/binnadev/smart-contracts-demand-better-infrastructure-building-on-contractdev-3ha3</link>
      <guid>https://dev.to/binnadev/smart-contracts-demand-better-infrastructure-building-on-contractdev-3ha3</guid>
      <description>&lt;p&gt;When you first start writing smart contracts on the EVM chains, your journey usually begins in Remix. It is a powerful, open-source web and desktop application for writing, compiling, testing, and deploying code, making it a fantastic tool for quick iterations and local simulations. But eventually, you outgrow it. Even with its robust features, a localized environment does not perfectly replicate how your contract will interact with live mainnet state.&lt;/p&gt;

&lt;p&gt;To truly test a protocol, you move to a public testnet. This is where the engineering friction begins.&lt;/p&gt;

&lt;p&gt;You suddenly need to set up an Alchemy or Infura account just to get an RPC URL. Then comes the faucet fatigue. You scour Discord or click traffic lights on web pages just to beg for a fraction of Sepolia ETH. Many faucets now require a minimum mainnet balance to prevent spam. This is a massive roadblock if you are following basic security practices.&lt;/p&gt;

&lt;p&gt;To protect your real-world funds, you should never paste your primary wallet's private key into a local &lt;code&gt;.env&lt;/code&gt; file where an accidental GitHub push or a malicious package could expose it. Instead, you use fresh, $0-balance "dummy" accounts. But when a faucet requires a mainnet balance, it breaks this safety measure, forcing you to either risk your real wallet or fund a dummy account with real money just to get testnet tokens. Others force you to connect your personal social media and tweet just to get a single drop.&lt;/p&gt;

&lt;p&gt;If you are designing a system that requires multiple actors like a Client, a Provider, and an Arbiter in an Escrow, this friction multiplies. You either have to repeat this frustrating process for three separate wallets, or manually transfer tokens between them. And ironically, those manual transfers still cost you the precious testnet gas you just spent time trying to collect.&lt;/p&gt;

&lt;p&gt;The testing infrastructure quickly becomes more exhausting than writing the actual architecture.&lt;/p&gt;

&lt;p&gt;Recently, I began exploring ways to reduce this environmental friction and found a highly practical workflow using &lt;a href="https://contract.dev" rel="noopener noreferrer"&gt;contract.dev&lt;/a&gt;. It provides the visual simplicity of a tool like Remix, but on a globally accessible, private EVM testnets that replay mainnet state and come with built-in tools for testing, analysing, and simulating smart contracts before launch.&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%2Fmax1xjz1jde5rs86lz5a.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%2Fmax1xjz1jde5rs86lz5a.png" alt="Contract.dev Dashboard" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a look at how I approach testing a 3-actor Escrow system visually, eliminating local node overhead and faucet fatigue so I can focus purely on protocol behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architecture: A Trust-Minimized Escrow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To demonstrate this workflow, I built &lt;code&gt;TrustEscrow.sol&lt;/code&gt;. The architecture is designed to be reusable and relies on strict access control across three distinct roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client:&lt;/strong&gt; Funds the escrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; Executes the work and receives the funds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arbiter:&lt;/strong&gt; A neutral third party that resolves disputes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the state transitions depend entirely on who is calling the function, verifying role transitions is critical. I explicitly designed this contract with a &lt;code&gt;resetEscrow&lt;/code&gt; function to allow continuous lifecycle testing without needing to redeploy the contract. Here is the core state machine governing the escrow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum Stage {
    AwaitingFunding, // 0 - deployed or reset, waiting for client
    Funded,          // 1 - ETH held; work in progress
    Completed,       // 2 - client released funds; ready for reset
    Disputed,        // 3 - either party raised a dispute; arbiter needed
    Resolved         // 4 - arbiter settled dispute; ready for reset
}
Stage public stage;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Note: Making the state explicitly public is a deliberate choice here, it allows visual dashboards to read and display the exact stage of the protocol later in the workflow).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Normally, verifying these transitions requires writing extensive &lt;code&gt;.t.sol&lt;/code&gt; files with heavy &lt;code&gt;vm.prank()&lt;/code&gt; overhead. Here is how we can verify them visually on a live network instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The Instant Environment (No Alchemy, No Faucets)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The immediate relief of this workflow is the infrastructure. By creating a project on &lt;a href="https://contract.dev" rel="noopener noreferrer"&gt;contract.dev&lt;/a&gt;, I was instantly handed a live &lt;strong&gt;&lt;a href="https://docs.contract.dev/stagenets" rel="noopener noreferrer"&gt;Stagenet RPC URL&lt;/a&gt;&lt;/strong&gt;. No third-party node providers required.&lt;/p&gt;

&lt;p&gt;To test this escrow, I needed three distinct, funded wallets. Instead of configuring these locally or begging a public faucet, I used the platform's &lt;strong&gt;&lt;a href="https://docs.contract.dev/tools/wallet-generator" rel="noopener noreferrer"&gt;Wallet Generator&lt;/a&gt;&lt;/strong&gt; to spin up accounts for the Client, the Provider, and the Arbiter.&lt;/p&gt;

&lt;p&gt;Using the built-in Stagenet Faucet, I funded the Client's wallet with 100 ETH instantly. This is a live RPC that anyone in the world can interact with. You get the freedom of a public testnet without the artificial scarcity of testnet tokens.&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%2Fhd3fetxa5vdjrzkz8dvx.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%2Fhd3fetxa5vdjrzkz8dvx.png" alt="The Wallet Generator showing the 3 distinct addresses and the Client's 100 ETH balance" width="799" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: CI/CD Sync and Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike local nodes, the platform doesn't just guess your contract's ABI. By linking my GitHub repository, the platform's CI/CD pipeline automatically synced and compiled my Foundry project.&lt;/p&gt;

&lt;p&gt;To deploy, I didn't have to learn a new framework. I simply opened my terminal and ran my standard Foundry deployment script (&lt;code&gt;forge script&lt;/code&gt;), pointing it to the new Stagenet RPC and my newly funded wallet private key.&lt;/p&gt;

&lt;p&gt;The moment the transaction confirmed on the network, the platform detected the deployed contract and automatically generated a Workspace: a dedicated visual dashboard perfectly mapped to my Solidity code.&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%2F11a574yhq6i5purkkwc8.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%2F11a574yhq6i5purkkwc8.png" alt="The CI/CD dashboard or the Workspace activating after running the forge script" width="799" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Crucial note for advanced developers: Unlike a local simulation, this Stagenet utilizes &lt;strong&gt;Mainnet Replay&lt;/strong&gt;. It actively mirrors live mainnet state. If your contract interacts with live Uniswap liquidity or Chainlink oracles, you can test against those live states immediately without writing complex fork scripts).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Verifying State Transitions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When testing architecture, immediate feedback is invaluable. Using the &lt;strong&gt;&lt;a href="https://docs.contract.dev/rpc-routes/impersonation" rel="noopener noreferrer"&gt;Account Impersonation&lt;/a&gt;&lt;/strong&gt; feature directly from the dashboard UI, I impersonated the Client and executed the &lt;code&gt;fundEscrow()&lt;/code&gt; transaction with 1 ETH.&lt;/p&gt;

&lt;p&gt;The Workspace updated in real-time to reflect the new reality. Instead of relying on terminal queries or &lt;code&gt;cast call&lt;/code&gt; commands, the dashboard visually confirmed that the contract balance was strictly locked at 1 ETH, and the &lt;code&gt;stage&lt;/code&gt; variable had transitioned correctly from &lt;code&gt;AwaitingFunding&lt;/code&gt; to &lt;code&gt;Funded&lt;/code&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%2Fcljnww16lg412sgquvkr.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%2Fcljnww16lg412sgquvkr.png" alt="The Workspace dashboard showing the 1 ETH balance and the state variable changed to Funded - 1" width="799" height="348"&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%2Fdy237kwitudaoynd9h3r.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%2Fdy237kwitudaoynd9h3r.png" alt="The Workspace dashboard showing the 1 ETH balance and the state variable changed to Funded - 2" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Continuing the simulation, I impersonated the Provider to raise a dispute, and finally the Arbiter to resolve it. The entire lifecycle of a complex 3-actor protocol was verified in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Automating the Ecosystem (AI User Activity)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Manually clicking through a happy path is great for an initial sanity check. But what if you want to simulate a living, breathing protocol under continuous load? Normally, this requires writing complex Hardhat scripts or custom bots to continuously fire transactions and redeploy contracts.&lt;/p&gt;

&lt;p&gt;This is where my workflow completely changed.&lt;/p&gt;

&lt;p&gt;The platform includes an &lt;strong&gt;&lt;a href="https://docs.contract.dev/tools/activity-scheduler" rel="noopener noreferrer"&gt;Activity Scheduler&lt;/a&gt;&lt;/strong&gt; powered by AI. Instead of writing mock scripts, the AI reads your contract's source code, ABI, and storage layout, and designs realistic user personas for your specific protocol.&lt;/p&gt;

&lt;p&gt;Because I built &lt;code&gt;TrustEscrow&lt;/code&gt; to be reusable via the &lt;code&gt;resetEscrow&lt;/code&gt; function, the AI was able to create infinite, continuous testing loops. With one click, it generated new dummy wallets, funded them, and scheduled recurring transaction workflows. In my Workspace, it automatically created two distinct, recurring timelines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Happy Path:&lt;/strong&gt; It scheduled the Client to &lt;code&gt;fundEscrow&lt;/code&gt;, &lt;code&gt;releaseFunds&lt;/code&gt;, and &lt;code&gt;resetEscrow&lt;/code&gt; every 20 blocks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Dispute Path:&lt;/strong&gt; It scheduled a complex continuous workflow of &lt;code&gt;fundEscrow&lt;/code&gt; -&amp;gt; &lt;code&gt;raiseDispute&lt;/code&gt; -&amp;gt; &lt;code&gt;resolveDispute&lt;/code&gt; -&amp;gt; &lt;code&gt;resetEscrow&lt;/code&gt; every 30 blocks.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fma6uybe4vhnzmsogq21o.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%2Fma6uybe4vhnzmsogq21o.png" alt="The AI Activity Scheduler showing the Happy Path and Dispute Path running on autopilot" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn't have to write a single line of testing script. I was able to sit back and watch the Stagenet process concurrent, multi-actor state transitions continuously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bridging the Frontend Gap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beyond testing smart contract logic, this approach solves a very real integration problem.&lt;/p&gt;

&lt;p&gt;When you build in Remix or on a local Anvil node, handing the protocol off to a frontend developer usually requires them to recreate your local environment just to build the UI.&lt;/p&gt;

&lt;p&gt;Because a &lt;strong&gt;Stagenet&lt;/strong&gt; is a persistent, globally accessible network, you can simply share the RPC URL with your frontend team. You hand them a live, fully-funded sandbox where the AI Activity Simulator is already generating realistic background traffic. They can begin building the interface immediately against predictable, living data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smart contracts handle real value, so engineering them requires a deep focus on security, invariants, and mathematical correctness. Testing infrastructure should support that focus, not distract from it.&lt;/p&gt;

&lt;p&gt;While robust local environments are great for early iterations, serious architecture requires a production-like setting. Visual testing on a Stagenet makes it easier to observe protocol behavior, simulate concurrent user activity, and collaborate with your team without getting bogged down in RPC setups and faucet fatigue.&lt;/p&gt;

&lt;p&gt;If you are tired of fighting local nodes to test multi-wallet architecture, I highly recommend integrating a visual Stagenet into your workflow.&lt;/p&gt;

&lt;p&gt;You can view the full &lt;code&gt;TrustEscrow&lt;/code&gt; architecture on my GitHub here: &lt;a href="https://github.com/obinnafranklinduru/trust-escrow" rel="noopener noreferrer"&gt;github.com/obinnafranklinduru/trust-escrow&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📚 Glossary of Terms (For the Curious)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EVM (Ethereum Virtual Machine):&lt;/strong&gt; The underlying engine that processes and executes smart contracts on Ethereum and other compatible blockchains. &lt;em&gt;Web2 Analogy: Think of it like the global operating system or server environment (like Node.js or AWS) that runs your code.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RPC URL:&lt;/strong&gt; The digital bridge that allows your interface to communicate with the blockchain. &lt;em&gt;Web2 Analogy: Just like an API endpoint connects a frontend web app to a backend database.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faucet:&lt;/strong&gt; A developer service that provides free test tokens. &lt;em&gt;Web2 Analogy: Like getting free "sandbox credits" in Stripe to test payments without using real credit cards.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mainnet vs Testnet:&lt;/strong&gt; Mainnet is the live network with real financial value. A testnet is a public sandbox with zero-value tokens. &lt;em&gt;Web2 Analogy: Mainnet is your live "Production" environment. A testnet is your "Staging" environment.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stagenet:&lt;/strong&gt; A private testing environment that actively mirrors the live mainnet state. &lt;em&gt;Web2 Analogy: A high-fidelity staging server that has been perfectly synced with a live copy of your production database.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private Key:&lt;/strong&gt; The secret cryptographic password that grants total control over a wallet and its funds. &lt;em&gt;Web2 Analogy: The root password to your bank account.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; File:&lt;/strong&gt; A hidden local file used to store sensitive information securely. &lt;em&gt;Web2 Analogy: It serves the exact same purpose in Web2—keeping API keys and passwords out of public GitHub repositories.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ABI (Application Binary Interface):&lt;/strong&gt; A file that acts as a translation manual telling a frontend exactly how to interact with a compiled smart contract. &lt;em&gt;Web2 Analogy: Like an OpenAPI/Swagger document that defines the endpoints and expected payloads for a REST API.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Machine:&lt;/strong&gt; An architectural pattern where a contract can only exist in one specific stage at a time (e.g., AwaitingFunding or Completed). &lt;em&gt;Web2 Analogy: Like an e-commerce order that must strictly move from 'Pending' -&amp;gt; 'Shipped' -&amp;gt; 'Delivered' without skipping steps.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foundry:&lt;/strong&gt; A fast, specialized command-line toolkit used for compiling, testing, and deploying smart contracts. &lt;em&gt;Web2 Analogy: A mix between a build tool (like Webpack) and a testing framework (like Jest).&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vm.prank()&lt;/code&gt;:&lt;/strong&gt; A specific testing command used to impersonate a different user to verify access controls. &lt;em&gt;Web2 Analogy: Like a "Login as User" feature in an admin dashboard to test what a specific customer sees.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>web3</category>
      <category>testing</category>
      <category>solidity</category>
      <category>smartcontract</category>
    </item>
    <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://dev.to/binnadev/the-brief-for-the-judge-writing-audit-ready-documentation-h4n</link>
      <guid>https://dev.to/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://dev.to/binnadev/thinking-like-an-attacker-the-airbags-and-seatbelts-of-smart-contract-security-4g4a</link>
      <guid>https://dev.to/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://dev.to/binnadev/stop-guessing-start-proving-a-guide-to-stateful-fuzzing-in-foundry-57l4</link>
      <guid>https://dev.to/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>
  </channel>
</rss>
