<?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: Olena  </title>
    <description>The latest articles on DEV Community by Olena   (@olenadevsoft).</description>
    <link>https://dev.to/olenadevsoft</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%2F3908355%2F2225cc41-06f2-4371-a91e-fc6292a4bc7b.jpeg</url>
      <title>DEV Community: Olena  </title>
      <link>https://dev.to/olenadevsoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olenadevsoft"/>
    <language>en</language>
    <item>
      <title>A .NET Dinosaur in Web3. Day 19 - Upgradeable Contracts</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Mon, 01 Jun 2026 16:30:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-19-upgradeable-contracts-5b2a</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-19-upgradeable-contracts-5b2a</guid>
      <description>&lt;h3&gt;
  
  
  🏁 Day 7 of 7: Proxy Patterns, EVM Assembly, and Hot-Swapping Logic in Production
&lt;/h3&gt;

&lt;p&gt;Day 7 was the one I didn't expect to enjoy this much.&lt;/p&gt;

&lt;p&gt;Assembly code. Low-level EVM opcodes. A pattern that lets you upgrade a deployed smart contract without changing its address, without migrating data, and without breaking a single integration. All in one session.&lt;/p&gt;

&lt;p&gt;✅ The 7-day challenge is officially done. Here's how it ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Immutability Is Not Always a Feature
&lt;/h2&gt;

&lt;p&gt;By default, every smart contract on Ethereum is immutable. Once deployed, the bytecode cannot be changed. If you find a critical bug six months later — or the business logic needs to change — you can't patch it. You can't roll out a new version to the same address.&lt;/p&gt;

&lt;p&gt;In .NET, this is solved trivially: deploy a new DLL, update a config, restart the service. On a blockchain, there's no restart. There's no config file. There's only the bytecode sitting permanently at an address.&lt;/p&gt;

&lt;p&gt;The Proxy Pattern solves this by splitting one logical contract into two physical ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Proxy Pattern: Splitting State From Logic
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Proxy Contract&lt;/strong&gt; — holds the state. All balances, mappings, variables. Users always interact with this address. It never changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation Contract&lt;/strong&gt; — holds the logic. Just functions, no state of its own. Stateless by design.&lt;/p&gt;

&lt;p&gt;When a user calls a function on the Proxy, the Proxy uses &lt;code&gt;delegatecall&lt;/code&gt; to execute code from the Implementation — but in the context of the Proxy's own storage. The logic is borrowed. The memory is the Proxy's.&lt;/p&gt;

&lt;p&gt;The .NET analogy: &lt;code&gt;AssemblyLoadContext&lt;/code&gt; — dynamically loading an external DLL into the host process. The code is external, but the runtime memory and local state belong to the host.&lt;/p&gt;

&lt;p&gt;To upgrade: deploy a new Implementation, call &lt;code&gt;upgradeTo(newAddress)&lt;/code&gt; on the Proxy. Same address for users. New logic underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Critical Trap: Storage Slot Collisions
&lt;/h2&gt;

&lt;p&gt;In Solidity, state variables are stored sequentially in 32-byte slots starting from Slot 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;address public implementation; // Slot 0
address public admin;          // Slot 1
uint256 public storedValue;    // Slot 2

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

&lt;/div&gt;



&lt;p&gt;If V2 adds a new variable at the top of the list, it shifts everything down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 public newCounter;     // Slot 0 ← COLLISION
address public implementation; // Slot 1 ← was Slot 0
address public admin;          // Slot 2 ← was Slot 1

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

&lt;/div&gt;



&lt;p&gt;The Proxy's storage hasn't changed. But V2's bytecode now reads &lt;code&gt;admin&lt;/code&gt; from Slot 2 — which in the Proxy holds &lt;code&gt;storedValue&lt;/code&gt;. The owner address is silently overwritten. The contract is permanently broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule: new variables can only be appended at the end. Never insert, never reorder.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contracts
&lt;/h2&gt;



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

