<?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.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 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>
    <item>
      <title>A .NET Dinosaur in Web3. Day 5 - First dApp.</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Tue, 12 May 2026 07:24:31 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-5-first-dapp-1hf3</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-day-5-first-dapp-1hf3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚠️ Honest disclaimer: Yes, this one is late. The dinosaur went travelling, then came back and started building something. Both count.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 5 was about wiring the WishList contract to a real frontend — React, TypeScript, ethers.js, MetaMask. But somewhere between the travel and the code, a project idea formed. Two-week sprint, MVP target. It stays under wraps for now — but the daily insights don't stop.&lt;/p&gt;

&lt;p&gt;Today: the dApp. Soon: something real.&lt;/p&gt;

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

&lt;p&gt;Take the WishList contract from Day 4 and make it usable in the real world. (If someone feels like fulfilling one of my wishes — I'm not stopping you.)&lt;/p&gt;

&lt;p&gt;Solidity: &lt;a href="http://github.com/alena-dev-soft/solidity-learn/contracts/05day/" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/solidity-learn/contracts/05day/&lt;/a&gt;&lt;br&gt;
UI: &lt;a href="https://github.com/alena-dev-soft/wishlist-dapp.git" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/wishlist-dapp&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;WishlistV2 — an upgraded contract plus a React dApp on top of it.&lt;/p&gt;

&lt;p&gt;Contract additions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;createdAt: block.timestamp&lt;/code&gt; — creation timestamp. Like &lt;code&gt;DateTime.UtcNow&lt;/code&gt; in C# but in Unix seconds, written permanently on-chain.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deleteWish(uint _index)&lt;/code&gt; — removes a wish. The trick: swap the target with the last element, then &lt;code&gt;pop()&lt;/code&gt;. Cheaper than shifting the entire array. This breaks the original order — acceptable here, but something to be aware of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend stack: Vite + React + TypeScript + ethers.js.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Actually Clicked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ABI is just an interface.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To call a smart contract from JavaScript, you need its ABI — the list of function signatures. The mental model for .NET developers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ABI&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IWishlist&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It describes what exists. Not how it works. The frontend doesn't need the implementation — just the signatures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;provider vs signer — the key distinction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;provider&lt;/code&gt; reads from the blockchain — no permissions needed, no gas. &lt;code&gt;signer&lt;/code&gt; represents an account that can authorize transactions. If something changes state, it must be signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockchain is not a REST API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a normal React app — button click, data saves in 50ms, UI updates instantly. In Web3 — the transaction goes to network nodes, gets included in a block, block gets confirmed. On Sepolia that takes 10–15 seconds. &lt;code&gt;await tx.wait()&lt;/code&gt; literally waits for the block. This is not a bug. This is the execution model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;isOwner flag — access control in the UI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The contract enforces owner-only rules on-chain. But the UI should also reflect them — no point showing "Delete" to someone who can't delete. Solution: load &lt;code&gt;owner()&lt;/code&gt; from the contract, compare with the connected wallet, set an &lt;code&gt;isOwner&lt;/code&gt; flag. Owner sees all controls. Everyone else sees a read-only list.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Things That Can Waste Your Time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TypeScript doesn't know about MetaMask out of the box.&lt;/strong&gt; &lt;code&gt;window.ethereum&lt;/code&gt; isn't part of the default typings. Quick fix: declare it as &lt;code&gt;any&lt;/code&gt;. Good enough for now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vite creates a nested folder structure by default.&lt;/strong&gt; If you run it inside an existing project directory, you end up one level too deep. Easy to miss, costs a few minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testnet latency is real.&lt;/strong&gt; Sepolia blocks roughly every ~12 seconds. Even after &lt;code&gt;tx.wait()&lt;/code&gt; resolves, the updated state might not be immediately visible. A small delay or refetch avoids reading stale data.&lt;/p&gt;

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

&lt;p&gt;The days off weren't wasted. An idea formed — something that combines what I'm learning with a real use case. Two-week sprint, MVP target. The project stays under wraps for now, but the daily learning logs continue. The insights will keep coming. Just with a different backdrop.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — first dApp live. Backend meets frontend. Something is forming.&lt;/em&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 4 - Writing My First Contract From Scratch</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sun, 03 May 2026 15:58:16 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-4-2llg</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-4-2llg</guid>
      <description>&lt;p&gt;Three days of guided exercises. Today — no template, no "here's the complete code, just copy and paste." Just a brief: based on what you know — build your WishList contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Task
&lt;/h2&gt;

&lt;p&gt;A personal WishList where only the owner can fulfill a wish. Small enough to finish in one session. Not so small that the decisions made themselves.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="http://github.com/alena-dev-soft/solidity-learn/contracts/04day/" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/solidity-learn/contracts/04day/&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The first thing I got tripped up on wasn't syntax — it was initialization. In .NET, almost everything needs to be explicitly initialized, especially array-related types. In Solidity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WishItem[] public wishes;

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

&lt;/div&gt;



&lt;p&gt;That's it. The array exists, it's empty, &lt;code&gt;push()&lt;/code&gt; works immediately. The EVM assigns default values at deployment — &lt;code&gt;uint256&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt; is the zero address.&lt;/p&gt;

&lt;p&gt;Worth noting though: arrays in Solidity aren't free the way they are in .NET. Every &lt;code&gt;push()&lt;/code&gt; is a state change with a gas cost. And "EVM handles defaults" is not the same as "no initialization needed" — that distinction matters once you're working with memory arrays, nested structs, or upgradeable contracts.&lt;/p&gt;

&lt;p&gt;The next decision was identification. Every wish needs some kind of handle — otherwise how do you reference the one you want to fulfill? My instinct was to reach for something explicit, maybe even overcomplicate it. The options in Solidity are straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Array index — simplest, no overhead&lt;/li&gt;
&lt;li&gt;Manual counter — more control, slightly more code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mapping(uint256 =&amp;gt; WishItem)&lt;/code&gt; — keyed table, most flexible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Went with array index. &lt;code&gt;fulfillWish(uint _index)&lt;/code&gt; takes the position. For a contract this size, there's no reason to reach for more than you need.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Fair warning: array index works as an identifier only as long as the array is append-only and items are never removed or reordered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  On Validation — Index Safety
&lt;/h2&gt;

&lt;p&gt;Nothing in Solidity protects you unless you do it explicitly. Using an array index as an identifier is clean and simple — but only if you validate it first.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;fulfillWish(uint _index)&lt;/code&gt; without a boundary check is asking for trouble. At minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(_index &amp;lt; wishes.length, "Invalid index");

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

&lt;/div&gt;



&lt;p&gt;Without this, you're relying on implicit behavior and risking unexpected reverts. In .NET you often get guardrails for free. In Solidity — you build them yourself, or they don't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Access Control — There Is No Default Security
&lt;/h2&gt;

&lt;p&gt;The requirement was simple: only the owner can fulfill a wish. What clicked is that in Solidity this isn't a built-in rule — it's just code you have to write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(msg.sender == owner, "Not the owner");

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

&lt;/div&gt;



&lt;p&gt;Without this line, anyone can call your function. Solidity doesn't assume intent. It executes what's written — nothing more, nothing less.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Tooling Trap Worth Knowing
&lt;/h2&gt;

&lt;p&gt;I use Edge, and I tend to keep tabs open for days. After a two-day gap — life happens — I came back to Remix with MetaMask still connected from before.&lt;/p&gt;

&lt;p&gt;Here's the gotcha: switching to Sepolia in the top network dropdown isn't enough. The wallet panel at the bottom has its own network selector, and it needs to match. If they silently disagree, you get a gas estimation error with null revert data. Looks exactly like a contract bug. Isn't one.&lt;/p&gt;

&lt;p&gt;Web3 tooling fails loudly and explains itself poorly. File that under "features, probably."&lt;/p&gt;

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

&lt;p&gt;Solidity alone is a backend with no front door. The next step is wiring this contract to something a human can actually use — ethers.js, React, TypeScript. The blockchain as a data layer, with a real interface on top.&lt;/p&gt;

&lt;p&gt;That's Day 5.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 — first contract, no scaffolding.&lt;/em&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 3 - Voting, Sybil Attacks and Identity</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sun, 03 May 2026 09:51:21 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-3-7j9</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-3-7j9</guid>
      <description>&lt;p&gt;Day 3 was the first day that felt like actual software engineering rather than syntax tourism. The task: write a voting contract. Simple enough on the surface - until you start poking at the security model and realize the whole thing has serious gaps in its logic.&lt;/p&gt;

&lt;p&gt;What looked like a toy example turned out to be a good proxy for real system design problems.&lt;/p&gt;

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

&lt;p&gt;Instead of dumping a wall of code here, I moved the full contract and instructions to GitHub. This post is about what actually matters - how it works.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/alena-dev-soft/solidity-learn/tree/main/contracts/03day" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/solidity-learn/contracts/03day/&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;code&gt;struct&lt;/code&gt; → &lt;code&gt;record&lt;/code&gt; in C#&lt;/strong&gt;&lt;br&gt;
Not a class. No methods, no behavior - pure data container. Closer to &lt;code&gt;record&lt;/code&gt;&lt;br&gt;
in C# than anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;mapping(address =&amp;gt; bool)&lt;/code&gt; → &lt;code&gt;Dictionary&amp;lt;address, bool&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Exact mental model. The key is a wallet address, the value is whether they've&lt;br&gt;
voted. Lookup is O(1), there's no iteration - same tradeoffs as &lt;code&gt;Dictionary&lt;/code&gt;&lt;br&gt;
in .NET.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;view&lt;/code&gt; modifier → read-only, free to call&lt;/strong&gt;&lt;br&gt;
Methods marked &lt;code&gt;view&lt;/code&gt; don't write to state, so they don't cost gas. The EVM&lt;br&gt;
equivalent of a GET endpoint versus a POST. This clicked immediately because&lt;br&gt;
the cost model maps directly to why you'd separate reads from writes in&lt;br&gt;
any system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;require()&lt;/code&gt; → guard clauses + exception in one&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;require(condition, "message")&lt;/code&gt; is exactly &lt;code&gt;if (!condition) throw new Exception("message")&lt;/code&gt; - except when it reverts, the entire transaction is reverted. No state is changed, but gas is still spent. Closer to a database transaction abort than a simple exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Insight
&lt;/h2&gt;

&lt;p&gt;At some point I stopped and asked myself a simple question.&lt;/p&gt;

&lt;p&gt;How does the contract know that "Alice" is actually Alice?&lt;/p&gt;

&lt;p&gt;The answer was a little unsettling - because I've spent years designing systems where knowing who the user is was fundamental. Authentication, authorization, identity verification. That was always the baseline.&lt;/p&gt;

&lt;p&gt;In Web3 there is no baseline like that.&lt;/p&gt;

&lt;p&gt;The contract sees only an address. Just a string starting with &lt;code&gt;0x&lt;/code&gt;. No name, no history, no face. If the same person creates 10 wallets - congratulations, they now have 10 votes.&lt;/p&gt;

&lt;p&gt;This is just how the system works.&lt;/p&gt;

&lt;p&gt;And once that clicks, it quietly rewires how you think about everything else: access control, fairness, "one person = one vote." All the assumptions we carry from Web2 - where identity is tied to accounts, emails, phone numbers - simply don't apply here.&lt;/p&gt;

&lt;p&gt;Ownership of a wallet is not identity. It's just… ownership of a wallet.&lt;/p&gt;

&lt;p&gt;The fix exists, of course. Several of them, actually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitelist&lt;/strong&gt; - the owner manually approves addresses. Simple, but it requires trusting whoever manages the list. And it scales terribly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NFT / Token gating&lt;/strong&gt; - only wallets holding a specific token can participate. Think of it as a membership card. Still doesn't prove who the person is - just that they own the token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proof of Humanity&lt;/strong&gt; - on-chain verification that a real human stands behind the address. Technically elegant. Still a largely unsolved problem at scale.&lt;/p&gt;

&lt;p&gt;And then there's the quiet irony: most production solutions still route back to Web2 identity providers - Google, Binance, Microsoft and others. Web3 solved decentralized execution beautifully - identity remains outsourced to the old world.&lt;/p&gt;

&lt;p&gt;So no, dinosaurs aren't extinct yet. Apparently we're still needed. 🦕 (like me 🙃)&lt;/p&gt;

&lt;h2&gt;
  
  
  Side Observations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Etherscan shows bytecode by default.&lt;/strong&gt; The contract is there, but unreadable - same as looking at compiled IL instead of C# source. To expose the actual Solidity code, you need to verify the contract: upload the source, match the compiler version exactly. One wrong version number and it fails silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remix doesn't persist deployed contracts across page reloads.&lt;/strong&gt; After a refresh, the contract still exists on-chain - but Remix has no memory of it. Recovery is straightforward: find the contract address on Etherscan, use "At Address" in Remix to reattach. Good to know before it happens in a less forgiving context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing multi-wallet scenarios requires either Remix VM or separate wallets with testnet ETH.&lt;/strong&gt; Browser Extension mode only sees what MetaMask sees. Not a problem - just a constraint to know upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Day 3 changed something in how I think about system design in Web3.&lt;/p&gt;

&lt;p&gt;In Web2, identity is assumed. You build on top of it. In Web3, identity is your problem to solve - and every solution is either a tradeoff or a dependency on something outside the chain.&lt;/p&gt;

&lt;p&gt;The contract works. The logic is sound. The gaps are in the model, not the code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 - mapping the terrain. Starting to see where the edges are.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Day 4 incoming. 🚀&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 2 - Access Control</title>
      <dc:creator>Olena  </dc:creator>
      <pubDate>Sun, 03 May 2026 09:44:52 +0000</pubDate>
      <link>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-2-4g41</link>
      <guid>https://dev.to/olenadevsoft/a-net-dinosaur-in-web3-2-4g41</guid>
      <description>&lt;p&gt;New day, new challenge...&lt;br&gt;
Counter.sol - a little better than "Hello World", right?&lt;/p&gt;

&lt;p&gt;The goal: write a simple Counter contract - increment, decrement, reset -&lt;br&gt;
with real access rules.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: MIT&lt;/span&gt;
&lt;span class="n"&gt;pragma&lt;/span&gt; &lt;span class="n"&gt;solidity&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mf"&gt;0.8.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;uint256&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="k"&gt;public&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;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;count&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;count&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&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="s"&gt;"Already zero"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;count&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Only owner can reset!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;count&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="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;GitHub: &lt;a href="http://github.com/alena-dev-soft/solidity-learn/contracts/02day/" rel="noopener noreferrer"&gt;github.com/alena-dev-soft/solidity-learn/contracts/02day/&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In the .NET world we use &lt;code&gt;HttpContext.User&lt;/code&gt; to know who's making a request. In Solidity it's &lt;code&gt;msg.sender&lt;/code&gt; - the wallet address of whoever called the method. No login system, no JWT, no sessions. Just a cryptographically verified address. The contract knows exactly who you are. Always.&lt;/p&gt;

&lt;p&gt;The next thing - calling &lt;code&gt;count&lt;/code&gt; to check the current value is free.Simple rule: no transaction, no gas.&lt;/p&gt;

&lt;p&gt;But there's a warning that almost everyone forgets - which makes it kind of dangerous for your budget. If you forget to switch from the real network to the test network, those transactions will cost you actual money. Every single one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The .NET → Solidity Map (So Far)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solidity&lt;/th&gt;
&lt;th&gt;.NET equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;msg.sender&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HttpContext.User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;require()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Guard clause + exception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uint256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uint&lt;/code&gt; (unsigned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;address&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No direct equivalent — wallet ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Day 2 Score: 10/10
&lt;/h2&gt;

&lt;p&gt;Everything is mapping cleanly to .NET concepts. The mental model is familiar, the syntax is new.&lt;/p&gt;

&lt;p&gt;Tomorrow I want something with real-world application - not a toy example, but a contract that actually does something useful.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stage: Dinosaur 🦕 - mapping the terrain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Day 3 incoming. 🚀&lt;/em&gt;&lt;/p&gt;

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