contract MiniProxy {
    address public implementation; // Slot 0
    address public admin;          // Slot 1

    constructor(address _implementation) {
        implementation = _implementation;
        admin = msg.sender;
    }

    function upgradeTo(address _newImplementation) external {
        require(msg.sender == admin, "Only admin");
        implementation = _newImplementation;
    }

    fallback() external payable {
        address _impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}

contract VaultV1 {
    address public implementation; // Slot 0 — mirrors Proxy
    address public admin;          // Slot 1 — mirrors Proxy
    uint256 public storedValue;    // Slot 2

    function setValue(uint256 _newValue) external {
        storedValue = _newValue;
    }
}

contract VaultV2 {
    address public implementation; // Slot 0 — unchanged
    address public admin;          // Slot 1 — unchanged
    uint256 public storedValue;    // Slot 2 — unchanged
    uint256 public transactionCount; // Slot 3 — new, appended at the end

    function setValue(uint256 _newValue) external {
        storedValue = _newValue;
        transactionCount += 1;
    }
}

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fallback&lt;/code&gt; function is the heart of the proxy. Every call that doesn't match a function in &lt;code&gt;MiniProxy&lt;/code&gt; lands here. The assembly block copies the calldata, fires &lt;code&gt;delegatecall&lt;/code&gt; to the implementation, copies the return data, and routes based on success or failure.&lt;/p&gt;

&lt;p&gt;This is raw EVM. No abstractions. No safety net. Exactly what you want when you need to understand what's actually happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console: Hot Upgrade in Action
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hardhat ignition deploy ignition/modules/UpgradeableVault.ts &lt;span class="nt"&gt;--network&lt;/span&gt; localhost &lt;span class="nt"&gt;--reset&lt;/span&gt;
npx hardhat console &lt;span class="nt"&gt;--network&lt;/span&gt; localhost

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x9A676e781A523b5d0C0e43731313A708CB607508&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v2Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MiniProxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxyAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 1: use V1 interface on the Proxy address&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VaultV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxyAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;V1 stored value:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storedValue&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 42n&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: upgrade&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;v2Address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upgrade successful.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3: use V2 interface on the SAME Proxy address&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VaultV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxyAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Value preserved after upgrade:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storedValue&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 42n ✅&lt;/span&gt;

&lt;span class="c1"&gt;// Step 4: call V2 logic&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New stored value:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storedValue&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 100n&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transaction count:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vaultAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transactionCount&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 1n&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;42n&lt;/code&gt; survived the upgrade without collision&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transactionCount&lt;/code&gt; initialised at zero and incremented correctly&lt;/li&gt;
&lt;li&gt;The Proxy address never changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hot upgrade of production logic with zero downtime and zero data migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  7 Days. 7 Contracts. What Actually Happened.
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Contract&lt;/th&gt;
&lt;th&gt;Core concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;AccessControlledVault&lt;/td&gt;
&lt;td&gt;constructor, modifier, custom errors, renounceOwnership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SimpleEscrow&lt;/td&gt;
&lt;td&gt;Pull-over-Push, Bulkhead pattern, CEI, death of .transfer()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;SimpleVotingDAO&lt;/td&gt;
&lt;td&gt;O(1) storage design, Gas Limit DoS, block.timestamp in PoS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;MyToken &amp;amp; ICO&lt;/td&gt;
&lt;td&gt;ERC-20 standard, fixed-point math, approve race condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;SimpleStaking&lt;/td&gt;
&lt;td&gt;Lazy Update pattern, time travel testing, IERC20 vs ERC20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;SimpleAMM&lt;/td&gt;
&lt;td&gt;x·y=k invariant, slippage, internal reserves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;UpgradeableVault&lt;/td&gt;
&lt;td&gt;delegatecall, Proxy pattern, storage slot collisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What transferred directly from .NET: transaction thinking, CEI as a guard clause pattern, interface segregation, defensive architecture, separation of concerns.&lt;/p&gt;

&lt;p&gt;What was genuinely new: gas as a design constraint, immutability as a default, price as an emergent property of math, and the fact that a 7-line assembly block can replace an entire upgrade infrastructure.&lt;/p&gt;

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

&lt;p&gt;The challenge covered maybe 5% of what's possible in Solidity. The plan now: take WishList Chain and Smart Money Tracker from MVP to v1, and go deeper into security — reentrancy patterns, audit techniques, real attack vectors.&lt;/p&gt;

&lt;p&gt;✨ If you have ideas for the next challenge — drop them in the comments. 👇&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Hybrid 🦕⚡ — understands both worlds. Challenge complete.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>smartcontract</category>
      <category>blockchain</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 18 - Automated Market Maker</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sun, 31 May 2026 15:26:18 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-18-automated-market-maker-5c9k</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-18-automated-market-maker-5c9k</guid>
      <description>&lt;h3&gt;
  
  
  🏦 Day 6 of 7: Building a Mini Uniswap in 80 Lines of Solidity
&lt;/h3&gt;

&lt;p&gt;Imagine a vending machine. It has 1,000 coffee beans and 1,000 coins. No menu, no cashier — just one iron rule: the product of the two numbers inside must never decrease.&lt;/p&gt;

&lt;p&gt;That's it!&lt;/p&gt;

&lt;p&gt;This is how Uniswap works — and this is what I built on Day 6, coming from .NET. Here's how, why it's elegant, and where you can step on a rake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an Order Book Doesn't Work on a Blockchain
&lt;/h2&gt;

&lt;p&gt;Traditional exchanges — Binance, NYSE, any CEX — run on an &lt;strong&gt;order book&lt;/strong&gt;. Market makers post bids and asks. A matching engine pairs them. Millions of updates per second, all in a centralised database.&lt;/p&gt;

&lt;p&gt;In a blockchain, this is impossible. Transactions take 12 seconds. Every state change costs gas. Storing millions of constantly changing orders would eat all the profit before a single trade completes.&lt;/p&gt;

&lt;p&gt;Uniswap's solution: replace the order book with a &lt;strong&gt;liquidity pool&lt;/strong&gt; — a smart contract holding two tokens — and replace the matching engine with pure math.&lt;/p&gt;

&lt;p&gt;Just a formula — below.&lt;/p&gt;

&lt;h2&gt;
  
  
  x · y = k — The Formula That Broke Finance
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Constant Product Invariant&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;x · y = k

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

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;x&lt;/code&gt; is the reserve of Token0, &lt;code&gt;y&lt;/code&gt; is the reserve of Token1, and &lt;code&gt;k&lt;/code&gt; is a constant that must never decrease during swaps.&lt;/p&gt;

&lt;p&gt;When a trader sells Token0 into the pool, &lt;code&gt;x&lt;/code&gt; increases. To keep &lt;code&gt;k&lt;/code&gt; constant, &lt;code&gt;y&lt;/code&gt; must decrease — the contract sends out Token1. The price is determined automatically by the ratio of reserves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live example with numbers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pool: 1,000 Token0, 1,000 Token1. k = 1,000,000.&lt;/p&gt;

&lt;p&gt;Trader sells 100 Token0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amountOut = (reserveOut × amountIn) / (reserveIn + amountIn)
amountOut = (1000 × 100) / (1000 + 100)
amountOut = 100,000 / 1,100
amountOut ≈ 90.9 Token1

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

&lt;/div&gt;



&lt;p&gt;The trader gets ~90.9, not 100. That gap is &lt;strong&gt;slippage&lt;/strong&gt; — and it's not a bug. It's the formula protecting the pool. The more you buy relative to pool size, the worse your price gets. Naturally. Mathematically.&lt;/p&gt;

&lt;p&gt;After the swap: pool has 1,100 Token0 and ~909.1 Token1. k ≈ 1,000,000. Invariant holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contract: SimpleAMM
&lt;/h2&gt;

&lt;p&gt;Three functions. Each one exists for a specific reason.&lt;br&gt;
&lt;/p&gt;

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleAMM {
    error ZeroAmount();
    error InvalidToken();
    error ZeroLiquidity();
    error TransferFailed();
    error InvalidRatio();

    IERC20 public immutable token0;
    IERC20 public immutable token1;

    // Internal reserves — cheaper than calling balanceOf() every time
    uint256 public reserve0;
    uint256 public reserve1;

    event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1);
    event Swap(address indexed trader, address tokenIn, uint256 amountIn, uint256 amountOut);

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    // Pure math — no state, no side effects
    function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut)
        public pure returns (uint256)
    {
        if (_amountIn == 0) revert ZeroAmount();
        if (_reserveIn == 0 || _reserveOut == 0) revert ZeroLiquidity();

        // Multiply first, divide last — always
        uint256 numerator = _reserveOut * _amountIn;
        uint256 denominator = _reserveIn + _amountIn;
        return numerator / denominator;
    }

    function addLiquidity(uint256 _amount0, uint256 _amount1) external {
        if (_amount0 == 0 || _amount1 == 0) revert ZeroAmount();

        // If pool already has liquidity, enforce the current price ratio
        if (reserve0 &amp;gt; 0 &amp;amp;&amp;amp; reserve1 &amp;gt; 0) {
            if (_amount0 * reserve1 != _amount1 * reserve0) revert InvalidRatio();
        }

        if (!token0.transferFrom(msg.sender, address(this), _amount0)) revert TransferFailed();
        if (!token1.transferFrom(msg.sender, address(this), _amount1)) revert TransferFailed();

        reserve0 += _amount0;
        reserve1 += _amount1;

        emit LiquidityAdded(msg.sender, _amount0, _amount1);
    }

    function swap(address _tokenIn, uint256 _amountIn) external returns (uint256 amountOut) {
        if (_amountIn == 0) revert ZeroAmount();
        if (_tokenIn != address(token0) &amp;amp;&amp;amp; _tokenIn != address(token1)) revert InvalidToken();

        bool isToken0 = _tokenIn == address(token0);

        (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = isToken0
            ? (token0, token1, reserve0, reserve1)
            : (token1, token0, reserve1, reserve0);

        // CEI: pull tokens in first
        if (!tokenIn.transferFrom(msg.sender, address(this), _amountIn)) revert TransferFailed();

        // Calculate output
        amountOut = getAmountOut(_amountIn, reserveIn, reserveOut);

        // Update reserves
        if (isToken0) {
            reserve0 += _amountIn;
            reserve1 -= amountOut;
        } else {
            reserve0 -= amountOut;
            reserve1 += _amountIn;
        }

        emit Swap(msg.sender, _tokenIn, _amountIn, amountOut);

        // Send output tokens to trader
        if (!tokenOut.transfer(msg.sender, amountOut)) revert TransferFailed();
    }
}

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;getAmountOut&lt;/code&gt;&lt;/strong&gt; — pure math, no state. Separated deliberately so it can be called by anyone to preview a trade before executing it. In DeFi this is standard: quote first, then transact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;addLiquidity&lt;/code&gt;&lt;/strong&gt; — the ratio check is the interesting part. If the pool already has reserves, you can't deposit in arbitrary proportions. &lt;code&gt;_amount0 * reserve1 != _amount1 * reserve0&lt;/code&gt; detects any imbalance. Deposit skewed amounts and you'd instantly change the price — essentially donating money to arbitrageurs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;swap&lt;/code&gt;&lt;/strong&gt; — the ternary tuple assignment is the cleanest part of the contract. Instead of two separate if/else branches, one line maps all four variables correctly based on direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = isToken0
    ? (token0, token1, reserve0, reserve1)
    : (token1, token0, reserve1, reserve0);

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where You Can Step on a Rake
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Integer division truncates, silently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getAmountOut&lt;/code&gt; divides at the end — intentionally. But the truncation still happens. &lt;code&gt;100,000 / 1,100 = 90&lt;/code&gt;, not &lt;code&gt;90.909...&lt;/code&gt;. The pool keeps the remainder. At scale across millions of trades, this accumulated dust is non-trivial. Production AMMs handle this with basis points (fee = 30 bps = multiply by 997/1000 before dividing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal reserves vs &lt;code&gt;balanceOf&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The contract tracks &lt;code&gt;reserve0&lt;/code&gt; and &lt;code&gt;reserve1&lt;/code&gt; internally instead of calling &lt;code&gt;token0.balanceOf(address(this))&lt;/code&gt; every time. Two reasons: gas savings (SLOAD is expensive, external calls are more expensive), and security — if someone sends tokens directly to the contract without going through &lt;code&gt;addLiquidity&lt;/code&gt;, the reserves won't silently become unbalanced and break the invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console Verification Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hardhat ignition deploy ignition/modules/SimpleAMM.ts &lt;span class="nt"&gt;--network&lt;/span&gt; localhost &lt;span class="nt"&gt;--reset&lt;/span&gt;
npx hardhat console &lt;span class="nt"&gt;--network&lt;/span&gt; localhost

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trader&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWalletClients&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x5FbDB2315678afecb367f032d93F642f64180aa3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t1Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ammAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t0Address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t1Address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SimpleAMM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ammAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Trader buys tokens via ICO&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0AsTrader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t0Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t1AsTrader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t1Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t0AsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buyTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 2000 Token0&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t1AsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buyTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 2000 Token1&lt;/span&gt;

&lt;span class="c1"&gt;// Add liquidity 1000:1000&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ammAsTrader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SimpleAMM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ammAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t0AsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ammAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t1AsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ammAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ammAsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addLiquidity&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reserve 0:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;amm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserve0&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt; &lt;span class="c1"&gt;// 1000&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reserve 1:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;amm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserve1&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt; &lt;span class="c1"&gt;// 1000&lt;/span&gt;

&lt;span class="c1"&gt;// Swap 100 Token0 → Token1&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t0AsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ammAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ammAsTrader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;t0Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;traderT1Balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;trader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Trader Token1 after swap:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;traderT1Balance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// ~1090.909... — math checks out&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The formula lands exactly. 1,000 × 100 / 1,100 = 90.909... Token1 received. The invariant holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Day Actually Meant
&lt;/h2&gt;

&lt;p&gt;Six days ago I was writing &lt;code&gt;owner = msg.sender&lt;/code&gt; in a constructor. Today I implemented the core pricing engine of a decentralised exchange.&lt;/p&gt;

&lt;p&gt;What transferred directly from .NET:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CEI pattern — same as any transactional system&lt;/li&gt;
&lt;li&gt;Separation of pure logic (&lt;code&gt;getAmountOut&lt;/code&gt;) from state mutation (&lt;code&gt;swap&lt;/code&gt;) — same as keeping domain logic out of controllers&lt;/li&gt;
&lt;li&gt;Defensive checks before any state change — same as guard clauses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What was genuinely new:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thinking in invariants instead of conditions&lt;/li&gt;
&lt;li&gt;Price as an emergent property of reserves, not a stored value&lt;/li&gt;
&lt;li&gt;The elegance of &lt;code&gt;x · y = k&lt;/code&gt; — one line that replaces an entire matching engine&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Day 7: Reentrancy Protection — the vulnerability that cost $60M in the 2016 DAO hack, and how to write contracts that can't be drained.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 6 of 7.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>blockchain</category>
      <category>smartcontract</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 17 - Staking</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sat, 30 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-17-staking-eln</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-17-staking-eln</guid>
      <description>&lt;h3&gt;
  
  
  Day 5 of 7: Lazy Updates, Time Travel, and Earning Rewards Without Loops
&lt;/h3&gt;

&lt;p&gt;Day 5 connected everything from the previous four days into one working system. The ERC-20 token from Day 4 becomes the asset. The staking contract locks it, tracks time, and pays out rewards — without a single loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Naive Staking
&lt;/h2&gt;

&lt;p&gt;The obvious approach: store all stakers in an array, iterate through them on a schedule, distribute rewards. In .NET this would be a background job touching a database table.&lt;/p&gt;

&lt;p&gt;In Solidity, iterating through all stakers is a Gas Limit DoS attack. The more users stake, the more expensive the distribution call becomes. Eventually it becomes physically impossible to mine. The contract freezes.&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;Lazy Updates&lt;/strong&gt;. Never iterate over users. Calculate each user's rewards on-demand, at the exact moment they interact with the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contract: SimpleStaking
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: MIT&lt;/span&gt;
&lt;span class="nx"&gt;pragma&lt;/span&gt; &lt;span class="nx"&gt;solidity&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@openzeppelin/contracts/token/ERC20/IERC20.sol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;contract&lt;/span&gt; &lt;span class="nx"&gt;SimpleStaking&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nc"&gt;ZeroAmount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nc"&gt;InsufficientStake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nc"&gt;TransferFailed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;IERC20&lt;/span&gt; &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;immutable&lt;/span&gt; &lt;span class="nx"&gt;stakingToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;constant&lt;/span&gt; &lt;span class="nx"&gt;REWARD_RATE_PER_SECOND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;struct&lt;/span&gt; &lt;span class="nx"&gt;Staker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;stakedAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;rewardsAccrued&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;lastUpdateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Staker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nc"&gt;Staked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="nx"&gt;indexed&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;uint256&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nc"&gt;Unstaked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="nx"&gt;indexed&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;uint256&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nc"&gt;RewardClaimed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="nx"&gt;indexed&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;uint256&lt;/span&gt; &lt;span class="nx"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="nx"&gt;_tokenAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;stakingToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IERC20&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_tokenAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;modifier&lt;/span&gt; &lt;span class="nf"&gt;updateReward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="nx"&gt;_account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Staker&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="nx"&gt;staker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_account&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&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;uint256&lt;/span&gt; &lt;span class="nx"&gt;timeElapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUpdateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;earned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;timeElapsed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;REWARD_RATE_PER_SECOND&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rewardsAccrued&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;earned&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUpdateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="nf"&gt;updateReward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&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;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;revert&lt;/span&gt; &lt;span class="nc"&gt;ZeroAmount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;_amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;emit&lt;/span&gt; &lt;span class="nc"&gt;Staked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&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="nx"&gt;bool&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stakingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transferFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;revert&lt;/span&gt; &lt;span class="nc"&gt;TransferFailed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;unstake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="nf"&gt;updateReward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Staker&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="nx"&gt;staker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;revert&lt;/span&gt; &lt;span class="nc"&gt;InsufficientStake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;_amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;emit&lt;/span&gt; &lt;span class="nc"&gt;Unstaked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&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="nx"&gt;bool&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stakingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;revert&lt;/span&gt; &lt;span class="nc"&gt;TransferFailed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;claimReward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="nf"&gt;updateReward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Staker&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="nx"&gt;staker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rewardsAccrued&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reward&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;staker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rewardsAccrued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;emit&lt;/span&gt; &lt;span class="nc"&gt;RewardClaimed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;bool&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stakingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;revert&lt;/span&gt; &lt;span class="nc"&gt;TransferFailed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&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;h2&gt;
  
  
  What Actually Clicked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;updateReward&lt;/code&gt; modifier is the entire architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every function that changes staker state — &lt;code&gt;stake&lt;/code&gt;, &lt;code&gt;unstake&lt;/code&gt;, &lt;code&gt;claimReward&lt;/code&gt; — is decorated with &lt;code&gt;updateReward&lt;/code&gt;. Before anything else runs, the modifier calculates how much time has passed since the user's last interaction and accrues the earned rewards to their record. Then &lt;code&gt;_;&lt;/code&gt; passes control to the function body.&lt;/p&gt;

&lt;p&gt;This is the Lazy Update pattern: no background job, no scheduler, no iteration. The math runs exactly once per user per transaction, in O(1).&lt;/p&gt;

&lt;p&gt;The .NET analogy: instead of a scheduled &lt;code&gt;IHostedService&lt;/code&gt; that iterates all accounts every minute, you calculate the accrued interest inline when the user touches their account. Same result, zero coordination overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;IERC20&lt;/code&gt; vs &lt;code&gt;ERC20&lt;/code&gt; — interface vs implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The staking contract imports &lt;code&gt;IERC20&lt;/code&gt; rather than &lt;code&gt;ERC20&lt;/code&gt;. It doesn't need the full implementation — it only needs to call &lt;code&gt;transferFrom&lt;/code&gt; and &lt;code&gt;transfer&lt;/code&gt;. Using the interface keeps the contract lean and makes it compatible with any ERC-20 token, not just MyToken. The same pattern as coding to &lt;code&gt;IRepository&amp;lt;T&amp;gt;&lt;/code&gt; instead of a concrete class in .NET.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;approve&lt;/code&gt; dependency.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before calling &lt;code&gt;stake()&lt;/code&gt;, the user must call &lt;code&gt;approve(stakingAddress, amount)&lt;/code&gt; on the token contract. The staking contract then calls &lt;code&gt;transferFrom&lt;/code&gt; to pull the tokens in. This is the same OAuth2-style delegation from Day 4 — now applied in a real protocol flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Struct field access in Viem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When reading a mapping that returns a struct, Viem returns a tuple. The fields are accessible both by index and by destructuring:&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="c1"&gt;// By index&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;staking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// stakedAmount&lt;/span&gt;

&lt;span class="c1"&gt;// By destructuring — cleaner&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rewardsAccrued&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastUpdateTime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;staking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The destructuring version is more readable and avoids magic index numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing: Time Travel in Hardhat
&lt;/h2&gt;

&lt;p&gt;Testing time-dependent reward logic requires advancing the blockchain clock. Hardhat exposes two low-level RPC commands for this:&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="c1"&gt;// Shift the node's clock forward by 100 seconds&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evm_increaseTime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Mine an empty block to commit the new timestamp&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evm_mine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via the test helpers in Hardhat 3:&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;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networkHelpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reward formula: &lt;code&gt;stakedAmount × timeElapsed × REWARD_RATE_PER_SECOND / 100&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With 100 MET staked for 100 seconds at rate 1:&lt;br&gt;
&lt;code&gt;100 × 100 × 1 / 100 = 100 MET&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The test: investor buys 1000 MET, stakes 100, advances 100 seconds, claims reward. Final balance: 1001 MET (900 unstaked + 100 reward claimed — with one extra block of time from the &lt;code&gt;claimReward&lt;/code&gt; call itself adding ~1 MET).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One fix that took time:&lt;/strong&gt; the original test called &lt;code&gt;buyTokens([], { value: ... })&lt;/code&gt; with an empty args array. Viem requires omitting the args array entirely for functions with no parameters: &lt;code&gt;buyTokens({ value: ... })&lt;/code&gt;. Small difference, silent failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console Use Case: Full Staking Cycle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hardhat ignition deploy ignition/modules/SimpleStaking.ts &lt;span class="nt"&gt;--network&lt;/span&gt; localhost &lt;span class="nt"&gt;--reset&lt;/span&gt;
npx hardhat console &lt;span class="nt"&gt;--network&lt;/span&gt; localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWalletClients&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x5FbDB2315678afecb367f032d93F642f64180aa3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stakingAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SimpleStaking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stakingAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Seed staking contract with reward reserves&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;stakingAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="c1"&gt;// Investor buys 1000 MET via ICO&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;investor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buyTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// approve + stake&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stakingAsInvestor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SimpleStaking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stakingAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;investor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;stakingAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stakingAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stake&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="c1"&gt;// Verify staked amount&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;staking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stakers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Staked:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stakedAmount&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 200&lt;/span&gt;

&lt;span class="c1"&gt;// Time travel: advance 100 seconds&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evm_increaseTime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evm_mine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Claim rewards&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stakingAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claimReward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Check final balance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Final MET balance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalBalance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// 800 (unstaked) + rewards accrued over 100 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The balance grows. The reward math lands. The Lazy Update pattern works.&lt;/p&gt;

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

&lt;p&gt;Day 6: ERC-721 NFT — non-fungible tokens, metadata, and what "ownership" means on-chain.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 5 of 7.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>blockchain</category>
      <category>solidity</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 16 - ERC-20 Token &amp; ICO</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Fri, 29 May 2026 07:20:56 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-16-erc-20-token-ico-1mj</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-16-erc-20-token-ico-1mj</guid>
      <description>&lt;h2&gt;
  
  
  A .NET Dinosaur in Web3. Day 16 - ERC-20 Token &amp;amp; ICO
&lt;/h2&gt;

&lt;p&gt;😈 Day 4 of 7: Building Own Currency… Cryptocurrency!&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Day 4 it’s exactly give me some practice with build something cool and satisfied… and real. By the end of the session I had deployed my own cryptocurrency, run an ICO, transferred tokens between wallets and delegated spending rights between accounts.&amp;nbsp;&lt;br&gt;&lt;br&gt;
I think I will back to this day of challenge when I will start build my token for WishList Chain project.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;
  
  
  What Is ERC-20?
&lt;/h3&gt;

&lt;p&gt;I little bit of theory:&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERC-20&lt;/strong&gt; is an interface standard. Any smart contract that implements it must expose a specific set of functions: &lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;, &lt;code&gt;transferFrom&lt;/code&gt;, &lt;code&gt;balanceOf&lt;/code&gt;, &lt;code&gt;allowance&lt;/code&gt;, &lt;code&gt;totalSupply&lt;/code&gt;. Any DEX, any wallet, any protocol knows how to interact with your token automatically — because they know the interface.&lt;/p&gt;

&lt;p&gt;The .NET analogy is exact: it's a C# interface. The standard is the contract. The implementation is yours.&lt;/p&gt;

&lt;p&gt;Instead of writing the entire standard from scratch, we inherit from OpenZeppelin's battle-tested implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    // All standard ERC-20 logic inherited
    // We add our own ICO mechanics on top
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multiply. Multiply. Multiply. Then — and only then — divide.
&lt;/h3&gt;

&lt;p&gt;Maths in Solidity really surprised me.&amp;nbsp;&lt;/p&gt;

&lt;h4&gt;
  
  
  No floating point - ever.
&lt;/h4&gt;

&lt;p&gt;It what we learned in previous challenge day. Just for remind…&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Solidity has no &lt;code&gt;float&lt;/code&gt;, no &lt;code&gt;double&lt;/code&gt;, no &lt;code&gt;decimal&lt;/code&gt;. If you divide 1 by 3, you get 0. Not 0.333 — zero.&lt;/p&gt;

&lt;p&gt;Every token has a &lt;code&gt;decimals&lt;/code&gt; parameter. ETH and standard ERC-20 tokens use 18. That means "1 token" is stored as:&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="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000_000_000_000_000&lt;/span&gt;  &lt;span class="c1"&gt;// 10^18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimum unit is called wei — the equivalent of a penny, except there are 10^18 of them per token. All financial logic operates in wei. If you want to mint 2.5 tokens, you write &lt;code&gt;2500000000000000000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule: &lt;strong&gt;always multiply before dividing&lt;/strong&gt;. Dividing first truncates to zero before the multiplication can recover anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Correct — multiply first
uint256 tokensToMint = (msg.value * 333) / 1000;

// Wrong — divide first, then multiply by a truncated zero
uint256 tokensToMint = (msg.value / 1000) * 333;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  SafeMath is dead.&amp;nbsp;
&lt;/h4&gt;

&lt;p&gt;Pre-Solidity 0.8.0, every arithmetic operation required a library call: &lt;code&gt;a.add(b)&lt;/code&gt; instead of &lt;code&gt;a + b&lt;/code&gt;. This protected against overflow — when adding 1 to &lt;code&gt;uint256&lt;/code&gt; max wraps around to zero.&lt;/p&gt;

&lt;p&gt;Since 0.8.0, the compiler checks automatically. Overflow reverts the transaction. The library is no longer needed and old tutorials that still use it are just showing their age.&lt;/p&gt;

&lt;h3&gt;
  
  
  approve — The OAuth2 of Web3
&lt;/h3&gt;

&lt;p&gt;This was the most important concept of the day, because it's the foundation of DeFi.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;approve(spender, amount)&lt;/code&gt; grants an external address (another contract, another wallet) the right to spend your tokens up to a limit. This is stored in the &lt;code&gt;allowance&lt;/code&gt; mapping. The spender then calls &lt;code&gt;transferFrom(owner, destination, amount)&lt;/code&gt; to act on that permission.&lt;/p&gt;

&lt;p&gt;The .NET mental model: it's OAuth2 delegated access. The &lt;code&gt;approve&lt;/code&gt; call is issuing a scoped token. The &lt;code&gt;allowance&lt;/code&gt; mapping is the token store. &lt;code&gt;transferFrom&lt;/code&gt; is the API call using that scoped token.&lt;/p&gt;

&lt;p&gt;The tokens don't move during &lt;code&gt;approve&lt;/code&gt;. The ledger only updates when &lt;code&gt;transferFrom&lt;/code&gt; is called. The owner retains their balance the entire time — the contract just knows who is permitted to move it.&lt;/p&gt;

&lt;p&gt;This same pattern is how staking contracts work: you approve the staking contract to spend your tokens, then the staking contract calls &lt;code&gt;transferFrom&lt;/code&gt; to lock them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Approve Race Condition
&lt;/h3&gt;

&lt;p&gt;There's a known vulnerability in the standard &lt;code&gt;approve&lt;/code&gt; function worth understanding.&lt;/p&gt;

&lt;p&gt;Scenario: you've approved a spender for 100 tokens. You change your mind and call &lt;code&gt;approve(spender, 50)&lt;/code&gt;. If the spender is watching the mempool, they can front-run your change — execute &lt;code&gt;transferFrom&lt;/code&gt; for the original 100 before your reduction lands, then execute another &lt;code&gt;transferFrom&lt;/code&gt; for 50 after it does. Result: they drained 150 instead of 50.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;increaseAllowance&lt;/code&gt; and &lt;code&gt;decreaseAllowance&lt;/code&gt;, which modify the limit atomically relative to the current value. OpenZeppelin provides both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing in Console: Full Use Case
&lt;/h3&gt;

&lt;p&gt;Before moving on, it's worth running the full token lifecycle manually in the Hardhat console. This is where the theory becomes tangible — you actually watch balances jump between mappings inside a deployed contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One important thing about viem in the Hardhat REPL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;viem&lt;/code&gt; object returned by &lt;code&gt;network.create()&lt;/code&gt; is not the full viem package — it's only Hardhat's internal helpers. So &lt;code&gt;viem.parseEther()&lt;/code&gt; and &lt;code&gt;viem.formatEther()&lt;/code&gt; don't exist on it. The fix:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// works&lt;/span&gt;
&lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also: &lt;code&gt;const&lt;/code&gt; declarations can't be redeclared in the same REPL session. If you get &lt;code&gt;Identifier already declared&lt;/code&gt;, just use a different variable name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Initialise clients and contract&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWalletClients&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x5FbDB2315678afecb367f032d93F642f64180aa3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Verify the pre-mint&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ownerBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Owner balance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ownerBalance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 50000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Investor buys tokens via ICO&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;investor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buyTokens&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000000000000000000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;investorBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Investor balance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;investorBalance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 2000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2 ETH × 1000 rate = 2000 MET. The math lands exactly because both ETH and MET use 18 decimals — no scaling needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Peer-to-peer transfer&lt;/strong&gt;&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;await&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;friendBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Investor remaining:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 1500&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Friend balance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;friendBalance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5 — Delegated spending: approve + transferFrom&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the OAuth2 moment. Investor grants Friend a spending allowance:&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;await&lt;/span&gt; &lt;span class="nx"&gt;tokenAsInvestor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;300&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="c1"&gt;// Verify the allowance is recorded&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allowance&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allowance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Friend acts on that allowance — pulls 100 MET from Investor's balance:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenAsFriend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokenAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;friend&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenAsFriend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transferFrom&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalAllowance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allowance&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;investor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalFriendBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;friend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Remaining allowance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalAllowance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 200&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Friend final balance:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cViem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatEther&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalFriendBalance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Final state:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Owner: 50,000 MET (pre-mint)&lt;/li&gt;
&lt;li&gt;Investor: 1,500 MET (bought 2000, transferred 500)&lt;/li&gt;
&lt;li&gt;Friend: 600 MET (received 500 + pulled 100 via transferFrom)&lt;/li&gt;
&lt;li&gt;Remaining allowance: 200 MET&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All ledger arithmetic balanced. The &lt;code&gt;approve&lt;/code&gt; + &lt;code&gt;transferFrom&lt;/code&gt; pattern is exactly how staking contracts work — the staking contract gets approved, then calls &lt;code&gt;transferFrom&lt;/code&gt; to lock your tokens. Day 5 will use this exact flow.&lt;/p&gt;

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

&lt;p&gt;Day 5: Staking — locking tokens in a contract to earn rewards over time. The &lt;code&gt;approve&lt;/code&gt; + &lt;code&gt;transferFrom&lt;/code&gt; pattern from today is exactly what makes it work.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 4 of 7.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>blockchain</category>
      <category>solidity</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 15 - DAO Voting</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Thu, 28 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-15-dao-voting-1bj2</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-15-dao-voting-1bj2</guid>
      <description>&lt;p&gt;&lt;strong&gt;🗳️ Challenge Day 3 of 7: &lt;em&gt;Arrays, Mappings, and the Gas Limit Trap&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Day 3 was about storage design — when every action with data has a real cost.&lt;/p&gt;

&lt;p&gt;I think what helped me here is that I have some background from high school in low-level programming. A long time ago I learned how to write code for microcontrollers — my first programming language was Assembly, then BASCOM, C, and C++.&lt;/p&gt;

&lt;h2&gt;
  
  
  Life Without LINQ: The Cost of Loops in Solidity
&lt;/h2&gt;

&lt;p&gt;In the .NET world we have many options for managing collections. In Solidity we have just a couple of mechanisms to work with them, and all of them must be implemented very carefully.&lt;/p&gt;

&lt;p&gt;In Solidity we have &lt;code&gt;mapping&lt;/code&gt; — looks similar to &lt;code&gt;Dictionary&amp;lt;K,V&amp;gt;&lt;/code&gt; — but behaves completely differently.&lt;/p&gt;

&lt;p&gt;A Solidity mapping has no &lt;code&gt;.Count&lt;/code&gt;. No &lt;code&gt;.Keys&lt;/code&gt;. No &lt;code&gt;.Values&lt;/code&gt;. You cannot iterate it with &lt;code&gt;foreach&lt;/code&gt;. It's a virtual hash table where every possible key in the universe pre-exists and defaults to zero. The EVM computes &lt;code&gt;keccak256(key)&lt;/code&gt; and points directly to a 32-byte storage slot. That's it.&lt;/p&gt;

&lt;p&gt;There is no collection. There is no enumeration. There is just a key and a slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Solidity More Limited Than C++?
&lt;/h2&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;Coming from C/C++, you had vectors, lists, maps, full iterator support, exceptions with catch hierarchies, floating point, recursion, full pointer control. Solidity has none of that — because of the execution model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C++ / C#  →  CPU + RAM + OS  →  resources are cheap
Solidity   →  EVM  →  every operation = real money

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

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;SLOAD&lt;/code&gt; (reading from storage) costs ~2,100 gas, the language can't afford conveniences that hide cost. Syntactic sugar that obscures what's expensive is dangerous.&lt;/p&gt;

&lt;p&gt;⚠️ The most striking example: no floating point. All financial logic uses integers:&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="c1"&gt;// "1.5 tokens" = 1_500_000_000_000_000_000&lt;/span&gt;
&lt;span class="nx"&gt;uint256&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="nx"&gt;ether&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// compiler expands to 10^18&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The reason is determinism. Every node in the network must produce byte-for-byte identical results. IEEE 754 floating point gives platform-dependent rounding — unacceptable for a consensus system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;C++ / C#&lt;/th&gt;
&lt;th&gt;Solidity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Collection iteration&lt;/td&gt;
&lt;td&gt;foreach, iterators&lt;/td&gt;
&lt;td&gt;for by index only, if you track length yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic structures&lt;/td&gt;
&lt;td&gt;vector, list, map&lt;/td&gt;
&lt;td&gt;mapping (not iterable), array (dangerous in loops)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exceptions&lt;/td&gt;
&lt;td&gt;full try/catch hierarchy&lt;/td&gt;
&lt;td&gt;revert / require — no hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Floating point&lt;/td&gt;
&lt;td&gt;float, double&lt;/td&gt;
&lt;td&gt;absent entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursion&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;td&gt;stack depth limit = 1024, gas explodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String type&lt;/td&gt;
&lt;td&gt;full object&lt;/td&gt;
&lt;td&gt;primitive — no &lt;code&gt;.length&lt;/code&gt;, no &lt;code&gt;==&lt;/code&gt; comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also looked into Rust and what we have in the Solana network — and my next priority will be a deeper investigation of Solana.&lt;/p&gt;

&lt;p&gt;These limitations are specific to the EVM. Rust is used for smart contracts on Solana and NEAR — and there the story is different. Those chains compile to WebAssembly (WASM), where Rust's zero-cost abstractions, ownership model, and low-level memory control are a natural fit. Writing a Solana program in Rust looks like systems programming — you work with raw account bytes directly. The expressive power is much greater, but the entry bar is significantly higher.&lt;/p&gt;

&lt;p&gt;Solidity is a language designed for mathematically auditable contracts where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;convenience is sacrificed for predictability&lt;/li&gt;
&lt;li&gt;syntactic sugar is sacrificed for cost transparency&lt;/li&gt;
&lt;li&gt;flexibility is sacrificed for auditability&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architectural Crime: Dynamic Arrays in Loops
&lt;/h2&gt;

&lt;p&gt;Because mappings can't be iterated, junior developers often reach for dynamic arrays to track participants, then loop through them to compute results — and commit a crime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ GAS LIMIT DoS VULNERABILITY
function countVotes(uint256 proposalId) external {
    for (uint256 i = 0; i &amp;lt; voters[proposalId].length; i++) {
        // some counting logic...
    }
}

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

&lt;/div&gt;



&lt;p&gt;In .NET, iterating 10,000 items is microseconds of CPU time. In the EVM, every iteration reads from storage — that's an &lt;code&gt;SLOAD&lt;/code&gt; opcode, and it costs real gas. Write operations inside a loop are worse — &lt;code&gt;SSTORE&lt;/code&gt; can cost 5,000 to 20,000+ gas per slot.&lt;/p&gt;

&lt;p&gt;With 2,000 voters, calling &lt;code&gt;countVotes()&lt;/code&gt; would exceed Ethereum's Block Gas Limit. The transaction becomes physically impossible to mine. The contract freezes permanently. Votes are locked forever. This is a &lt;strong&gt;Gas Limit DoS Attack&lt;/strong&gt; — written into the contract at design time, not introduced later by an attacker.&lt;/p&gt;

&lt;p&gt;Worth noting: the vulnerability doesn't require hitting the gas limit today. An unbounded loop that's safe with 100 voters becomes dangerous with 10,000. The state grows; the gas cost grows with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: O(1) Everything
&lt;/h2&gt;

&lt;p&gt;The fix is architectural: track state at the moment it changes, not after the fact.&lt;/p&gt;

&lt;p&gt;Instead of storing voters and counting later, increment counters at vote time. One voter votes → one counter increments. Loops — NEVER ❌&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Clicked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;storage&lt;/code&gt; vs &lt;code&gt;memory&lt;/code&gt; — the correct mental model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Proposal storage proposal = proposals[_proposalId]&lt;/code&gt; creates a reference to the actual storage slot. It doesn't copy the struct into transient memory — it points directly to where the data lives on-chain. When you write &lt;code&gt;proposal.votesFor += 1&lt;/code&gt;, that change persists.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Proposal memory proposal = proposals[_proposalId]&lt;/code&gt; creates a copy. Changes to that copy disappear when the function returns. Nothing is written to the blockchain. The data is silently discarded.&lt;/p&gt;

&lt;p&gt;The performance note is more subtle than "storage saves gas on writes." &lt;code&gt;storage&lt;/code&gt; pointers are useful when you need to access the same slot multiple times in one function — they avoid redundant lookups. But writes to storage (&lt;code&gt;SSTORE&lt;/code&gt;) are among the most expensive operations in the EVM. The &lt;code&gt;storage&lt;/code&gt; keyword doesn't make writes cheaper; it makes them possible and avoids unnecessary copies when reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CEI applies here too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The order inside &lt;code&gt;vote()&lt;/code&gt; matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (hasVoted[_proposalId][msg.sender]) revert AlreadyVoted(); // Check
hasVoted[_proposalId][msg.sender] = true;                      // Effect
proposal.votesFor += 1;                                        // Effect

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

&lt;/div&gt;



&lt;p&gt;State is updated before any counters change. Checks-Effects-Interactions, same principle as Day 2's &lt;code&gt;withdraw()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested mappings for O(1) duplicate detection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mapping(uint256 =&amp;gt; mapping(address =&amp;gt; bool)) public hasVoted&lt;/code&gt; — proposalId maps to a voter address maps to a boolean. Any double-vote check is a single storage lookup, regardless of how many proposals or voters exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sybil Attack Problem
&lt;/h2&gt;

&lt;p&gt;One voter, one vote sounds fair. In practice, it's fragile.&lt;/p&gt;

&lt;p&gt;An attacker generates 1,000 wallets, funds each with a tiny amount for gas, and votes 1,000 times. The contract can't distinguish them from 1,000 real participants.&lt;/p&gt;

&lt;p&gt;This is the Sybil Attack — the same identity problem that came up earlier in this series when building the voting contract from scratch. I covered it in detail in &lt;a href="https://dev.to/alenadevsoft/a-net-dinosaur-in-web3-3-7j9"&gt;Day 3 of the original series&lt;/a&gt;. The problem is unsolved at the protocol level. Most production solutions still route back to Web2 identity providers.&lt;/p&gt;

&lt;p&gt;The architectural response in real DAOs is token-weighted voting: instead of &lt;code&gt;votesFor += 1&lt;/code&gt;, use &lt;code&gt;votesFor += tokenContract.balanceOf(msg.sender)&lt;/code&gt;. Controlling 1,000 empty wallets doesn't help if voting power requires holding tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  block.timestamp — The Honest Picture
&lt;/h2&gt;

&lt;p&gt;The contract uses &lt;code&gt;block.timestamp&lt;/code&gt; to enforce voting deadlines. The honest picture after PoS is more nuanced than the usual "validators can manipulate timestamps" warning.&lt;/p&gt;

&lt;p&gt;After The Merge, Ethereum time is divided into strict 12-second slots. Each validator is assigned a slot and is expected to produce a block at that slot's designated time. They cannot arbitrarily drift the timestamp by seconds — the timestamp must be greater than the parent block's timestamp and consistent with the slot timing. The "15-second manipulation window" that existed under Proof-of-Work no longer applies.&lt;/p&gt;

&lt;p&gt;The realistic manipulation vector in PoS is a validator skipping a slot — which shifts the timestamp by 12 seconds. For DAO voting periods that run for days or weeks, this is completely irrelevant. A 12-second drift doesn't change the outcome of a vote that closes in 7 days.&lt;/p&gt;

&lt;p&gt;Timestamp manipulation becomes a real concern only for hyper-sensitive financial logic — MEV opportunities, flash loans, contracts where a single block matters. For governance and voting, &lt;code&gt;block.timestamp&lt;/code&gt; is the correct and standard approach.&lt;/p&gt;

&lt;p&gt;The advice to use &lt;code&gt;block.number&lt;/code&gt; instead is outdated. In PoS, skipped slots mean block numbers don't map reliably to real-world time — gaps accumulate over long periods. &lt;code&gt;block.timestamp&lt;/code&gt; is more accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing The Contract
&lt;/h3&gt;

&lt;p&gt;After a clean deploy, the full happy path in the console:&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="nx"&gt;npx&lt;/span&gt; &lt;span class="nx"&gt;hardhat&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="nx"&gt;localhost&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;deployer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voter1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voter2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWalletClients&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Connect to the contract&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daoAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x5FbDB2315678afecb367f032d93F642f64180aa3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dao&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SimpleVotingDAO&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;daoAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Create a proposal - duration: 24 hours 86400 sec&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createProposal&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Should we officially declare 2026 the year of Hardhat 3?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. switch the account and vote&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daoAsVoter1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;viem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SimpleVotingDAO&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;daoAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;voter1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;daoAsVoter1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. checking the poposal state of index 0&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proposals&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing Time-Dependent Logic
&lt;/h2&gt;

&lt;p&gt;Testing deadline enforcement requires moving the blockchain clock forward. Hardhat's local network supports this:&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="c1"&gt;// In Hardhat 3, networkHelpers.time.increase() shifts the local chain forward&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;networkHelpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// advance 65 seconds&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This is the blockchain equivalent of mocking &lt;code&gt;DateTime.UtcNow&lt;/code&gt; via &lt;code&gt;ITimeProvider&lt;/code&gt; in .NET — same problem, different mechanism. The underlying local node (&lt;code&gt;npx hardhat node&lt;/code&gt;) exposes &lt;code&gt;evm_increaseTime&lt;/code&gt; and &lt;code&gt;evm_mine&lt;/code&gt; RPC commands. Hardhat wraps them in a clean API.&lt;/p&gt;

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

&lt;p&gt;Day 4: ERC-20 Token — the standard interface that powers most of DeFi, and what implementing a standard looks like in Solidity.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 3 of 7.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>solidity</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 14 — Pull Pattern vs Dangerous Push Payments</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Wed, 27 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-14-pull-pattern-vs-dangerous-push-payments-2a2o</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-14-pull-pattern-vs-dangerous-push-payments-2a2o</guid>
      <description>&lt;h3&gt;
  
  
  &lt;strong&gt;🧐 Challenge Day 2 of 7: &lt;em&gt;Why Push Payments Will Ruin Your dApp (Pull-over-Push &amp;amp; Escrow)&lt;/em&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Today's learning topic left me confused — in the best possible way. It was hard, but interesting and genuinely challenging, because it starts to touch real risks and real money. The main question of the day: how do you get funds out of a contract safely?&lt;/p&gt;

&lt;p&gt;The answer is less obvious than it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In smart contracts, automatically distributing ETH to external addresses within core state-changing functions is called the &lt;strong&gt;Push Pattern&lt;/strong&gt;. In reality, it's a dangerous architectural flaw.&lt;/p&gt;

&lt;p&gt;If a contract attempts to push funds to an external address and that address fails to accept the transfer, the entire transaction reverts. In a naive implementation, a single failed transfer blocks the administrative function — causing a &lt;strong&gt;Denial of Service (DoS)&lt;/strong&gt; condition and permanently locking everyone's assets inside the contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ BAD — Push Pattern
function release() external onlyArbiter {
    (bool success, ) = seller.call{value: balance}("");
    require(success, "Transfer failed");
    // If seller's contract reverts, this entire function is blocked forever
}

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Contract: SimpleEscrow
&lt;/h2&gt;

&lt;p&gt;Three participants: a buyer, a seller, and an arbiter. The buyer deposits ETH. The arbiter either releases funds to the seller or refunds the buyer. Nobody pushes anything — the seller and buyer pull their own funds when they're ready.&lt;br&gt;
&lt;/p&gt;

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

contract SimpleEscrow {
    error OnlyArbiter();
    error InvalidState();
    error TransferFailed();
    error NothingToWithdraw();

    enum State { AwaitingPayment, AwaitingDelivery, Completed, Refunded }

    address public immutable buyer;
    address public immutable seller;
    address public immutable arbiter;

    uint256 public immutable amount;
    State public currentState;

    mapping(address =&amp;gt; uint256) public balanceOf;

    event Deposited(address indexed buyer, uint256 amount);
    event Released();
    event RefundedToBuyer();
    event Withdrawn(address indexed payee, uint256 amount);

    modifier onlyArbiter() {
        if (msg.sender != arbiter) revert OnlyArbiter();
        _;
    }

    constructor(address _seller, address _arbiter, uint256 _amount) {
        buyer = msg.sender;
        seller = _seller;
        arbiter = _arbiter;
        amount = _amount;
        currentState = State.AwaitingPayment;
    }

    function deposit() external payable {
        if (msg.sender != buyer) revert InvalidState();
        if (currentState != State.AwaitingPayment) revert InvalidState();
        if (msg.value != amount) revert InvalidState();
        currentState = State.AwaitingDelivery;
        emit Deposited(buyer, msg.value);
    }

    function release() external onlyArbiter {
        if (currentState != State.AwaitingDelivery) revert InvalidState();
        currentState = State.Completed;
        balanceOf[seller] += amount;
        emit Released();
    }

    function refund() external onlyArbiter {
        if (currentState != State.AwaitingDelivery) revert InvalidState();
        currentState = State.Refunded;
        balanceOf[buyer] += amount;
        emit RefundedToBuyer();
    }

    function withdraw() external {
        // 1. Checks
        uint256 payment = balanceOf[msg.sender];
        if (payment == 0) revert NothingToWithdraw();

        // 2. Effects — zero out BEFORE sending
        balanceOf[msg.sender] = 0;

        // 3. Interactions — low-level call, no gas limit
        (bool success, ) = msg.sender.call{value: payment}("");
        if (!success) revert TransferFailed();

        emit Withdrawn(msg.sender, payment);
    }
}

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pull-over-Push: The Bulkhead Pattern
&lt;/h2&gt;

&lt;p&gt;Coming from an enterprise .NET background, the natural instinct is to reach for fault tolerance patterns. But there's an important distinction to make here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit Breaker&lt;/strong&gt; (via Polly in .NET) stops all traffic to a failing downstream service globally. If the payment gateway breaks, the Circuit Breaker opens — no requests go through for anyone until it recovers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulkhead&lt;/strong&gt; is different. It isolates resources so a failure in one compartment doesn't sink the ship. Inspired by watertight compartments in a ship's hull: if one section floods, the bulkhead contains it. The rest of the ship stays dry.&lt;/p&gt;

&lt;p&gt;Pull-over-Push is a Bulkhead implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;release()&lt;/code&gt; and &lt;code&gt;refund()&lt;/code&gt; only modify internal state — &lt;code&gt;balanceOf[user]&lt;/code&gt;. This is a guaranteed EVM storage operation. Nothing external, nothing that can fail.&lt;/li&gt;
&lt;li&gt;The actual ETH movement is deferred to &lt;code&gt;withdraw()&lt;/code&gt;, executed independently by the payee.&lt;/li&gt;
&lt;li&gt;If a recipient's contract is broken, the failure is strictly contained within their transaction. Other participants and core contract logic are completely unaffected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;.transfer()&lt;/code&gt; Is Dead
&lt;/h2&gt;

&lt;p&gt;Tutorials recommend &lt;code&gt;payee.transfer(amount)&lt;/code&gt; because it limits forwarded gas to 2,300 units, theoretically preventing reentrancy. But this is wrong — and the Istanbul hardfork of 2019 shows the concrete reason why.&lt;/p&gt;

&lt;p&gt;Ethereum core developers increased the gas cost of the &lt;code&gt;SLOAD&lt;/code&gt; opcode (storage reads). Overnight, thousands of production contracts using &lt;code&gt;.transfer()&lt;/code&gt; broke — because honest recipient contracts suddenly needed more than 2,300 gas.&lt;/p&gt;

&lt;p&gt;Gnosis Safe is the canonical example. It's a multi-signature corporate wallet where transactions require approval from multiple signers. When receiving ETH, it runs internal verification checks — storage reads. After Istanbul, those checks cost more than 2,300 gas. Every &lt;code&gt;.transfer()&lt;/code&gt; to a Gnosis Safe wallet started failing with Out of Gas.&lt;/p&gt;

&lt;p&gt;The bad idea was the hardcoded gas limit. Tying runtime gas expectations into deployed bytecode violates loose coupling. The EVM evolves; contracts with hardcoded gas assumptions become bricks.&lt;/p&gt;

&lt;p&gt;The correct approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(bool success, ) = target.call{value: amount}("");
if (!success) revert TransferFailed();

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.call&lt;/code&gt; forwards all remaining gas. The EVM can change opcode pricing; the contract adapts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One critical note:&lt;/strong&gt; &lt;code&gt;.call&lt;/code&gt; does not revert on failure — it returns a boolean. The &lt;code&gt;if (!success) revert&lt;/code&gt; check is mandatory. Omitting it means the Effects step has already zeroed the balance with no automatic rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checks-Effects-Interactions (CEI)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;withdraw()&lt;/code&gt; function enforces CEI strictly to prevent reentrancy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checks&lt;/strong&gt; — validate that the caller has a positive balance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effects&lt;/strong&gt; — zero out storage state &lt;em&gt;before&lt;/em&gt; initiating the transfer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactions&lt;/strong&gt; — trigger the external call&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A natural question: if we zero out the balance first and then &lt;code&gt;.call&lt;/code&gt; fails, does the user lose their money?&lt;/p&gt;

&lt;p&gt;No — and this is where EVM atomicity comes in. The blockchain equivalent of ACID database transactions: if any part of a transaction reverts, all state changes within that transaction are rolled back. The zeroed balance reverts to its previous value.&lt;/p&gt;

&lt;p&gt;But this only applies if the failure actually causes a revert. Since &lt;code&gt;.call&lt;/code&gt; returns a boolean instead of reverting, you must check it and revert manually. Without &lt;code&gt;if (!success) revert TransferFailed()&lt;/code&gt;, the balance stays zeroed and the ETH never moves. CEI without the explicit failure check is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Contract
&lt;/h2&gt;

&lt;p&gt;⚠️ Do not make the same mistake I did.&lt;/p&gt;

&lt;p&gt;When testing this contract on a local node, my first attempts kept failing. The cause: I had been running the local node continuously, and after redeploying — with different parameters during experimentation — Ignition was using cached state from a previous run. The contract on-chain had &lt;code&gt;amount&lt;/code&gt; set to something different from what the console session expected.&lt;/p&gt;

&lt;p&gt;The fix: reset everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Stop the running node (Ctrl+C), then restart it fresh
npx hardhat node

# Deploy with --reset to force Ignition to ignore its journal
npx hardhat ignition deploy ignition/modules/SimpleEscrow.ts --network localhost --reset

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

&lt;/div&gt;



&lt;p&gt;After a clean deploy, the full happy path in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx hardhat console --network localhost

const { viem } = await network.create();
const [buyer, seller, arbiter] = await viem.getWalletClients();

const escrowAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const escrow = await viem.getContractAt("SimpleEscrow", escrowAddress);

// Buyer deposits exactly 1 ETH
await escrow.write.deposit([], { value: 1000000000000000000n });

// State should be 1 (AwaitingDelivery)
await escrow.read.currentState();

// Arbiter releases funds — only updates balanceOf, no ETH moves yet
const escrowAsArbiter = await viem.getContractAt("SimpleEscrow", escrowAddress, { client: { wallet: arbiter } });
await escrowAsArbiter.write.release();

// Seller pulls their funds
const escrowAsSeller = await viem.getContractAt("SimpleEscrow", escrowAddress, { client: { wallet: seller } });
await escrowAsSeller.write.withdraw();

// Seller's balance inside the contract should now be 0n
await escrow.read.balanceOf([seller.account.address]);

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

&lt;/div&gt;



&lt;p&gt;The sequence maps exactly to the Pull-over-Push design: &lt;code&gt;release()&lt;/code&gt; only changes numbers in a mapping, &lt;code&gt;withdraw()&lt;/code&gt; is where ETH actually moves — initiated by the seller, on their own terms.&lt;/p&gt;

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

&lt;p&gt;Day 3: DAO Voting — dynamic arrays, struct mappings, and on-chain governance logic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft/solidity-7days-challenge/tree/main/contracts/Day2_Escrow" rel="noopener noreferrer"&gt;Day 2 of 7: Escrow contract&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 2 of 7.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>blockchain</category>
      <category>solidity</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 13 — Access Control</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Tue, 26 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-13-access-control-3l89</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-13-access-control-3l89</guid>
      <description>&lt;p&gt;🆕 &lt;strong&gt;New Challenge. Day 1 of 7: &lt;em&gt;Access Control &amp;amp; Vault Management&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After building two MVPs around smart contracts, I wanted to go deeper into the contracts themselves. Not just use them — understand them.&lt;/p&gt;

&lt;p&gt;🙈 7 days.&lt;br&gt;
😉 7 contracts.&lt;br&gt;
💪 7 articles.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;By default, every function in a deployed smart contract is public. Anyone on the network can call anything. If you leave administrative functions — like changing rates, withdrawing funds, pausing the contract — without protection, anyone can call them. It's how real exploits happen.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Contract: AccessControlledVault
&lt;/h2&gt;

&lt;p&gt;The concept is straightforward: one address is the owner, set at deployment. Certain functions are restricted to that address only. Final version of this contract:&lt;br&gt;
&lt;/p&gt;

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

contract AccessControlledVault {
    // Owner address
    address public owner;

    // Rate variable
    uint256 public conversionRate;

    // Custom error for gas optimization
    error NotAnOwner();

    // Event emitted when the rate changes
    event RateChanged(uint256 newRate);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    // Constructor — sets the deployer as the owner
    constructor() {
        owner = msg.sender;
    }

    // Modifier using custom error instead of require
    modifier onlyOwner() {
        if (msg.sender != owner) {
            revert NotAnOwner();
        }
        _;
    }

    // Apply the modifier and emit the event
    function setRate(uint256 _newRate) public onlyOwner {
        conversionRate = _newRate;
        emit RateChanged(_newRate);
    }

    function renounceOwnership() external onlyOwner {
        emit OwnershipTransferred(owner, address(0));
        owner = address(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Actually Clicked
&lt;/h2&gt;

&lt;p&gt;Some things I'd learned before, but some I'd missed or hadn't gone deeper into. Some are just reminders, some were genuinely new.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Already knew — nothing new:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;constructor&lt;/code&gt; runs exactly once at deployment. &lt;code&gt;msg.sender&lt;/code&gt; at that moment is the deployer's address. Capture it then and it's stored permanently on-chain. There's no other moment to do this cleanly.&lt;/p&gt;

&lt;p&gt;The .NET analogy: a static initialiser that runs before anything else, with access to who triggered the class load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New things:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;modifier&lt;/code&gt; is a reusable wrapper. Without modifiers, every protected function needs its own &lt;code&gt;require&lt;/code&gt; or &lt;code&gt;if&lt;/code&gt; check. With a modifier, the check is defined once and applied by name. &lt;code&gt;onlyOwner&lt;/code&gt; reads like a type annotation on the function — the intent is immediately visible.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;_;&lt;/code&gt; placement matters. The underscore marks where the function body executes relative to the modifier's checks. Put &lt;code&gt;_;&lt;/code&gt; before the check, and the function runs first — the check happens after. In a function that transfers funds, this is a critical security vulnerability: the money moves before the authorisation check fires.&lt;/p&gt;

&lt;p&gt;Custom errors vs &lt;code&gt;require&lt;/code&gt; strings. &lt;code&gt;revert NotAnOwner()&lt;/code&gt; costs less gas than &lt;code&gt;require(msg.sender == owner, "Not an owner")&lt;/code&gt;. The string in &lt;code&gt;require&lt;/code&gt; gets stored and returned on every failed call. A custom error is just a 4-byte selector. At scale, the difference adds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modifiers
&lt;/h2&gt;

&lt;p&gt;In .NET there's no direct analogy to Solidity modifiers, but there are a couple of things that can help a .NET developer understand the concept:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Authorize]&lt;/code&gt; — a decorator that restricts access. And Middleware — you can inject custom access restriction logic at any point in the runtime pipeline. &lt;code&gt;_;&lt;/code&gt; and &lt;code&gt;await _next(context)&lt;/code&gt; are conceptually the same thing: pass control to what comes next. That's where the similarity ends.&lt;/p&gt;

&lt;p&gt;A Solidity modifier is the logic. It wraps the function, controls entry, can control exit. It's active behaviour, not a passive label.&lt;/p&gt;

&lt;p&gt;Solidity modifiers are compile-time macros. The compiler takes the modifier code and physically inlines it into every function that uses it, replacing &lt;code&gt;_;&lt;/code&gt; with the function body. There is no runtime pipeline. There is no shared instance. Just flat bytecode.&lt;/p&gt;

&lt;p&gt;This has a practical consequence: &lt;strong&gt;EIP-170&lt;/strong&gt;. Ethereum limits deployed contract bytecode to 24,576 bytes. If your modifier contains complex logic and you apply it to 15 functions, that logic is duplicated 15 times in the bytecode. Contracts can fail to deploy because they're too large.&lt;/p&gt;

&lt;p&gt;The fix: extract heavy logic into an &lt;code&gt;internal&lt;/code&gt; function. The modifier then inlines only a function call — a single jump instruction — not the full check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _validateAccess() internal view {
    require(msg.sender == owner, "Not owner");
    require(isActive, "Not active");
}

modifier onlyOwner() {
    _validateAccess(); // only the jump is inlined
    _;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: middleware is the right mental model for &lt;em&gt;what&lt;/em&gt; modifiers do. But the &lt;em&gt;how&lt;/em&gt; is nothing like middleware — it's a compiler feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Node — No Testnet Needed
&lt;/h2&gt;

&lt;p&gt;When I first started with Web3, my testing environment was... my MetaMask wallet, hunting for Sepolia ETH from faucets, and struggling with Remix IDE. Alchemy was part of the setup too.&lt;/p&gt;

&lt;p&gt;Turns out Hardhat has a built-in local node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx hardhat node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up a local blockchain with 20 test accounts, each pre-funded with 1,000 ETH. No faucets, no limits — very useful. The network exists only while the process runs.&lt;/p&gt;

&lt;p&gt;To deploy inside the local network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx hardhat ignition deploy ignition/modules/AccessControlledVault.ts --network localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the Hardhat console lets you interact with the live contract directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx hardhat console --network localhost

const { viem } = await network.create();
const vault = await viem.getContractAt("AccessControlledVault", "0x5FbDB...");

// Read owner
await vault.read.owner();

// Change rate as owner
await vault.write.setRate([420n]);
await vault.read.conversionRate(); // → 420n

// Try as non-owner — should revert
const [owner, addr1] = await viem.getWalletClients();
const vaultAsAddr1 = await viem.getContractAt("AccessControlledVault", "0x5FbDB...", { client: { wallet: addr1 } });
await vaultAsAddr1.write.setRate([999n]); // → ContractFunctionExecutionError: NotAnOwner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The revert fires. The custom error name appears in the output. Access control works.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Thing: renounceOwnership
&lt;/h2&gt;

&lt;p&gt;After writing the contract, an architectural question came up: what happens to the owner role at the end of the contract's life? Can ownership be revoked permanently?&lt;/p&gt;

&lt;p&gt;It can. And in production Web3, it often should be.&lt;/p&gt;

&lt;p&gt;In .NET systems, there's always a super-admin — someone who can access the database directly in an emergency. In Web3, that kind of absolute power is a red flag for users and investors. If the contract manages significant funds and the owner holds a &lt;code&gt;setRate&lt;/code&gt; or &lt;code&gt;withdraw&lt;/code&gt; function, two risks exist: the private key gets stolen, or the developers pull the funds themselves.&lt;/p&gt;

&lt;p&gt;The solution: &lt;code&gt;renounceOwnership()&lt;/code&gt;. When the project is live and all settings are locked, the owner calls this function. It permanently overwrites the &lt;code&gt;owner&lt;/code&gt; variable with the zero address — &lt;code&gt;address(0)&lt;/code&gt;. No one controls the contract after that. Not even its creator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * @notice Leaves the contract without owner.
 * @dev It will not be possible to call `onlyOwner` functions anymore.
 * Can only be called by the current owner.
 */
function renounceOwnership() external onlyOwner {
    emit OwnershipTransferred(owner, address(0));
    owner = address(0);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event is important — analytics services like Etherscan use it to display that the contract has no owner.&lt;/p&gt;

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

&lt;p&gt;Day 2: Escrow &amp;amp; Pull-over-Push Pattern.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — going deeper into the bedrock. Day 1 of 7.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>blockchain</category>
      <category>smartcontract</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 11-12. Two projects, One Stack and What’s Next</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Mon, 25 May 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-11-12-two-projects-one-stack-and-whats-next-35kb</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-11-12-two-projects-one-stack-and-whats-next-35kb</guid>
      <description>&lt;h2&gt;
  
  
  A .NET Dinosaur in Web3 — Days 11–12: Two Projects, One Stack, and What's Next
&lt;/h2&gt;

&lt;p&gt;While building WishList Chain, I decided to dive deeper into transaction flows within decentralised trading systems and build something completely different. Initially, I wasn't sure about making this second project public, as building a commercial product wasn't my goal. However, I eventually decided to share it because it turned out to be a very good example of how vastly different development paths can be hidden behind the magical umbrella term "Web3."&lt;/p&gt;

&lt;h2&gt;
  
  
  Another Direction
&lt;/h2&gt;

&lt;p&gt;While WishList Chain is a fun, game-like project focused heavily on on-chain interactions — contracts, goals, and donations — the second project approaches the space from a completely different angle. It is far more practical, real-time, and closely tied to trading and finance. The "serious stuff."&lt;/p&gt;

&lt;p&gt;The core idea is straightforward: you follow a specific wallet, and the moment it makes a move, you get notified. Right now it can track any wallet, but the long-term vision is to track "smart money" wallets and understand patterns behind their moves, get real-time alerts on potential issues, or gain insights into why someone made a specific move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Money Tracker AI
&lt;/h2&gt;

&lt;p&gt;The MVP is intentionally simple. Since this is uncharted territory for me, I focused purely on the foundational flow.&lt;/p&gt;

&lt;p&gt;A Telegram bot monitors Ethereum wallets and fires alerts on new transactions. Instead of serving raw, cryptic data, every transaction passes through an AI layer that interprets what actually happened and why it matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🐋 Smart wallet activity

Wallet: 0xd8da...6045 (Ethereum)
Type: Swap · Value: ~$45,200

🤖 AI Insight: This wallet rotated a large ETH position into stablecoins.
Combined with three similar moves this week, this looks like a risk-off
signal — possibly bracing for macro uncertainty.

⚠️ Risk: Medium
💡 Actionable info: If you're holding a sizable long ETH position,
this is worth watching closely.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fo4fsvjwolzm610dina8i.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%2Fo4fsvjwolzm610dina8i.png" alt="Real screen" width="399" height="438"&gt;&lt;/a&gt;&lt;br&gt;
The bot is live and completely free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web:&lt;/strong&gt; &lt;a href="https://smart-money-tracker-ai-web.vercel.app" rel="noopener noreferrer"&gt;smart-money-tracker-ai-web.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sign up → Connect Telegram → &lt;code&gt;/follow [wallet_address]&lt;/code&gt; → Done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Same Stack — Different System
&lt;/h2&gt;

&lt;p&gt;Both projects leverage almost identical tech stacks: Next.js, Supabase, Alchemy, Telegram, and the Claude API. However, their architectural blueprints are very different.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;WishList Chain&lt;/th&gt;
&lt;th&gt;Smart Money Tracker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Mechanic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Users create goals and receive donations&lt;/td&gt;
&lt;td&gt;System monitors wallets and fires alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blockchain Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Backend — reads and writes state&lt;/td&gt;
&lt;td&gt;Data source — listens to events only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates commentary on donations&lt;/td&gt;
&lt;td&gt;Analyses transactions with risk context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue System&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None needed&lt;/td&gt;
&lt;td&gt;BullMQ on Upstash Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trigger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User-initiated actions&lt;/td&gt;
&lt;td&gt;On-chain events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Testnet MVP&lt;/td&gt;
&lt;td&gt;Live on Ethereum Mainnet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Shifting Perspectives
&lt;/h2&gt;

&lt;p&gt;My biggest takeaway was realising that Web3 is far from a single, standardised tech stack. It's an expansive, often chaotic ecosystem where you can design fully on-chain systems, launch hybrid architectures, or treat the blockchain merely as an immutable data feed. Navigating these fluid standards felt like entering uncharted territory.&lt;/p&gt;

&lt;p&gt;Yet, to my surprise, my .NET background proved highly transferable. The same engineering principles still apply — event-driven design, separation of concerns, modularity. The syntax required an evolution. The core architectural patterns did not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Current State
&lt;/h2&gt;

&lt;p&gt;Both projects are functional MVPs, but still quite raw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WishList Chain&lt;/strong&gt; — on-chain goals, donations, DreamPower mechanics. What's next: donation history, analytical dashboards, refined DreamPower logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart Money Tracker&lt;/strong&gt; — real-time wallet tracking, AI commentary, Telegram alerts. What's next: multi-chain support (Base + Arbitrum + Solana), smart wallet discovery, weekly digest summaries.&lt;/p&gt;

&lt;p&gt;I won't stop working on either — both deserve to grow. The plan is to ship new features for each project roughly twice a week.&lt;/p&gt;
&lt;h2&gt;
  
  
  Next Step — Back to Contracts
&lt;/h2&gt;

&lt;p&gt;After building infrastructure around contracts, I want to go deeper into the smart contracts themselves. The next chapter: &lt;strong&gt;7 days — 7 contracts — 7 articles.&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;Day 1 — Access Control (Ownable, Modifiers)
Day 2 — Escrow &amp;amp; Pull-over-Push Pattern
Day 3 — DAO Voting Systems
Day 4 — ERC-20 Token Implementation
Day 5 — Staking Mechanisms
Day 6 — ERC-721 NFT Basics
Day 7 — Reentrancy Protection &amp;amp; Security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All repositories are open source: &lt;a href="https://github.com/alena-dev-soft" rel="noopener noreferrer"&gt;github.com/alena-dev-soft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — two systems live. Now going deeper into the bedrock.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>blockchain</category>
      <category>smartcontract</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 9–10 — MVP of WishList Chain</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sat, 23 May 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-9-10-mvp-of-wishlist-chain-4fpl</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-9-10-mvp-of-wishlist-chain-4fpl</guid>
      <description>&lt;p&gt;&lt;em&gt;"It's alive!!! It's alive!!!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Days 9 and 10 were about infrastructure — building the bridge between the smart contract and the real world. Supabase, Drizzle, Claude API, a Telegram bot, and a Vercel deploy are classic Web2 tools. But the magic ingredient was &lt;strong&gt;Alchemy webhooks — the definitive Web3 piece&lt;/strong&gt; that connected the blockchain straight into my traditional stack. All of it was new to me.&lt;/p&gt;

&lt;p&gt;And honestly… that was the most interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Got Built
&lt;/h2&gt;

&lt;p&gt;The full pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Donation → Sepolia → Alchemy webhook →
Next.js API → Claude AI → Supabase →
Telegram Bot → notification to owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live at &lt;a href="https://wish-list-chain.vercel.app" rel="noopener noreferrer"&gt;wish-list-chain.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app is running on Sepolia (testnet). To try it, make sure your wallet is connected to Sepolia and use test ETH only.&lt;/p&gt;

&lt;p&gt;How to set up Sepolia correctly and get test funds: &lt;a href="https://github.com/alena-dev-soft/wish-list-chain/blob/main/GETTING_STARTED.md" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wish-list-chain/blob/main/GETTING_STARTED.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase + Drizzle
&lt;/h2&gt;

&lt;p&gt;I've worked with .NET EF (Core and Framework) for years. Drizzle is the closest thing to it in the TypeScript ecosystem — and I got familiar with it much faster than I expected.&lt;/p&gt;

&lt;p&gt;The schema is just TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;donations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;donations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;defaultRandom&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;goalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;goal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;donorAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;donor_address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tx_hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;aiComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai_comment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;defaultNow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;notNull&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;Then &lt;code&gt;npx drizzle-kit generate&lt;/code&gt; produces a SQL migration file. &lt;code&gt;npx drizzle-kit migrate&lt;/code&gt; runs it against the database. That's it. Very simple.&lt;/p&gt;

&lt;p&gt;The .NET analogy is exact: &lt;code&gt;dotnet ef migrations add&lt;/code&gt; followed by &lt;code&gt;dotnet ef database update&lt;/code&gt;. Same mental model, different syntax.&lt;/p&gt;

&lt;p&gt;Supabase also has a Schema Visualizer that automatically draws relationships between tables after migration. Small thing — but satisfying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alchemy Webhooks — The Blockchain Calls You
&lt;/h2&gt;

&lt;p&gt;This was the conceptual shift of the sprint.&lt;/p&gt;

&lt;p&gt;Instead of polling for changes — asking "did something happen?" on a schedule — the system becomes event-driven. With Alchemy webhooks, the blockchain effectively calls you.&lt;/p&gt;

&lt;p&gt;Every time a &lt;code&gt;DreamPowerIncreased&lt;/code&gt; event fires on the contract, Alchemy sends a POST request to our API route with the raw log data. We decode it with viem's &lt;code&gt;decodeEventLog&lt;/code&gt;, extract the donor address, goal index, and amount, and write it to Supabase.&lt;/p&gt;

&lt;p&gt;The GraphQL filter in Alchemy is precise — we only listen for logs from our specific contract address. Everything else on Sepolia is ignored.&lt;/p&gt;

&lt;p&gt;Testing locally required ngrok. This is a pattern that comes up in any webhook development — your localhost is not publicly accessible. ngrok solves it by giving you a public HTTPS URL that forwards to your local port. One command, done.&lt;/p&gt;

&lt;p&gt;One thing worth noting: if the webhook fires and the handler throws an error, the transaction still happened. The ETH moved. The event was emitted. The blockchain doesn't care that your API crashed. Your handler needs to be idempotent — &lt;code&gt;onConflictDoNothing()&lt;/code&gt; on the donations insert handles duplicate deliveries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude API — AI Comments on Donations
&lt;/h2&gt;

&lt;p&gt;Each donation triggers a call to Claude with the goal name and amount. It returns a short encouraging comment, which gets saved and later included in the Telegram notification.&lt;/p&gt;

&lt;p&gt;The integration itself is straightforward — one API call, structured prompt, short response. At this scale, cost is negligible (around $0.002 per comment), so it's not a concern for MVP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telegram Bot — grammY
&lt;/h2&gt;

&lt;p&gt;The bot is intentionally minimal — just outbound notifications for now. When a donation happens, it sends a message with the goal, amount, sender, and the AI-generated comment. grammY is clean and easy to work with.&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%2Fpdrn1yssx2i7jakxv6xb.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%2Fpdrn1yssx2i7jakxv6xb.png" alt="Tg bot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Vercel — Not Just Hosting
&lt;/h2&gt;

&lt;p&gt;I didn't have much experience with Vercel before this. Coming from an Azure background, I expected another cloud platform with configuration overhead.&lt;/p&gt;

&lt;p&gt;It turned out to be much simpler. Connect a GitHub repo, set environment variables, deploy. Vercel detects Next.js automatically, builds it, and gives you a working URL. The whole process took just a few minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing that came up during deployment:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Direct Postgres connections can behave unexpectedly in a serverless environment. It's not that they don't work — but they're not designed for short-lived execution. Each function invocation may try to establish a new connection, which doesn't always play well with how serverless functions scale. Using Supabase's transaction pooler solved the issue immediately.&lt;/p&gt;

&lt;p&gt;This is less about Vercel specifically and more about the execution model. Instead of a long-running process managing connections, you have short-lived functions that start, execute, and stop. It's a small detail — but one that can be confusing if you approach it with a traditional backend mindset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment It All Connected
&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.amazonaws.com%2Fuploads%2Farticles%2F595ry43gidiehp37fqfu.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%2F595ry43gidiehp37fqfu.png" alt="Final resul"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a point where things stop feeling like separate pieces and start behaving like a system.&lt;/p&gt;

&lt;p&gt;For me, it was surprisingly simple.&lt;/p&gt;

&lt;p&gt;Open the app. Connect MetaMask. Click "Donate". Confirm the transaction. Wait a bit.&lt;/p&gt;

&lt;p&gt;And then — a Telegram notification arrives.&lt;/p&gt;

&lt;p&gt;Just because everything is connected.&lt;/p&gt;

&lt;p&gt;It's a strange feeling to see even a small project come alive in the cloud. You can open it, connect your wallet, send a donation to a goal — and it just works. The contract, the frontend, the notifications — all reacting to each other.&lt;/p&gt;

&lt;p&gt;Right now everything is still pretty raw. Over the next few days I want to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a proper donations list&lt;/li&gt;
&lt;li&gt;maybe some simple dashboards&lt;/li&gt;
&lt;li&gt;and rethink how DreamPower actually grows as more donations come in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because if the chain is supposed to reflect collective support, then the way this "power" is calculated should probably evolve as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Sprint Taught Me
&lt;/h2&gt;

&lt;p&gt;The Web3 part of this project — the contract, wallet connection, on-chain reads and writes — was actually done a few days ago. What followed was everything around it. Infrastructure. Integrations. Small details that make the system usable.&lt;/p&gt;

&lt;p&gt;And that's probably the most honest picture of what building a dApp looks like. The contract handles trust and transparency. Everything else — storage, notifications, AI, hosting — is still very much traditional engineering.&lt;/p&gt;

&lt;p&gt;The .NET dinosaur is still needed. Just… in a slightly different ecosystem.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft/wish-list-chain" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wish-list-chain&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — MVP shipped. Testnet live. The system works.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>nextjs</category>
      <category>showdev</category>
      <category>web3</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 8 — Reading &amp; Writing — WishList Chain</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Thu, 21 May 2026 16:30:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-8-reading-writing-wishlist-chain-4c4o</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-8-reading-writing-wishlist-chain-4c4o</guid>
      <description>&lt;p&gt;Long time no see.&lt;/p&gt;

&lt;p&gt;The dinosaur was a bit busy finishing a module on .NET Windows Forms. To be honest, it's not my favourite stack — I'm much more into web development — but it's part of the job.&lt;/p&gt;

&lt;p&gt;Now that I'm done with those tasks, I can finally get back to my favourite projects and the whole learn-in-public vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Day 8 of trying to get into Web3 turned out to be an amazing experience.&lt;/p&gt;

&lt;p&gt;It didn't feel like just another step where you "add a feature and move on." It felt more like a point where the system actually starts behaving like a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Reading to Writing
&lt;/h2&gt;

&lt;p&gt;Reading from the blockchain feels almost like calling a regular API. You ask for data, you get data, you render it.&lt;/p&gt;

&lt;p&gt;Writing introduces a completely different flow.&lt;/p&gt;

&lt;p&gt;Now there's a wallet involved. The user has to confirm the action. The transaction is sent to the network, included in a block, and only then reflected in the UI — and you don't control that timeline anymore.&lt;/p&gt;

&lt;p&gt;At that point it became obvious that building a UI for Web3 is not just about displaying data. It's about handling uncertainty, delays, and state that lives somewhere outside of your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weird Environment Reality
&lt;/h2&gt;

&lt;p&gt;It seems I might need a more modern laptop — my MacBook almost gave up because of Turbopack. I used it initially, but it was consuming far more resources than expected.&lt;/p&gt;

&lt;p&gt;Nothing critical — I switched back to good old webpack.&lt;/p&gt;

&lt;p&gt;But if you're working on an older machine, it's definitely something to be aware of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reality of Building UI
&lt;/h2&gt;

&lt;p&gt;The UI is no longer just reactive — it becomes dependent on external events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user confirms the transaction (or not)&lt;/li&gt;
&lt;li&gt;the network processes it&lt;/li&gt;
&lt;li&gt;the block is mined&lt;/li&gt;
&lt;li&gt;the state becomes available
And only then can your UI reflect what actually happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This introduces a different kind of thinking. You're not just updating state — you're waiting for the system to converge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Actually Matter
&lt;/h2&gt;

&lt;p&gt;There was a moment where the transaction clearly went through — MetaMask confirmed it — but the UI still showed "No goals yet."&lt;/p&gt;

&lt;p&gt;The contract state had changed, but nothing triggered a refetch on the frontend. From the UI perspective, nothing happened.&lt;/p&gt;

&lt;p&gt;And then there was the more serious one.&lt;/p&gt;

&lt;p&gt;At some point I realised I was sending transactions to the wrong contract address.&lt;/p&gt;

&lt;p&gt;Everything looked correct. MetaMask confirmed the transaction. No errors. No warnings.&lt;/p&gt;

&lt;p&gt;And that's exactly the problem.&lt;/p&gt;

&lt;p&gt;In Web3, if you send a transaction to the wrong address — it doesn't fail in a helpful way. It just… succeeds somewhere else.&lt;/p&gt;

&lt;p&gt;There's no backend validation, no "wrong destination" error. The transaction goes through, and from the system's perspective — everything is fine.&lt;/p&gt;

&lt;p&gt;But your funds are gone.&lt;/p&gt;

&lt;p&gt;In my case it was just a test transaction — nothing critical — but it was a very clear reminder: double-check addresses. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment
&lt;/h2&gt;

&lt;p&gt;And then it all came together.&lt;/p&gt;

&lt;p&gt;First goal created. First donation sent. DreamPower increased.&lt;/p&gt;

&lt;p&gt;Balance changed. Progress updated. State reflected in the UI.&lt;/p&gt;

&lt;p&gt;End-to-end:&lt;/p&gt;

&lt;p&gt;MetaMask → smart contract → Sepolia → frontend&lt;/p&gt;

&lt;p&gt;At that point it stopped feeling like a collection of separate parts and started feeling like a real system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Clicked
&lt;/h2&gt;

&lt;p&gt;The biggest shift for me was understanding that in this setup, the blockchain is not just another service.&lt;/p&gt;

&lt;p&gt;It is the backend.&lt;/p&gt;

&lt;p&gt;There's no API layer translating requests. The frontend talks directly to the contract, and that removes an entire layer of abstraction I'm used to in .NET systems.&lt;/p&gt;

&lt;p&gt;It also means that things like latency, consistency, and state management behave differently — and you have to design with that in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Question
&lt;/h2&gt;

&lt;p&gt;At some point I also started thinking about limits.&lt;/p&gt;

&lt;p&gt;How many users can this support? How many goals can a contract realistically store?&lt;/p&gt;

&lt;p&gt;Technically — a lot. Practically — every write costs gas, and large reads eventually hit limits. It's not something that matters right now, but it's definitely something that will matter later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — first full interaction. Not just reading. Not just UI. A system that reads, writes, and reacts.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/alena-dev-soft/wish-list-chain" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wish-list-chain&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the journey on Telegram:&lt;/strong&gt; &lt;a href="https://t.me/dotnetToWeb3" rel="noopener noreferrer"&gt;t.me/dotnetToWeb3&lt;/a&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>dotnet</category>
      <category>beginners</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 7 — First connect</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sat, 16 May 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-7-first-connect-2ajp</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-7-first-connect-2ajp</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;This day is a bit different from the previous ones. Initially, the idea was simple — one contract per day. But Web3 is not just about contracts and blockchain. There are other layers — UI, integration, the way everything connects together. At some point it became obvious: it makes more sense to start structuring the project early, instead of treating each part in isolation. As a developer who is getting close to an architect level — and someone who enjoys microservice architecture — I got curious: how are these projects actually structured?&lt;/p&gt;

&lt;p&gt;A contract is just one piece. A dApp is a system. I assume the same principles apply here — some form of clean architecture, separation of concerns, clear boundaries. And for something like WishList Chain, it felt important to start thinking about the structure from the beginning.&lt;/p&gt;

&lt;p&gt;At that point, I started thinking not just about the contract itself, but about the structure of the whole project. Because Web3 is clearly not only about writing smart contracts. There is always a frontend, some kind of interaction layer, maybe scripts or bots, and potentially even a backend later on. All of these parts are connected, whether you plan for it or not.&lt;/p&gt;

&lt;p&gt;So the question became less about "what to build next" and more about "how to structure it properly from the beginning."&lt;/p&gt;

&lt;p&gt;That's where the idea of using a monorepo came in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Monorepo?
&lt;/h2&gt;

&lt;p&gt;I decided to approach this a bit differently. Usually, I create separate repositories by responsibility (the "S" in SOLID), but not this time.&lt;/p&gt;

&lt;p&gt;Here the picture is clearer. The contract and the frontend are tightly connected. Any change in the contract — new fields, updated logic, even a different address — will require changes in multiple places. That means more manual work, more context switching, and more chances to break something.&lt;/p&gt;

&lt;p&gt;Better to keep things as simple as possible.&lt;/p&gt;

&lt;p&gt;So instead of splitting things too early, I decided to keep everything in one place. A single repository, but clearly separated layers inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wish-list-chain/
├── apps/
│   └── web/       ← Next.js 15
├── contracts/     ← Hardhat
├── package.json
└── .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a structural point of view, it already feels closer to a real system rather than a set of experiments.&lt;/p&gt;

&lt;p&gt;I know this goes a bit against how I usually structure things. Normally, I prefer to separate repositories by responsibility — the "S" in SOLID. It keeps boundaries clear and makes scaling easier later. But in this case, it felt like the separation would introduce more friction than value. The responsibilities are still separated — just not at the repository level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Stack
&lt;/h2&gt;

&lt;p&gt;Before the code — a quick map of what we're working with and why.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;.NET analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js 15 (App Router)&lt;/td&gt;
&lt;td&gt;Frontend framework&lt;/td&gt;
&lt;td&gt;ASP.NET Core MVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wagmi 2&lt;/td&gt;
&lt;td&gt;React hooks for blockchain&lt;/td&gt;
&lt;td&gt;SDK wrapper for Web3 calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;viem 2&lt;/td&gt;
&lt;td&gt;Low-level Ethereum client&lt;/td&gt;
&lt;td&gt;HttpClient for the blockchain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RainbowKit&lt;/td&gt;
&lt;td&gt;Wallet connection UI&lt;/td&gt;
&lt;td&gt;OAuth login button&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@tanstack/react-query&lt;/td&gt;
&lt;td&gt;Async state management&lt;/td&gt;
&lt;td&gt;No direct equivalent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WalletConnect / Reown&lt;/td&gt;
&lt;td&gt;Multi-wallet protocol&lt;/td&gt;
&lt;td&gt;OAuth provider&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Theory — A Little Bit
&lt;/h2&gt;

&lt;p&gt;wagmi sits on top of viem. viem handles the low-level blockchain calls. wagmi provides React hooks like &lt;code&gt;useAccount&lt;/code&gt;, &lt;code&gt;useReadContract&lt;/code&gt;, &lt;code&gt;useWriteContract&lt;/code&gt;. You can drop down to viem when you need more control, but most of the time wagmi is enough.&lt;/p&gt;

&lt;p&gt;RainbowKit is just a UI layer on top of wagmi — the modal, the button, the wallet list. It doesn't do any blockchain work itself.&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%2Fodo3o96walie4quz7vlg.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%2Fodo3o96walie4quz7vlg.png" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Clicked
&lt;/h2&gt;

&lt;p&gt;Next.js 15 App Router makes everything a Server Component by default. wagmi hooks rely on React state and browser APIs — they can't run on the server. Any component that calls &lt;code&gt;useAccount&lt;/code&gt;, &lt;code&gt;useReadContract&lt;/code&gt;, or renders &lt;code&gt;&amp;lt;ConnectButton /&amp;gt;&lt;/code&gt; needs &lt;code&gt;'use client'&lt;/code&gt; at the top. Otherwise, the code looks correct — but simply doesn't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ConnectKit doesn't support React 19 yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I actually started with ConnectKit — clean UI, good docs, everything looked straightforward. It failed immediately. Peer dependency error: it requires React 17 or 18, while Next.js 15 already ships with React 19. So that path just… stopped there.&lt;/p&gt;

&lt;p&gt;This is the current state of the Web3 frontend ecosystem: some libraries are ahead, some are behind. Check React version compatibility before installing anything.&lt;/p&gt;

&lt;p&gt;Downgrading React didn't feel right. So I kept React 19 and aligned the rest of the stack around it — wagmi 2.x ended up being the stable choice.&lt;/p&gt;

&lt;p&gt;RainbowKit requires wagmi 2. wagmi 3 is out but RainbowKit hasn't released a compatible version yet. Pinning wagmi to version 2 for now. This will resolve eventually — until then, mixing versions breaks the dependency tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;ConnectButton /&amp;gt;&lt;/code&gt; renders. Click it — RainbowKit modal opens with MetaMask, WalletConnect, and others. Connect MetaMask — wallet address and balance appear.&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%2Fi8ed6ok7xfc7z8lttt3c.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%2Fi8ed6ok7xfc7z8lttt3c.png" alt=" " width="461" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a Next.js 15 app reading live data from the Sepolia blockchain through a connected wallet. No backend. No API. The blockchain is the data source.&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%2Fivasohu01bvq9gwmyxtb.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%2Fivasohu01bvq9gwmyxtb.png" alt=" " width="551" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next: read &lt;code&gt;totalDreamPower&lt;/code&gt; directly from the WishlistV3 contract using &lt;code&gt;useReadContract&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/alena-dev-soft/wish-list-chain" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wish-list-chain&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — frontend connected. Blockchain is the backend.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ethereum</category>
      <category>beginners</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>A .NET Dinosaur in Web3. Day 6 - Wishlist or… “Dream Coins True”?</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Fri, 15 May 2026 15:07:18 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-6-wishlist-or-dream-coins-true-1kb9</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-6-wishlist-or-dream-coins-true-1kb9</guid>
      <description>&lt;p&gt;There's something that happened between Day 5 and Day 6 during brainstorming about project ideas. Day 6 wasn't just a deploy session — it was the first day of building something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Brainstorming started as a strategy question — I needed a practice project. Something to learn the full modern Web3 stack and something I could build in public.&lt;/p&gt;

&lt;p&gt;I already have another idea that I've started building, but until the MVP is ready — I decided to keep it under wraps.&lt;/p&gt;

&lt;p&gt;The idea was simple: pick something "common", but complex enough to cover real technical ground.&lt;/p&gt;

&lt;p&gt;Well… I'm a dreamer. So the wishlist idea just felt right.&lt;/p&gt;

&lt;p&gt;I started thinking — is it possible to build something interesting around it? Maybe the idea isn't new. It doesn't really matter… I just like it.&lt;/p&gt;

&lt;p&gt;Take a wishlist, let people make each other's dreams come true and… maybe build something on-chain around it.&lt;/p&gt;

&lt;p&gt;The base Wishlist contract was already written. It was already there — a working smart contract with a React frontend deployed on Sepolia.&lt;/p&gt;

&lt;p&gt;So why not just build something on top of it?&lt;/p&gt;

&lt;p&gt;Multi-user goals, ETH donations for specific goals, a Telegram bot for notifications. A proper monorepo with contract, Supabase, Drizzle, Next.js, and more.&lt;/p&gt;

&lt;p&gt;And then the idea shifted.&lt;/p&gt;

&lt;p&gt;What if every donation increases the "strength" of something?&lt;/p&gt;

&lt;p&gt;A global counter. An on-chain kind of energy. A number that grows every time someone believes in someone else's dream enough to send ETH.&lt;/p&gt;

&lt;p&gt;The working name became WishList Chain (WSHL) — not a token, at least not yet. An on-chain power score. &lt;code&gt;totalDreamPower&lt;/code&gt; in the contract. Every donation adds to it. Every goal has its own dream power.&lt;/p&gt;

&lt;p&gt;(DreamCoin, the name I originally wanted, is apparently already taken — so I had to rethink pretty quickly.)&lt;/p&gt;

&lt;p&gt;Is it a real product? Possibly. Is it a learning project? Definitely. Is the idea somewhat ridiculous? Yes, and that's exactly why it might work in crypto.&lt;/p&gt;

&lt;p&gt;The project is at &lt;a href="https://github.com/alena-dev-soft/wish-list-chain" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wish-list-chain&lt;/a&gt; — open source.&lt;/p&gt;

&lt;p&gt;Day 6 was about laying the foundation for all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;Write the core contract for WishList Chain — version 3 of the original Wishlist.sol.&lt;/p&gt;

&lt;p&gt;Multi-user goals, ETH donations, &lt;code&gt;dreamPower&lt;/code&gt; accumulation per goal, &lt;code&gt;totalDreamPower&lt;/code&gt; globally, and a &lt;code&gt;DreamPowerIncreased&lt;/code&gt; event for future Alchemy webhooks. Deploy with Hardhat. Verify on Etherscan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contract
&lt;/h2&gt;

&lt;p&gt;WishlistV3 is a different architecture from V2. V2 was one owner, one wishlist. V3 is a shared contract where every wallet has its own goals.&lt;/p&gt;

&lt;p&gt;The .NET analogy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// V2&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WishItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;wishes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// V3&lt;/span&gt;
&lt;span class="nc"&gt;Dictionary&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Goal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;goals&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Solidity: &lt;code&gt;mapping(address =&amp;gt; Goal[]) public goals&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;struct Goal&lt;/code&gt; gained financial fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Goal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;targetAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;currentAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;dreamPower&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFulfilled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;createdAt&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;code&gt;donate()&lt;/code&gt; is the core function. Two things worth noting:&lt;/p&gt;

&lt;p&gt;It must be &lt;code&gt;payable&lt;/code&gt; — without that modifier, Solidity won't accept ETH. The keyword is required, not optional.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Goal storage goal = goals[_owner][_goalIndex]&lt;/code&gt; — &lt;code&gt;storage&lt;/code&gt; means you're working with the actual on-chain data, not a copy. Change it, and the state changes. Use &lt;code&gt;memory&lt;/code&gt; instead, and you're editing a local copy that disappears after the function returns. This distinction doesn't exist in .NET — it's specific to the EVM execution model.&lt;/p&gt;

&lt;p&gt;The event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;DreamPowerIncreased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt; &lt;span class="n"&gt;goalIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;addedPower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="n"&gt;newTotalPower&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;indexed&lt;/code&gt; parameters are searchable in logs. They become filter keys — Alchemy webhooks can listen for specific owners or goal indices without scanning every event. The non-indexed parameters are just data payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardhat
&lt;/h2&gt;

&lt;p&gt;Moving from Remix to Hardhat is the next step in my Web3 journey. Remix is a great tool for exploration. Hardhat provides everything needed out of the box, including a proper build pipeline.&lt;/p&gt;

&lt;p&gt;The setup that actually works with Hardhat 3 + viem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; hardhat@latest @nomicfoundation/hardhat-toolbox-viem@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I really like this part — deploy and verify in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hardhat ignition deploy ignition/modules/WishlistV3.ts &lt;span class="nt"&gt;--network&lt;/span&gt; sepolia &lt;span class="nt"&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fair warning: getting there involved the usual package dependency hell — wrong versions, renamed config keys, cached artifacts. Nothing mysterious, just the standard tax you pay any time you touch a stack that lives and dies by npm. Read the error codes, fix one thing at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;WishlistV3 is not just a learning exercise. It's the core contract for something that's being built. The &lt;code&gt;donate()&lt;/code&gt; function, the &lt;code&gt;DreamPowerIncreased&lt;/code&gt; event, the multi-user architecture — all of it is designed for a real use case that will be revealed when the MVP is ready.&lt;/p&gt;

&lt;p&gt;Two-week sprint. This was the foundation.&lt;/p&gt;

&lt;p&gt;Contract address: &lt;code&gt;0x90de4a1934d0B062423adAEeDEe37Bb6fD12D0Ca&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Verified: &lt;a href="https://sepolia.etherscan.io/address/0x90de4a1934d0B062423adAEeDEe37Bb6fD12D0Ca" rel="noopener noreferrer"&gt;sepolia.etherscan.io/address/0x90de4a1934d0B062423adAEeDEe37Bb6fD12D0Ca&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — dependency hell survived. Foundation is live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>dotnet</category>
      <category>beginners</category>
      <category>ethereum</category>
    </item>
  </channel>
</rss>
