<?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: Elizabeth Afolabi</title>
    <description>The latest articles on DEV Community by Elizabeth Afolabi (@devduchess).</description>
    <link>https://dev.to/devduchess</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1464865%2Fe9f2f036-f4c0-4e09-8d7c-a3b74ae5dc30.jpeg</url>
      <title>DEV Community: Elizabeth Afolabi</title>
      <link>https://dev.to/devduchess</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devduchess"/>
    <language>en</language>
    <item>
      <title>The Solana Security Checklist I Built After Breaking Solana Programs</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Sat, 25 Jul 2026 18:17:41 +0000</pubDate>
      <link>https://dev.to/devduchess/the-solana-security-checklist-i-built-after-breaking-solana-programs-5e90</link>
      <guid>https://dev.to/devduchess/the-solana-security-checklist-i-built-after-breaking-solana-programs-5e90</guid>
      <description>&lt;p&gt;During Arc 12 of the #100DaysOfSolana. I stopped trying to make my programs work. I started trying to break them. I wrote adversarial tests, generated random inputs with fuzz testing, reproduced real-world exploits like Wormhole and Cashio, and intentionally passed the wrong accounts into my instructions.&lt;/p&gt;

&lt;p&gt;The biggest lesson wasn't how exploits happen. It was how surprisingly small the missing validation can be. By the end of the arc, I realized I wasn't just learning security techniques; I was building a checklist I'll probably use every time I write an Anchor program.&lt;/p&gt;

&lt;p&gt;This checklist is for anyone building Solana programs with Anchor. It's not meant to replace an audit, but it is a practical list of questions I'll run through before every mainnet deployment to catch the kinds of mistakes that are easy to overlook.&lt;/p&gt;

&lt;p&gt;Earlier in the challenge, I assumed that if my instruction received an account and deserialized it successfully, everything was probably fine. This arc completely changed that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Solana, any account can be passed into an instruction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deserializing an account only tells you how to read its bytes. It doesn't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who owns it&lt;/li&gt;
&lt;li&gt;whether it's the account you expected&lt;/li&gt;
&lt;li&gt;whether it should be allowed here&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why so much of Solana security is simply validating assumptions before your business logic ever runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Solana Program Security Checklist
&lt;/h2&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Account Validation
&lt;/h3&gt;

&lt;p&gt;The biggest lesson from reproducing the &lt;a href="https://ackee.xyz/blog/2022-solana-hacks-explained-wormhole/" rel="noopener noreferrer"&gt;Wormhole&lt;/a&gt; and &lt;a href="https://www.halborn.com/blog/post/explained-the-cashio-hack-march-2022" rel="noopener noreferrer"&gt;Cashio&lt;/a&gt; exploits was how small the root cause actually was. In both cases, the vulnerable program trusted an account before verifying that it was owned by the expected program. Because of the lack of validation, an attacker was able to provide a counterfeit account that appeared genuine enough to deserialize but was not the account intended by the program itself. In &lt;a href="https://ackee.xyz/blog/2022-solana-hacks-explained-wormhole/" rel="noopener noreferrer"&gt;Wormhole&lt;/a&gt;, that error resulted in a loss of around $326 million. &lt;a href="https://www.halborn.com/blog/post/explained-the-cashio-hack-march-2022" rel="noopener noreferrer"&gt;Cashio&lt;/a&gt; experienced a similar problem, leading to approximately $52 million being emptied.&lt;/p&gt;

&lt;p&gt;Rebuilding the vulnerable pattern locally made this much more tangible than simply reading the post-mortems. It reinforced a simple rule I'll carry into every program I write: &lt;strong&gt;never trust an account just because it was passed into your instruction. Validate it first.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Checklist
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Use typed Account&amp;lt;'info, T&amp;gt; whenever possible.&lt;/li&gt;
&lt;li&gt;Only use &lt;code&gt;UncheckedAccount&lt;/code&gt; when absolutely necessary.&lt;/li&gt;
&lt;li&gt;If using &lt;code&gt;UncheckedAccount&lt;/code&gt;, verify its owner manually.&lt;/li&gt;
&lt;li&gt;Validate any remaining_accounts before using them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Authority and signer checks
&lt;/h3&gt;

&lt;p&gt;One of Anchor's biggest strengths is that it turns many security checks into declarative constraints.&lt;/p&gt;

&lt;p&gt;Typed accounts, &lt;code&gt;Signer&lt;/code&gt;, &lt;code&gt;has_one&lt;/code&gt;, &lt;code&gt;seeds&lt;/code&gt;, and &lt;code&gt;bump&lt;/code&gt; allow you to express who can access an instruction and which accounts are valid directly in the account context. Anchor then verifies those constraints before your instruction handler runs, reducing the amount of manual validation you need to write yourself.&lt;/p&gt;

&lt;p&gt;That doesn't mean authorization becomes automatic. Anchor can enforce the constraints you declare, but it's still your responsibility to declare the right ones.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checklist
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Every privileged instruction requires the expected signer.&lt;/li&gt;
&lt;li&gt;Authority relationships are enforced with &lt;code&gt;has_one&lt;/code&gt; or explicit constraints.&lt;/li&gt;
&lt;li&gt;PDA accounts use the correct seeds and bump constraints.&lt;/li&gt;
&lt;li&gt;Never trust a public key alone, verify that the required signer is actually present.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Think like an attacker
&lt;/h3&gt;

&lt;p&gt;Instead of asking "&lt;em&gt;Does this instruction work?&lt;/em&gt;", I started asking: &lt;em&gt;How would I try to break it?&lt;/em&gt;. That single change produced much better tests.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checklist
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Test wrong signers.&lt;/li&gt;
&lt;li&gt;Test substituted accounts.&lt;/li&gt;
&lt;li&gt;Test over-withdrawals.&lt;/li&gt;
&lt;li&gt;Assert the exact Anchor/runtime error, not just that the transaction failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Fuzz what humans wouldn't think of
&lt;/h3&gt;

&lt;p&gt;At first, I thought testing meant coming up with the right test cases myself. But there's a limit to how many cases one person can realistically think of. There are simply too many possible inputs to write tests for manually. Instead of choosing every input myself, I let the computer generate thousands of different combinations and try them against the program. Fuzzing explores that space much faster and can uncover edge cases I'd probably never think to test on my own.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checklist
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Fuzz balance changes.&lt;/li&gt;
&lt;li&gt;Fuzz arithmetic.&lt;/li&gt;
&lt;li&gt;Fuzz edge-case values.&lt;/li&gt;
&lt;li&gt;Keep the fuzz tests after the bug is fixed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Let Anchor help - but know its limits
&lt;/h3&gt;

&lt;p&gt;Anchor protects me from an entire class of mistakes by enforcing owner validation, account discriminators, signer constraints, PDA seeds and bumps, and reinitialization checks. But it can't decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether my arithmetic is correct&lt;/li&gt;
&lt;li&gt;whether my business logic is safe&lt;/li&gt;
&lt;li&gt;whether a CPI should happen or &lt;/li&gt;
&lt;li&gt;whether an unchecked account should be trusted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anchor is a powerful safety net, but it only enforces the constraints you define. Writing secure programs still means understanding what Anchor validates for you and what it doesn't.&lt;br&gt;
&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Remarks
&lt;/h2&gt;

&lt;p&gt;I think about security a little differently now. It's less about writing clever defenses and more about consistently validating assumptions before your program trusts anything.&lt;/p&gt;

&lt;p&gt;This checklist is one I'll keep coming back to before every deployment. I'm sure it isn't complete, and I expect it to grow as I learn more and build more complex programs.&lt;/p&gt;

&lt;p&gt;If you spot something I've missed or have a security check that's become part of your own workflow, I'd love to hear it. Security checklists are living documents, and they're better when they're improved by the community.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>anchor</category>
      <category>solana</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Solana Programs Can Sign Without Private Keys</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:40:16 +0000</pubDate>
      <link>https://dev.to/devduchess/why-solana-programs-can-sign-without-private-keys-5a7i</link>
      <guid>https://dev.to/devduchess/why-solana-programs-can-sign-without-private-keys-5a7i</guid>
      <description>&lt;p&gt;For a long time, I assumed every signature on Solana came from someone's wallet. That seemed obvious. If a transaction needed authorization, the owner of the account would sign it with their private key.&lt;br&gt;
Then I built a program that withdrew SOL from a PDA-owned vault. The vault had no private key, yet the transfer succeeded. That forced me to rethink what "signing" actually means on Solana.&lt;/p&gt;
&lt;h2&gt;
  
  
  My mental model of CPIs
&lt;/h2&gt;

&lt;p&gt;When I first learned about Cross-Program Invocations (CPIs), I imagined them as a way of doing familiar things programmatically. If I wanted to mint tokens, my program would somehow mint the tokens itself instead of me running CLI commands.That wasn't what was happening at all.&lt;/p&gt;

&lt;p&gt;Instead, my program was asking another program to perform the operation. The first CPI I built transferred SOL by calling the System Program, my program never moved the SOL directly. It simply constructed the instruction, passed in the required accounts, and delegated the work to the System Program.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;sol_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SolTransfer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_accounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transfer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.sender&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.recipient&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.system_program&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;cpi_accounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SolTransfer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&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;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SystemAccount&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;&amp;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;That taught me something I hadn't appreciated before:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Solana programs don't duplicate functionality. They build on top of one another.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That design keeps programs small and composable. Instead of every program implementing the same logic, they can reuse trusted programs that already exist.&lt;/p&gt;

&lt;p&gt;I used exactly the same idea to mint Token-2022 tokens. The destination program changed, but the pattern didn't. Instead of calling the System Program, my Anchor program called the Token-2022 program.&lt;br&gt;
I took it one step further by making one Anchor program call another using &lt;code&gt;declare_program!.&lt;/code&gt; That was when I realized something important, a CPI isn't a feature tied to one program, it's a general communication pattern. Once I understood how one program invokes another, the specific program being called became almost incidental. Whether it's the System Program, the Token-2022 program, or another Anchor program, the idea stays the same.&lt;/p&gt;
&lt;h2&gt;
  
  
  Authority without private keys
&lt;/h2&gt;

&lt;p&gt;The biggest shift happened when I built a PDA-owned vault. The vault held SOL. Eventually, the program needed to withdraw those funds. At first, that raised an obvious question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can a PDA authorize a transfer if it doesn't have a private key?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was the assumption I had been carrying until I came across &lt;code&gt;with_signer()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Withdraw&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.user&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.bumps.vault&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;signer_seeds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"vault"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_key&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;]]];&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.system_program&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;Transfer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.vault&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.user&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.with_signer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signer_seeds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&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;Instead of providing a private key, the program supplies the PDA's signer seeds. The Solana runtime uses those seeds and the program ID to derive the PDA again. If everything matches, the runtime temporarily recognizes that PDA as the signer for that specific Cross-Program Invocation and no private key is involved. The program isn't forging a signature, it's proving that it has the authority to act on behalf of the PDA it originally derived. That distinction completely changed how I thought about program authority.&lt;/p&gt;

&lt;p&gt;If someone had asked me who signs transactions on Solana, my answer would have been simple: "The owner of the account."&lt;/p&gt;

&lt;p&gt;Now I'd answer differently. Sometimes the signer is a keypair-controlled account. Sometimes the authority belongs to a Program Derived Address. The important difference is that a PDA isn't another wallet. It has no private key. Instead, the runtime verifies that the program knows the correct seeds used to derive that PDA before allowing it to authorize the CPI. The authorization comes from deterministic derivation, not from possession of a secret key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking things made the logs make sense
&lt;/h2&gt;

&lt;p&gt;One of the most useful exercises this week was deliberately breaking working CPIs to understand how the runtime enforces authority.&lt;/p&gt;

&lt;p&gt;One error I ran into was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;compose-lab
       the &lt;span class="nb"&gt;caller &lt;/span&gt;bumps the counter through a CPI:
     Error: AnchorError caused by account: tally. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.

Program log: Left:
FsYY6DU3FZf7j1HnpfVEAMPr1p4HJbXEdgktN4bYJjmq

Program log: Right:
AAnoAEWwp4YgADN1Svd963buvUuWyuDdrJPXaYWi55Ck

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

&lt;/div&gt;



&lt;p&gt;After reading it more carefully, I realized it meant that the account I passed into the instruction didn't match the PDA Anchor derived from the seeds declared in my &lt;code&gt;#[derive(Accounts)]&lt;/code&gt; constraint. Because the derived address and the supplied account were different, Anchor rejected the instruction before any of my program logic executed.&lt;/p&gt;

&lt;p&gt;The fix was to make sure the client and the program were deriving the PDA from the exact same seeds (and bump). Once they matched, the constraint passed and the instruction executed normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I know now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CPI isn't another type of instruction. It's one program asking another program to perform work.&lt;/li&gt;
&lt;li&gt;Programs don't bypass authorization just because they're programs.&lt;/li&gt;
&lt;li&gt;A PDA doesn't have a private key. It authorizes actions by proving it was derived from the correct seeds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;with_signer()&lt;/code&gt; doesn't create a signature. It tells the runtime how to verify the PDA's authority.&lt;/li&gt;
&lt;li&gt;Runtime errors become much easier to understand once you know what the runtime is actually validating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is part of my &lt;a href="https://dev.to/100daysofsolana"&gt;#100DaysOfSolana&lt;/a&gt; journey and covers Days 71–75.&lt;br&gt;
If you've been following along, it builds on my previous post about Program Derived Addresses (PDAs) and continues the transition from understanding how programs find state to how they safely act on it.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>web3</category>
      <category>rust</category>
      <category>anchor</category>
    </item>
    <item>
      <title>Why Solana Programs Don't Need to Store Your Account Address</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 08:30:45 +0000</pubDate>
      <link>https://dev.to/devduchess/why-solana-programs-dont-need-to-store-your-account-address-166k</link>
      <guid>https://dev.to/devduchess/why-solana-programs-dont-need-to-store-your-account-address-166k</guid>
      <description>&lt;p&gt;For most applications, if you generate a random identifier for a piece of data, you need to store that identifier somewhere so you can find the data again later. That's what I expected when I started learning Solana.&lt;br&gt;
Instead, I discovered that Solana programs can derive the same account address whenever they need it. They don't have to store the address at all. That was the part I had been missing about Program Derived Addresses (PDAs), and it completely changed how I think about storing application state on Solana.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;When I first heard the term Program Derived Address, I assumed it simply meant "an address created by a program." That wasn't entirely wrong, but it missed the point.&lt;br&gt;
A PDA isn't just an address a program creates. It's an address a program can &lt;strong&gt;deterministically derive&lt;/strong&gt; from the same inputs every time.&lt;/p&gt;

&lt;p&gt;Those inputs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one or more seeds&lt;/li&gt;
&lt;li&gt;the program's ID&lt;/li&gt;
&lt;li&gt;a bump value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As long as those inputs don't change, the derived address never changes either. The important consequence isn't how the address is generated, it's that the program never needs to remember where your data lives. If it knows the same seeds and its own program ID, it can derive the address again whenever it needs to read or update the account.&lt;/p&gt;

&lt;p&gt;That felt very different from what I'm used to, where generating a random identifier usually means storing it somewhere before you can find the data again.&lt;/p&gt;
&lt;h2&gt;
  
  
  Anatomy of a derivation
&lt;/h2&gt;

&lt;p&gt;Here's what creating the counter account looked like in my program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[account(&lt;/span&gt;
    &lt;span class="nd"&gt;init,&lt;/span&gt;
    &lt;span class="nd"&gt;payer&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;user,&lt;/span&gt;
    &lt;span class="nd"&gt;space&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nd"&gt;Counter::INIT_SPACE,&lt;/span&gt;
    &lt;span class="nd"&gt;seeds&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;key()&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nd"&gt;as_ref()]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bump&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&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;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Each part has a purpose.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init&lt;/code&gt; tells Anchor to create the account if it doesn't already exist.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;payer = user&lt;/code&gt; specifies which wallet funds the account creation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;space&lt;/code&gt; reserves enough bytes to store the account data.&lt;/p&gt;

&lt;p&gt;The interesting part is the seeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;seeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first seed is a static identifier that tells the program what kind of account it's deriving. The second seed is dynamic; it uses the user's public key. Anchor combines these seeds with the program ID to derive a deterministic address.&lt;/p&gt;

&lt;p&gt;That program ID is more important than I initially realized. Even if another program used the exact same seeds, it would derive a completely different PDA because its program ID is different. That means another program can't simply pretend it owns your PDA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about the bump?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bump was probably the least intuitive part for me. I don't fully understand the mathematics behind it yet, but I do understand its role. Not every derived address is valid as a PDA. The runtime searches for a valid address by trying different bump values until it finds one that falls off the Ed25519 curve.&lt;/p&gt;

&lt;p&gt;When you write: &lt;code&gt;bump&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Anchor automatically derives and stores the canonical bump for you, so you don't have to calculate it yourself. That's enough for me to use PDAs correctly today, even if I couldn't explain the underlying algorithm from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the seeds matter
&lt;/h2&gt;

&lt;p&gt;Changing the seeds changes the kind of application you're building. For my counter, I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the user's public key is part of the derivation, every wallet gets its own counter. If I removed the user key and only used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;b"counter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;every wallet would derive exactly the same PDA.&lt;br&gt;
Sometimes that's exactly what you want. For example, a global configuration account shared by the entire application. For a personal counter, though, it would be a disaster because every user would be trying to update the same account.&lt;/p&gt;

&lt;p&gt;That small change in the seeds completely changes the behavior of the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full lifecycle and payoff
&lt;/h2&gt;

&lt;p&gt;What made this week interesting wasn't just deriving a PDA, it was seeing the entire lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wfj04simy0rvkrij6c0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wfj04simy0rvkrij6c0.png" alt="Image showing the full lifecycle of a PDA" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, derive the address.&lt;/li&gt;
&lt;li&gt;Then initialize the account at that address.&lt;/li&gt;
&lt;li&gt;Update its state through later instructions.&lt;/li&gt;
&lt;li&gt;Finally, close the account when it's no longer needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Closing the account - Earlier in the challenge, I assumed the lamports used for rent were a permanent cost. Closing my first PDA taught me they aren't.&lt;/p&gt;

&lt;p&gt;When the account is closed, its data is removed, the reserved lamports are refunded to the designated recipient, and the account is cleaned up by the network. That made rent feel much more like a refundable storage deposit than a permanent fee.&lt;/p&gt;

&lt;p&gt;Before this week, I assumed the client would need to remember every account address it created. Now I know that isn't necessary. As long as the frontend knows the same seeds and the program ID, it can derive the same PDA whenever it needs to fetch the account. Nothing extra needs to be stored, which makes the architecture feel much cleaner. It also answered another question I had.&lt;/p&gt;

&lt;p&gt;Since the program ID is part of the derivation, another program can't generate the same PDA just by knowing the seeds. The same inputs in a different program produce a completely different address.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The program never stores the account address; it derives it whenever it's needed.&lt;/li&gt;
&lt;li&gt;A PDA has no private key. It's not a wallet.&lt;/li&gt;
&lt;li&gt;The same seeds in a different program produce a different PDA because the program ID is part of the derivation.&lt;/li&gt;
&lt;li&gt;You don't need to fully understand the bump algorithm on day one. Understanding its purpose is enough to start building.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;PDAs solved one of the biggest questions I had about state management on Solana. On the surface, they seem like a small feature. In practice, they fundamentally change how applications manage state. Instead of remembering where data lives, programs can deterministically derive its location whenever they need it. That's a very different way of thinking about application architecture, and it's one of the biggest shifts I've had since I started learning Solana.&lt;/p&gt;

&lt;p&gt;If you're learning PDAs yourself, I'd recommend reading the official &lt;a href="https://solana.com/docs/core/pda" rel="noopener noreferrer"&gt;Solana PDA documentation&lt;/a&gt; alongside the &lt;a href="https://www.anchor-lang.com/docs/basics/pda" rel="noopener noreferrer"&gt;Anchor PDA documentation&lt;/a&gt;. Both helped reinforce the concepts I explored this week, and the full source code for my counter program is available on &lt;a href="https://github.com/BettyAfolabi/100daysofsolana/tree/main/counter-65" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>solana</category>
      <category>pda</category>
    </item>
    <item>
      <title>How a Simple Counter Program Helped Me Understand Anchor's Account Model</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Mon, 06 Jul 2026 21:13:58 +0000</pubDate>
      <link>https://dev.to/devduchess/how-a-simple-counter-program-helped-me-understand-anchors-account-model-ahg</link>
      <guid>https://dev.to/devduchess/how-a-simple-counter-program-helped-me-understand-anchors-account-model-ahg</guid>
      <description>&lt;p&gt;I've spent the last few weeks using Solana programs. This week, I finally wrote one.&lt;/p&gt;

&lt;p&gt;The program itself is simple: a counter that stores a number and the wallet authorized to update it. The interesting part wasn't the counter. It was seeing how Anchor uses account definitions to describe state, permissions, and validation before the instruction handler executes.&lt;br&gt;
Here's what I learned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The accounts struct
&lt;/h2&gt;

&lt;p&gt;In a normal backend, your function gets a request and your database is just... available. You call it when you need it.&lt;/p&gt;

&lt;p&gt;Solana doesn't work like that. Every piece of state your instruction touches has to be declared upfront, before your code runs. This is the struct that creates a new counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Accounts)]&lt;/span&gt;
&lt;span class="nd"&gt;#[account(init,&lt;/span&gt; &lt;span class="nd"&gt;payer&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;authority,&lt;/span&gt; &lt;span class="nd"&gt;space&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nd"&gt;Counter::INIT_SPACE)]&lt;/span&gt;
&lt;span class="k"&gt;pub&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;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;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;counter&lt;/code&gt; is the account being created. &lt;code&gt;init&lt;/code&gt; allocates it on-chain, &lt;code&gt;payer = authority&lt;/code&gt; says who pays the rent, and space is how many bytes to reserve.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;authority&lt;/code&gt; is the wallet calling the instruction. It has to sign the transaction and it has to be mutable because SOL is leaving it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;system_program&lt;/code&gt; is Solana's built-in program that actually creates accounts. You list it so Anchor can call it on your behalf.&lt;/p&gt;

&lt;p&gt;When I first saw this I thought it was boilerplate. It's not. This is you telling the runtime: here is exactly what this instruction is allowed to touch, and here are the rules. The runtime validates all of it before your code runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instruction handlers
&lt;/h2&gt;

&lt;p&gt;Once the accounts struct validates, the handler itself is almost boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.authority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.authority&lt;/span&gt;&lt;span class="nf"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&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="nf"&gt;Ok&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;ctx.accounts&lt;/code&gt; gives you typed access to every account you declared in the struct. You write to the fields directly. Anchor writes those changes back to the account automatically when the instruction finishes. The handler only describes what should change.&lt;/p&gt;

&lt;p&gt;The increment handler is where things get interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Increment&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt;
        &lt;span class="nf"&gt;.checked_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ProgramError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArithmeticOverflow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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;Two things worth noting here. First, &lt;code&gt;checked_add&lt;/code&gt; instead of&lt;code&gt;+= 1&lt;/code&gt;: if &lt;code&gt;count&lt;/code&gt; ever hit the max value of a &lt;code&gt;u64&lt;/code&gt; and you used plain addition, it would silently wrap back to zero and corrupt your data. &lt;code&gt;checked_add&lt;/code&gt; returns an error instead.&lt;/p&gt;

&lt;p&gt;Second, and more important: notice there is no authorization check in this function body. That check lives in the accounts struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Accounts)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Increment&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(mut,&lt;/span&gt; &lt;span class="nd"&gt;has_one&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;authority)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&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;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;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;&lt;code&gt;has_one = authority&lt;/code&gt; tells Anchor: before this handler runs, confirm that &lt;code&gt;counter.authority&lt;/code&gt; (the field stored inside the on-chain account) matches the authority wallet being passed into this transaction. If they don't match, the transaction fails with a constraint violation before your code ever executes.&lt;/p&gt;

&lt;p&gt;This is the Solana equivalent of the line in your backend route that says if&lt;code&gt;(req.user.id !== resource.ownerId) return res.status(403)&lt;/code&gt;. The difference is you declared it on the struct and Anchor enforces it for you. Because the constraint lives on the account definition, it's much harder to accidentally ship a handler without authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing It
&lt;/h2&gt;

&lt;p&gt;I tested everything locally with LiteSVM; an in-process Solana VM that runs your compiled program against a fresh ledger without touching devnet. Each test gets a clean slate in milliseconds.&lt;br&gt;
A passing test suite only means something if it turns red when something real breaks. Here are two tests that I know are load-bearing, because I deliberately broke the program and watched them catch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy path:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize_then_increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LiteSVM&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... setup omitted for brevity&lt;/span&gt;

    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inc_tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.get_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;counter_kp&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="nf"&gt;.as_slice&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="py"&gt;.authority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&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;This test proves the happy path: initialization stores the correct authority, increment updates the count, and both changes persist on-chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure path:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;increment_fails_when_wrong_authority_signs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// authority_a creates the counter&lt;/span&gt;
    &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"initialize should succeed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// authority_b tries to increment it&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.send_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad_tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"increment should fail when signed by the wrong authority"&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;This test would fail if the &lt;code&gt;has_one = authority&lt;/code&gt; constraint was removed. Without this test, a program that silently allows any wallet to increment anyone's counter would still look green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking things on purpose
&lt;/h2&gt;

&lt;p&gt;On Day 61 I planted three bugs to prove my tests were actually doing their job. The one that surprised me most was this: I commented out the line that sets authority during initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// counter.authority = ctx.accounts.authority.key();  &amp;lt;-- removed&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&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="nf"&gt;Ok&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;Because the authority field was never initialized, the has_one constraint failed immediately, and the test caught it before the program could proceed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Error Code: ConstraintHasOne. Error Number: 2001. Error Message: A has one constraint was violated.
Program log: Left.&lt;span class="s2"&gt;"
"&lt;/span&gt;Program log: 11111111111111111111111111111111&lt;span class="s2"&gt;",
"&lt;/span&gt;Program log: Right:&lt;span class="s2"&gt;"
"&lt;/span&gt;Program log: HeKXcf775y8MYdTbSSYiTSL64fHiEAoM7h6c86WV4isT&lt;span class="s2"&gt;", 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the assertion on &lt;code&gt;parsed.authority&lt;/code&gt;, this bug would have silently deployed and broken every increment call for every user whose counter got initialized after that change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd write next
&lt;/h2&gt;

&lt;p&gt;The counter works, but it still has one limitation: its address is random. The next thing I want to learn is Program Derived Addresses (PDAs), which let a program deterministically derive an account address from known seeds. That means a client can always find a user's counter without storing an extra keypair.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>solana</category>
    </item>
    <item>
      <title>Why Do Token-2022 Extensions Have to Be Added at Mint Creation?</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Wed, 01 Jul 2026 22:50:47 +0000</pubDate>
      <link>https://dev.to/devduchess/why-do-token-2022-extensions-have-to-be-added-at-mint-creation-1m58</link>
      <guid>https://dev.to/devduchess/why-do-token-2022-extensions-have-to-be-added-at-mint-creation-1m58</guid>
      <description>&lt;p&gt;On Day 31 of &lt;a href="https://dev.to/100daysofsolana/100-daily-challenges-to-learn-web3-and-solana-3g2i"&gt;MLH's 100DaysofSolana&lt;/a&gt;, I built a Token-2022 mint with a transfer fee extension. The instructions included one sentence that I didn't think much about at the time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Extensions must be configured when the mint is first created; you cannot add them later."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the time, I treated it like any other instruction in a tutorial. I assumed it was simply one of those framework-specific rules you follow without asking too many questions. So I created the mint, completed the challenge, and moved on.&lt;br&gt;
But over the next few days, as I created more Token-2022 mints with different extensions, I realized this wasn't just a one-off requirement for transfer fees. Every extension had to be decided before the mint existed. If I can deploy new features to a web application later, or add a new column to a database, why can't I create a token today with transfer fees and decide next week that I also want it to earn interest?&lt;br&gt;
As it turns out, the answer has less to do with Token-2022 itself and more to do with how Solana stores data.&lt;/p&gt;

&lt;p&gt;But before I go on, a quick detour if you are new to the world of tokens and Token-2022&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Tokens
&lt;/h2&gt;

&lt;p&gt;A token represents a digital asset on the blockchain. It could be a stablecoin, a governance token, a loyalty point, or even an NFT. But regardless of what it represents, every token begins with a mint.&lt;/p&gt;

&lt;p&gt;You can think of a mint as the blueprint for that token. It defines core properties such as the token's supply, decimal precision, mint authority, and the rules that govern how new tokens are issued.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Token-2022 Extensions
&lt;/h2&gt;

&lt;p&gt;The original &lt;a href="https://www.solana-program.com/docs/token-2022" rel="noopener noreferrer"&gt;Solana Token Program&lt;/a&gt; provides the basic functionality most tokens need: creating tokens, minting new ones, transferring them between accounts, and burning them. But many real-world applications require additional behavior. A stablecoin might charge a transfer fee. A rewards token might earn interest over time. An NFT collection might store metadata directly on-chain.&lt;/p&gt;

&lt;p&gt;Instead of creating a new token program for every one of these use cases, Token-2022 introduces &lt;a href="https://solana.com/docs/tokens/extensions" rel="noopener noreferrer"&gt;extensions&lt;/a&gt; (optional features) that add new capabilities to a token mint. You choose only the extensions your application needs, whether that's transfer fees, interest-bearing balances, metadata pointers, permanent delegates, or something else.&lt;/p&gt;

&lt;p&gt;Now that that's cleared, let's circle back to the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If they're just optional features... why can't I enable one later?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've ever enabled authentication, payments, or file uploads in a web application, you've already worked with the idea of adding capabilities to a system over time. That's why Solana's requirement to choose certain token capabilities before the token even exists feels so unusual.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Missing Piece
&lt;/h2&gt;

&lt;p&gt;As I continued through the challenge, I created different Token-2022 mints with different extensions. Then, on Day 39, instead of creating another token, we inspected the ones we'd already built by running the token display command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;spl-token display $&lt;/span&gt;MINT &lt;span class="nt"&gt;--program-2022&lt;/span&gt;
&lt;span class="go"&gt;

SPL Token Mint
---
Extensions
------------
Transfer Fee
Metadata Pointer
Metadata

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

&lt;/div&gt;



&lt;p&gt;I repeated this for three different tokens and compared their account sizes and rent costs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mint address&lt;/th&gt;
&lt;th&gt;Extensions enabled&lt;/th&gt;
&lt;th&gt;Account data size (bytes)&lt;/th&gt;
&lt;th&gt;Rent cost (SOL)&lt;/th&gt;
&lt;th&gt;Key authorities&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Ay6J...qTwGkDp&lt;/code&gt;&lt;br&gt;
&lt;/td&gt;
&lt;td&gt;Interest-bearing&lt;/td&gt;
&lt;td&gt;222&lt;/td&gt;
&lt;td&gt;0.002436&lt;/td&gt;
&lt;td&gt;Mint authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;&lt;br&gt;Freeze authority: None&lt;br&gt;Rate authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;5CyH...vwwSx&lt;/code&gt;&lt;br&gt;
&lt;/td&gt;
&lt;td&gt;Interest-bearing&lt;br&gt;Transfer fees&lt;br&gt;Metadata Pointer&lt;br&gt;Metadata&lt;/td&gt;
&lt;td&gt;599&lt;/td&gt;
&lt;td&gt;0.00505992&lt;/td&gt;
&lt;td&gt;Mint authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;&lt;br&gt;Freeze authority: None&lt;br&gt;Rate authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;&lt;br&gt;Transfer fee config authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;&lt;br&gt;Transfer fee withdrawal authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Cc1E...XvmH&lt;/code&gt;&lt;br&gt;
&lt;/td&gt;
&lt;td&gt;Default Account State (Frozen)&lt;/td&gt;
&lt;td&gt;171&lt;/td&gt;
&lt;td&gt;0.00208104&lt;/td&gt;
&lt;td&gt;Mint authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;&lt;br&gt;Freeze authority:&lt;br&gt;&lt;code&gt;EHAj6...XQgj&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing immediately stood out, the more extensions a mint had, the larger the account became.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Extensions Must Be Added at Creation
&lt;/h2&gt;

&lt;p&gt;The answer lies in how Solana stores data. On Solana, everything is stored in accounts. Unlike a typical database where storage can grow as you add more data, Solana accounts are created with a fixed amount of space. Once an account is initialized, that allocated space doesn't automatically expand.&lt;/p&gt;

&lt;p&gt;That means when you create a Token-2022 mint, Solana isn't just creating a token, it is allocating enough storage to hold everything that mint will ever need. Every extension you enable becomes part of that mint account, it isn't just a switch that turns a feature on or off. It stores its own configuration data inside the mint account.&lt;/p&gt;

&lt;p&gt;For example, a transfer fee extension needs to keep track of information such as the fee basis points, the maximum fee, and the authorities responsible for updating or withdrawing fees. A metadata extension stores information about where the token's metadata lives. Since each extension carries its own data, every additional capability increases the amount of storage the mint account requires and Solana needs to know exactly how much space to allocate before the mint exists. The process looks something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Choose the extensions your token needs&lt;br&gt;
 │ &lt;br&gt;
 ▼ &lt;br&gt;
Calculate the total account size &lt;br&gt;
 │ &lt;br&gt;
 ▼ &lt;br&gt;
Allocate storage for the mint account &lt;br&gt;
 │ &lt;br&gt;
 ▼ &lt;br&gt;
Calculate the rent-exempt balance &lt;br&gt;
 │ &lt;br&gt;
 ▼ &lt;br&gt;
Create the mint&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, that instruction from Day 31 stopped sounding like a rule I had to memorize, it became the natural consequence of Solana's account model. I now know that I can't add an extension later because there isn't extra space waiting to be used. Adding another extension later would require the mint account to grow, and Solana accounts don't grow like rows in a relational database.&lt;/p&gt;

&lt;p&gt;Coming from Web2, I was used to thinking of software as something that evolves over time. If I needed another feature, I'd update the schema or deploy another version of the application. Token-2022 challenged that mental model. On Solana, you're not just defining what your token does today; you're deciding how much space it will ever need before it's even created. A short comparison like this helps to put it more into perspective.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Web2&lt;/th&gt;
&lt;th&gt;Solana&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add a database column later&lt;/td&gt;
&lt;td&gt;Allocate account space up front&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy new application features&lt;/td&gt;
&lt;td&gt;Decide mint capabilities before creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage grows as needed&lt;/td&gt;
&lt;td&gt;Storage is fixed at account creation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

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

&lt;p&gt;Looking back, that sentence from Day 31 wasn’t just a rule to follow, it was a constraint that revealed how Token-2022 is designed. At first, it felt like an arbitrary limitation: why force developers to decide everything up front? But after working through multiple mints and eventually inspecting them on-chain, the reason became clear.&lt;/p&gt;

&lt;p&gt;Token-2022 isn’t designed around “features you can turn on later.” It’s designed around state that must be fully allocated before it exists. Every extension becomes part of the mint’s storage footprint from the very beginning, which is why the system forces you to define its shape at creation time.&lt;/p&gt;

&lt;p&gt;What changed for me wasn’t just my understanding of Token-2022, it was how I think about building on Solana. On Solana, you don’t evolve accounts over time; you design them before they exist. That’s a very different way of thinking compared to Web2, and one of the most valuable lessons I took away from Epoch 2 of 100 Days of Solana.&lt;/p&gt;

&lt;p&gt;If you've had a similar "Why does Solana work this way?" moment while learning, I'd love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>solana</category>
    </item>
    <item>
      <title>Reading Token Configuration on Solana: Why the Mint Became My Source of Truth</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Tue, 23 Jun 2026 16:59:47 +0000</pubDate>
      <link>https://dev.to/devduchess/reading-token-configuration-on-solana-why-the-mint-became-my-source-of-truth-1dj1</link>
      <guid>https://dev.to/devduchess/reading-token-configuration-on-solana-why-the-mint-became-my-source-of-truth-1dj1</guid>
      <description>&lt;p&gt;Early in my Solana journey, most of my attention was on commands. Which flag do I need? Which address goes here? Which extension am I enabling?. This week, I realized those questions matter less than I thought. The commands are temporary. The mint configuration is what survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  From commands to configuration
&lt;/h2&gt;

&lt;p&gt;Over the past few weeks, I've been working with Token-2022 extensions such as transfer fees, interest-bearing balances, metadata, and non-transferable tokens. At first, building these assets felt command-heavy. Every operation required long mint addresses, token account addresses, and extension flags.&lt;/p&gt;

&lt;p&gt;One small workflow improvement changed that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;mint-address&amp;gt; 
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MY_TA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;token-account&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of repeatedly pasting addresses into every command, I stored them as shell variables and reused them throughout the session.&lt;br&gt;
The immediate benefit was cleaner commands and fewer mistakes. But it also shifted my attention away from the mechanics of running commands and toward the asset itself. I stopped thinking about how I created a token and started thinking about how the token was configured.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mint is the source of truth
&lt;/h2&gt;

&lt;p&gt;The command I used most this week wasn't a creation command.&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token display $MINT&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This command became my primary inspection tool. Rather than relying on memory or searching through old terminal history, I could read the mint directly and verify its configuration.&lt;/p&gt;

&lt;p&gt;From a single output, I could see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transfer fee settings&lt;/li&gt;
&lt;li&gt;Interest-bearing configuration&lt;/li&gt;
&lt;li&gt;Authorities&lt;/li&gt;
&lt;li&gt;Metadata&lt;/li&gt;
&lt;li&gt;Extension information&lt;/li&gt;
&lt;li&gt;Non-transferable restrictions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a Web2 developer, this felt surprisingly familiar. When debugging an application, I often look for the configuration that defines how the system behaves. On Solana, the mint serves a similar role. It contains the configuration that determines what a token can do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh2r33pek64aupu81t6ca.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh2r33pek64aupu81t6ca.png" alt="screenshot of spl-token display output showing multiple extensions" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Token behavior lives in configuration
&lt;/h2&gt;

&lt;p&gt;One example that reinforced this idea was an interest-bearing token I had created earlier. My original expectation was simple: if a token earns interest, the balance stored in the account should increase over time. That's not what happens.&lt;/p&gt;

&lt;p&gt;The raw amount stored in the token account remains unchanged. Instead, the mint stores the interest-rate configuration, and wallets calculate the interest-adjusted balance when displaying it.&lt;/p&gt;

&lt;p&gt;In other words, the behavior comes from the mint's configuration rather than from continuously updating token accounts. That distinction is easy to miss if you're only looking at wallet balances. It becomes obvious when you inspect the mint itself. The token account stores the amount, the mint defines how that amount should be interpreted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tooling can hide complexity, but not configuration
&lt;/h2&gt;

&lt;p&gt;Another interesting moment came when creating a new non-transferable token.&lt;br&gt;
Earlier in the challenge, configuring Token-2022 assets often meant specifying programs and extensions manually. By Day 54, I was using simpler commands and extension flags such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--program-2022&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI was doing more of the work for me.&lt;/p&gt;

&lt;p&gt;From a developer experience perspective, this is great. The tooling becomes easier to use while still producing the same underlying result. What stood out, though, was that the mint configuration remained the important part. The command could change, the flags could change.&lt;br&gt;
Future versions of the tooling might simplify the workflow even further. But once the mint is created, its configuration is what determines how the asset behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;This week changed how I think about verification on Solana.&lt;br&gt;
Wallets, explorers, and developers are all reading the same on-chain configuration. If I want to know whether a token charges transfer fees, accrues interest, or can be transferred at all, I don't need a private API or application dashboard. I can inspect the mint directly.&lt;/p&gt;

&lt;p&gt;That's the shift that stuck with me. The commands that created the asset are temporary, the token's configuration is permanent and on Solana, the mint is often the best place to understand what an asset actually is.&lt;/p&gt;

&lt;p&gt;This post is part of #100DaysOfSolana. Follow along as I continue exploring how assets and applications are built on Solana.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
      <category>web3</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Understanding Solana NFTs as a Web2 Developer: More Than Just JPEGs</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Sat, 13 Jun 2026 11:37:41 +0000</pubDate>
      <link>https://dev.to/devduchess/understanding-solana-nfts-as-a-web2-developer-more-than-just-jpegs-5ekm</link>
      <guid>https://dev.to/devduchess/understanding-solana-nfts-as-a-web2-developer-more-than-just-jpegs-5ekm</guid>
      <description>&lt;p&gt;I knew NFT stood for non-fungible token, meaning a unique token that isn't interchangeable like a currency. Beyond that, I mostly associated NFTs with digital images and ownership.&lt;/p&gt;

&lt;p&gt;After spending a week creating NFTs, attaching metadata, organizing them into collections, auditing them on-chain, and updating them live on devnet, I came away with a different mental model. What changed wasn't my understanding of ownership.&lt;br&gt;
It was my understanding of structure.&lt;/p&gt;
&lt;h2&gt;
  
  
  An NFT is still a token
&lt;/h2&gt;

&lt;p&gt;The first thing I learned is that NFTs are not a separate asset type on Solana.&lt;/p&gt;

&lt;p&gt;Under the hood, the NFT I created used the same SPL token model I had been working with throughout the Token-2022 challenges.&lt;/p&gt;

&lt;p&gt;There was still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a mint account&lt;/li&gt;
&lt;li&gt;a token account&lt;/li&gt;
&lt;li&gt;an owner wallet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference was entirely in the configuration. The NFT mint was created with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a supply of 1&lt;/li&gt;
&lt;li&gt;0 decimals&lt;/li&gt;
&lt;li&gt;the mint authority removed after minting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the mint authority was disabled, no additional tokens could ever be created. That permanently locked the supply at one.&lt;/p&gt;

&lt;p&gt;What appears to be a completely different asset category is actually the same token architecture configured with different rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token create-token &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0

spl-token mint &amp;lt;NFT_MINT_ADDRESS&amp;gt; 1

spl-token authorize &amp;lt;NFT_MINT_ADDRESS&amp;gt; mint &lt;span class="nt"&gt;--disable&lt;/span&gt;
&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%2Fa64etiucrno0ebkjpj1n.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%2Fa64etiucrno0ebkjpj1n.png" alt="NFT Explorer page showing supply = 1 and decimals = 0" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata gives the asset context
&lt;/h2&gt;

&lt;p&gt;A token with a supply of one is unique, but uniqueness alone does not explain what the asset represents. To make the NFT meaningful, I attached metadata using the &lt;a href="https://solana.com/docs/tokens/extensions/metadata" rel="noopener noreferrer"&gt;Metadata Extension&lt;/a&gt; which included a name, a symbol and URI.&lt;br&gt;
The URI points to a JSON file containing additional information such as the image, description, and custom attributes.&lt;/p&gt;

&lt;p&gt;One detail I hadn't considered before is why the URI exists in the first place. Storing images and large metadata files directly on-chain would increase account size significantly, making storage more expensive. Instead, the NFT stores a reference to the metadata through a URI, while the larger files remain off-chain.&lt;/p&gt;

&lt;p&gt;When a wallet displays an NFT, it reads the metadata stored on-chain, follows the URI, retrieves the JSON file, and then renders the asset. In other words, the NFT stores the reference, not the image itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Collections introduce relationships
&lt;/h2&gt;

&lt;p&gt;Creating a collection introduced another layer of structure. Using the &lt;a href="https://solana.com/docs/tokens/extensions/group-member" rel="noopener noreferrer"&gt;Group&lt;/a&gt; and &lt;a href="https://solana.com/docs/tokens/extensions/group-member" rel="noopener noreferrer"&gt;Member&lt;/a&gt; extensions, I created a collection NFT and linked multiple NFTs to it.&lt;/p&gt;

&lt;p&gt;The simplest way I can describe it from a Web2 perspective is as a parent-child relationship.&lt;/p&gt;

&lt;p&gt;The collection acts as the parent, the individual NFTs act as members that reference that parent. This reminded me of how related records are connected in a database. The relationship is not implied by naming conventions or application logic. It is stored directly on-chain. That means anyone can independently verify whether an NFT belongs to a collection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a collection mint&lt;/span&gt;
spl-token create-token &lt;span class="nt"&gt;--enable-group&lt;/span&gt;

&lt;span class="c"&gt;# Create an NFT and attach it to the collection&lt;/span&gt;
spl-token create-token &lt;span class="nt"&gt;--group&lt;/span&gt; &amp;lt;COLLECTION_MINT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying the relationship on-chain
&lt;/h2&gt;

&lt;p&gt;After creating the collection, I wanted to see how that relationship was actually represented. Using spl-token display and &lt;a href="https://explorer.solana.com" rel="noopener noreferrer"&gt;Solana Explorer&lt;/a&gt;, I inspected both the collection mint and the NFT mints.&lt;/p&gt;

&lt;p&gt;The important detail was that each NFT contained a Group reference pointing back to the collection mint. That meant collection membership wasn't just something maintained by a marketplace or wallet interface. It was part of the NFT's on-chain configuration and could be verified by anyone reading the account data.&lt;/p&gt;

&lt;p&gt;This was one of the more useful exercises of the week because it reinforced a recurring theme in Solana development:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;don't assume something exists—inspect the account and verify it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffza1k1g3fxpkd93cf917.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%2Ffza1k1g3fxpkd93cf917.png" alt="Screenshot of the collection NFT CLI output showing Group / Member relationship" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating metadata after minting
&lt;/h2&gt;

&lt;p&gt;One thing I tested next was modifying the NFT after it had already been created.&lt;br&gt;
Using the Token-2022 metadata extension, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changed the NFT name&lt;/li&gt;
&lt;li&gt;added and removed custom metadata fields&lt;/li&gt;
&lt;li&gt;updated the URI to point to a different metadata file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The updates appeared immediately on-chain.&lt;/p&gt;

&lt;p&gt;The image, however, did not always update immediately. The reason is that these are two different layers of data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The name, symbol, URI, and metadata fields live on-chain.&lt;br&gt;
The image is stored off-chain and retrieved through the URI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many wallets cache metadata responses, which means visual updates can take longer to appear even though the on-chain change has already been confirmed. This was a useful reminder that an NFT is often a combination of on-chain state and off-chain content.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in my understanding
&lt;/h2&gt;

&lt;p&gt;I started the arc thinking of NFTs primarily in terms of uniqueness and ownership. I finished thinking about configuration, metadata, and relationships.&lt;/p&gt;

&lt;p&gt;The NFT itself was not the most interesting part.&lt;/p&gt;

&lt;p&gt;The more interesting discovery was that NFTs on Solana are built from the same foundations as every other asset I've worked with so far: mints, token accounts, metadata, and program-enforced rules. The image is what users see, the structure behind it is what defines the asset.&lt;/p&gt;

&lt;p&gt;This post is part of #100DaysOfSolana. Follow along as I continue exploring how assets are built on Solana.&lt;/p&gt;

</description>
      <category>nft</category>
      <category>webdev</category>
      <category>blockchain</category>
      <category>solana</category>
    </item>
    <item>
      <title>Building a Soulbound Credential on Solana with Token-2022</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Mon, 01 Jun 2026 12:53:27 +0000</pubDate>
      <link>https://dev.to/devduchess/building-a-soulbound-credential-on-solana-with-token-2022-57l8</link>
      <guid>https://dev.to/devduchess/building-a-soulbound-credential-on-solana-with-token-2022-57l8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How combining Non-Transferable and Permanent Delegate extensions changed my understanding of digital ownership on Solana.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine earning a high rank in an online game.&lt;/p&gt;

&lt;p&gt;You can use it. You can benefit from it. Other players can see it. But you cannot transfer that rank to someone else.&lt;/p&gt;

&lt;p&gt;If you break the game's rules, the game company can revoke it. And if the game gets acquired by another company, authority over that rank can be handed over to a new administrator.&lt;/p&gt;

&lt;p&gt;That was the closest mental model I found for understanding one of the most interesting things I built this week on Solana: a credential token using Token-2022 extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Token Extensions?
&lt;/h2&gt;

&lt;p&gt;One thing I've learned during this phase of the challenge is that Token-2022 is not just about creating tokens.&lt;/p&gt;

&lt;p&gt;It allows you to define behaviors directly at the token level through extensions.&lt;/p&gt;

&lt;p&gt;Instead of relying on application logic to enforce rules, the token program itself can enforce them.&lt;/p&gt;

&lt;p&gt;For this experiment, I combined two extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-Transferable&lt;/strong&gt; — prevents ownership from being transferred to another wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent Delegate&lt;/strong&gt; — gives a designated authority the ability to manage the token even after it has been issued.
Individually, those are useful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, they create something that behaves less like a currency and more like a credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transfer That Was Supposed to Fail
&lt;/h2&gt;

&lt;p&gt;After creating the token, I tried transferring it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token transfer Gn5PZzwDENvpQESaFwgVqzCUFba5sSka59iLtjFTYNvz 1 &lt;span class="nv"&gt;$THIRD_PARTY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--owner&lt;/span&gt; ~/recipient-wallet.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fee-payer&lt;/span&gt; ~/.config/solana/id.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fund-recipient&lt;/span&gt; &lt;span class="nt"&gt;--allow-unfunded-recipient&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recipient: ErYcpQYcpdoaaiufrB3MqQE2QaVEhoACZGcssppszPpY

Recipient Token Account:
8CvyoKt11UbkWYRVXey6UKK2gqQcvVvbh9Lv3crzo8C3

Funding ATA:
8Cvyokt11UbkWYRVXey6UKK2qqQcvVvbh9LV3crz0803

Status:
❌ Transaction failed during simulation

Error:
Transfer is disabled for this mint (Token-2022 restriction)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I liked about this failure is that it wasn't unexpected.&lt;br&gt;
The token holder owned the token account. Normally, that would be enough. But because the token was created with the Non-Transferable extension, the transfer was rejected by the token program itself.&lt;/p&gt;

&lt;p&gt;There was no application checking permissions, no backend deciding whether the action was allowed.&lt;/p&gt;

&lt;p&gt;The restriction lived inside the asset. That was the moment the idea really clicked for me.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Part That Surprised Me
&lt;/h2&gt;

&lt;p&gt;The failed transfer wasn't the surprising part, the Permanent Delegate extension was.&lt;/p&gt;

&lt;p&gt;While experimenting, I discovered that the delegate authority itself could be reassigned. I tried delegating the authority to another account&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spl-token authorize &lt;span class="se"&gt;\&lt;/span&gt;
  Gn5PZzwDENvpQESaFwgVqzCUFba5sSka59iLtjFTYNvz &lt;span class="se"&gt;\&lt;/span&gt;
  permanent-delegate 12ntWdec88nfRq5uaaxkEBy4EtdQjXuD8ywWzxDjCEM2  &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, that means the authority responsible for managing a credential can change over time.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A company issues employee credentials.&lt;/li&gt;
&lt;li&gt;The company gets acquired.&lt;/li&gt;
&lt;li&gt;Authority over those credentials is transferred to a new administrator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The credentials remain valid, the people holding them do not change but the entity responsible for managing them can.&lt;/p&gt;

&lt;p&gt;That felt much more flexible than I initially expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Confused Me
&lt;/h2&gt;

&lt;p&gt;I'll be honest: part of me kept thinking this felt surprisingly centralized.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A credential holder cannot transfer the token.&lt;/li&gt;
&lt;li&gt;A delegate can revoke it.&lt;/li&gt;
&lt;li&gt;Authority can even be reassigned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, that seems to clash with the idea of decentralization. What helped me reconcile that is realizing that not every asset is trying to solve the same problem.&lt;/p&gt;

&lt;p&gt;Money, governance tokens, certificates, memberships, and identity credentials all have different requirements.&lt;/p&gt;

&lt;p&gt;In some cases, transferability is the feature. In others, transferability defeats the entire purpose.&lt;/p&gt;

&lt;p&gt;A certification would lose its meaning if anyone could sell it.&lt;/p&gt;

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

&lt;p&gt;Earlier in the week, I experimented with other Token-2022 extensions, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfer fees&lt;/li&gt;
&lt;li&gt;interest-bearing rates&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What finally made sense was seeing multiple extensions work together inside a single asset.&lt;/p&gt;

&lt;p&gt;The token wasn't just representing value anymore, it was representing rules, permissions, and relationships.&lt;/p&gt;

&lt;p&gt;That's a very different way of thinking about assets.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does this token represent?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What behaviors does this token enforce?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Before this week, I mostly thought of tokens as things that hold value and move between wallets.&lt;/p&gt;

&lt;p&gt;Building a credential token challenged that assumption.&lt;/p&gt;

&lt;p&gt;The combination of Non-Transferable and Permanent Delegate extensions showed me that tokens can represent much more than currency. They can represent memberships, achievements, certifications, and other forms of ownership where movement is not the goal.&lt;/p&gt;

&lt;p&gt;If you want to explore these concepts further, I recommend reading the official &lt;a href="https://solana.com/docs/tokens/extensions" rel="noopener noreferrer"&gt;Token-2022 Extensions documentation&lt;/a&gt; and experimenting with a few combinations yourself.&lt;/p&gt;

&lt;p&gt;The most interesting part isn't any single extension.&lt;/p&gt;

&lt;p&gt;It's seeing what happens when you start combining them.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>learning</category>
      <category>100daysofsolana</category>
      <category>solana</category>
    </item>
    <item>
      <title>Why One Solana Token Needs Three Different Addresses</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Sun, 24 May 2026 12:06:08 +0000</pubDate>
      <link>https://dev.to/devduchess/why-one-solana-token-needs-three-different-addresses-2ocf</link>
      <guid>https://dev.to/devduchess/why-one-solana-token-needs-three-different-addresses-2ocf</guid>
      <description>&lt;p&gt;If you’re new to Solana, one of the most confusing things about SPL tokens is why a single token interaction can involve multiple addresses.&lt;br&gt;
This post breaks down the difference between mint addresses, wallet addresses, and token accounts, and why understanding that distinction makes the Solana CLI suddenly start making sense.&lt;/p&gt;

&lt;p&gt;When I first started working with SPL tokens, I kept running into the same problem. Every action seemed to require a different address. I wasn’t just dealing with “a token.”&lt;br&gt;
I was dealing with a mint address, a wallet address and a token account address.&lt;br&gt;
But the more I used the CLI, the more I realized something:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I wasn’t interacting with one system. I was interacting with three different responsibilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where the Address Model Starts Becoming Important
&lt;/h2&gt;

&lt;p&gt;What made things confusing was that different commands kept expecting different kinds of addresses.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-token&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This gives you a mint address.&lt;/p&gt;

&lt;p&gt;Then I would move to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-account &amp;lt;MINT_ADDRESS&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now I have a token account.&lt;/p&gt;

&lt;p&gt;And finally:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token transfer &amp;lt;MINT_ADDRESS&amp;gt; 100 &amp;lt;RECIPIENT_WALLET_ADDRESS&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is where things started to feel inconsistent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is the mint address used here?&lt;/li&gt;
&lt;li&gt;Why is the wallet used here?&lt;/li&gt;
&lt;li&gt;And where exactly is the token actually stored?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The key idea I was missing
&lt;/h3&gt;

&lt;p&gt;What helped everything click was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A wallet does not hold tokens on Solana.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds simple, but it changes everything.&lt;br&gt;
Because if wallets don’t hold tokens… then something else must. That “something else” is the token account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-address model
&lt;/h2&gt;

&lt;p&gt;Once I mapped everything properly, the system became much clearer.&lt;/p&gt;

&lt;p&gt;1.Mint Address — the token identity&lt;/p&gt;

&lt;p&gt;This is created when you run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-token&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The mint defines what the token is, its rules and its supply. It doesn’t hold tokens, it defines the token itself.&lt;/p&gt;

&lt;p&gt;2.Wallet Address — the owner identity&lt;/p&gt;

&lt;p&gt;This is your actual account (keypair). It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signs transactions&lt;/li&gt;
&lt;li&gt;authorizes actions&lt;/li&gt;
&lt;li&gt;represents the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it does not store token balances, it only provides authority.&lt;/p&gt;

&lt;p&gt;3.Token Account — the balance container&lt;/p&gt;

&lt;p&gt;This is where things actually live.&lt;/p&gt;

&lt;p&gt;Created using:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-account &amp;lt;MINT_ADDRESS&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A token account stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;balance&lt;/li&gt;
&lt;li&gt;association to one mint&lt;/li&gt;
&lt;li&gt;ownership by a wallet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real structure is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;one wallet → many token accounts → one per mint relationship&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why balance requires BOTH wallet and mint
&lt;/h3&gt;

&lt;p&gt;This was one of the most confusing parts for me:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token balance --owner &amp;lt;WALLET_ADDRESS&amp;gt; &amp;lt;MINT_ADDRESS&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Why both?&lt;/p&gt;

&lt;p&gt;Because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wallet = who owns the tokens&lt;/li&gt;
&lt;li&gt;mint = which token we are talking about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A wallet can hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;USDC&lt;/li&gt;
&lt;li&gt;custom tokens&lt;/li&gt;
&lt;li&gt;NFTs&lt;/li&gt;
&lt;li&gt;multiple SPL tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the CLI needs both inputs to find the correct token account. Under the hood, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;derives the associated token account (ATA) for that wallet + mint pair&lt;/li&gt;
&lt;li&gt;then reads the balance from it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why both are required.&lt;br&gt;
&lt;a href="https://solana.com/docs/tokens/basics/create-token-account?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Associated Token Accounts&lt;/a&gt; are derived automatically from a wallet + mint pair using Solana’s ATA standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why transfers feel “indirect”
&lt;/h3&gt;

&lt;p&gt;This command made more sense after understanding the model:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token transfer &amp;lt;MINT_ADDRESS&amp;gt; 100 &amp;lt;RECIPIENT_WALLET_ADDRESS&amp;gt; --fund-recipient&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The key detail is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tokens are not sent to wallets directly&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They are sent to token accounts. So what actually happens is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;system checks if recipient has a token account for this mint&lt;/li&gt;
&lt;li&gt;if not, &lt;code&gt;--fund-recipient&lt;/code&gt; creates it&lt;/li&gt;
&lt;li&gt;tokens are then transferred into that account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s why the wallet address is enough, but only because the system can derive the token account from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token-2022 and programmable assets
&lt;/h2&gt;

&lt;p&gt;One of the most interesting parts of this journey was Token-2022.&lt;/p&gt;

&lt;p&gt;Unlike standard SPL tokens, Token-2022 introduces extensions that add behavior directly at the protocol level.&lt;br&gt;
Solana’s &lt;a href="https://solana.com/docs/tokens/extensions?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Token Extensions documentation&lt;/a&gt; explains how these extensions allow tokens to enforce behavior directly at the protocol level.&lt;/p&gt;

&lt;p&gt;Examples include transfer fees, non-transferable tokens, metadata extensions, custom rules enforced by the token program itself&lt;/p&gt;

&lt;p&gt;What stood out to me is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These rules must be defined when the mint is created. They cannot be added later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That changes how I think about tokens entirely. They are no longer just balances. They are programmable assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed for me
&lt;/h2&gt;

&lt;p&gt;At first, the CLI felt inconsistent because I was thinking in terms of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“a token”&lt;/li&gt;
&lt;li&gt;“a wallet”&lt;/li&gt;
&lt;li&gt;“a transfer”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But Solana doesn’t structure it that way. Instead, it separates responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mint =&amp;gt; defines what exists&lt;/li&gt;
&lt;li&gt;wallet =&amp;gt; defines who controls actions&lt;/li&gt;
&lt;li&gt;token account =&amp;gt; defines what is owned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I saw that pattern, the system stopped feeling fragmented. It started feeling intentional, even elegant.&lt;/p&gt;

&lt;p&gt;The biggest shift for me was realizing this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Solana doesn’t store tokens in wallets. It stores relationships between identities, assets, and balances.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that the address model finally makes sense to me, I’m starting to see Token-2022 less as “tokens with extra features” and more as programmable assets with rules built directly into the protocol.&lt;/p&gt;

&lt;p&gt;I’m curious to see how far that model can go as the challenge continues.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Understanding Solana’s Account Model From a Web2 Perspective</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Tue, 19 May 2026 22:27:50 +0000</pubDate>
      <link>https://dev.to/devduchess/understanding-solanas-account-model-from-a-web2-perspective-1k35</link>
      <guid>https://dev.to/devduchess/understanding-solanas-account-model-from-a-web2-perspective-1k35</guid>
      <description>&lt;p&gt;In a previous article I posted on my #100daysofsolana journey, I wrote about how everything on Solana is an account. I spoke briefly about the difference between wallet accounts and program accounts. You can read the article &lt;a href="https://dev.to/bettyafolabi/when-wallet-stopped-meaning-wallet-my-solana-journey-3ap9"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we will go more in depth because I now have more knowledge after inspecting various accounts over the past few days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything is an account. But not all accounts are the same
&lt;/h3&gt;

&lt;p&gt;Unlike Ethereum, which separates wallet accounts from smart contracts, Solana uses the same model for all accounts. They all have the same five fields, but what differs is the meaning and value of those fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Account's Five Fields
&lt;/h3&gt;

&lt;p&gt;Every account on the Solana network has these five fields. Here's what they actually mean:&lt;/p&gt;

&lt;p&gt;1.Lamports&lt;br&gt;
This is the field that carries information on how much the account holds. Solana stores balances in lamports.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1 SOL = 1 billion lamports.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes, depending on how you are viewing the account structure, this field shows up as Balance with the value in SOL. Just know it's showing the same thing in a more human readable format.&lt;/p&gt;

&lt;p&gt;2.Data&lt;br&gt;
The data field refers to the storage space the account occupies. In Web2, this is similar to a database entry. It is stored as a raw byte array.&lt;br&gt;
For example, the Token Program's data field looks like this in the raw output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000: 02 00 00 00 27 f1 90 b1 e8 ca df f9 f8 fc 45 cb
0010: d3 af 98 b8 8e 5f ac 42 0d 97 dd 37 ce 71 4c 44
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a more readable format, it shows up as `Length: 36 (0x24) bytes.&lt;/p&gt;

&lt;p&gt;Wallet accounts have an empty or zero data field because they are simple SOL holders. They don't store anything beyond a balance. But program accounts, token accounts, and NFTs all have data because they store state.&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%2F74tctx87ntd43z3qrv0t.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%2F74tctx87ntd43z3qrv0t.png" alt="screenshot of cli output of a wallet account on solana devnet" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fruqt3dm18j5ccaaoq7ra.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%2Fruqt3dm18j5ccaaoq7ra.png" alt="Screenshot of cli output of vote program account and sysvars account" width="799" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also where something important happens: &lt;br&gt;
Programs on Solana don't store their own state inside themselves. A program's executable code lives in one account, and the data it reads or writes lives in completely separate accounts. So when you see a program account with only 36 bytes, that's not where the user balances are, those live in their own individual accounts that the program manages externally.&lt;/p&gt;

&lt;p&gt;3.Owner&lt;br&gt;
This field tells us which program controls and is able to modify the account. Wallet accounts are owned by the System Program. Program accounts are typically owned by the BPF Loader. Built-in system programs, like the System Program itself, are owned by the Native Loader.&lt;br&gt;
Solana has a security model in place such that a program can only write to the accounts it owns.&lt;/p&gt;

&lt;p&gt;4.Executable&lt;br&gt;
This is a boolean field that returns true or false. It tells us whether the account has a set of instructions written in code that it can run. &lt;br&gt;
Program accounts have &lt;code&gt;executable: true&lt;/code&gt; because they run programmed instructions. Other accounts like wallets and token accounts have &lt;code&gt;executable: false&lt;/code&gt; because they don't run code, they just hold state.&lt;/p&gt;

&lt;p&gt;5.Rent Epoch&lt;br&gt;
Solana has a rent system; a cost for storing data on-chain. The rent epoch field was originally designed to show when the next rent would be deducted from the balance. Currently, accounts have been made rent-exempt by being funded with enough SOL upfront, which made this field less important.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to think about it as a Web2 developer
&lt;/h3&gt;

&lt;p&gt;We can compare Solana's account model to how a React app makes calls to a backend REST API in Web2. Stay with me.&lt;/p&gt;

&lt;p&gt;The Solana program is like a backend API, and accounts are the databases the API reads or writes to. The frontend sends a request, and the backend(the program), executes what changes need to happen. On Solana, that request is called a transaction. Just like a REST API hits a POST or PUT endpoint, a Solana transaction invokes a specific program instruction.&lt;/p&gt;

&lt;p&gt;The main thing to remember is that neither system stores information inside the request handler. Instead, data is stored separately, in a database in Web2, or in accounts on Solana. These accounts are like rows in a database. Each one holds some information about the application, identified by a special key.&lt;/p&gt;

&lt;p&gt;So when you look at a Solana account, you can think of it like a record in a database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The public key is like the identifier for the record&lt;/li&gt;
&lt;li&gt;The data field is the actual information in the record&lt;/li&gt;
&lt;li&gt;The lamports field is like a balance or credit attached to the record&lt;/li&gt;
&lt;li&gt;The owner field is the service that is allowed to make changes to the record&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solana's ownership rules
&lt;/h3&gt;

&lt;p&gt;Like I mentioned earlier, in Solana only the owner of an account can modify it. This sounds straightforward, but let's drive it home from both a Web2 and Solana perspective.&lt;/p&gt;

&lt;p&gt;Only the owner can modify an account, but anyone can credit it. Meaning anyone can send SOL to any account. This is the security model Solana enforces at the protocol level.&lt;/p&gt;

&lt;p&gt;Just like in a REST API system, only the backend service that owns a database table is allowed to modify the rows. Other services can request to read the data, but they can't write to it unless they're authorized. Solana enforces the same idea, only the owner program is allowed to mutate the account's data.&lt;/p&gt;

&lt;p&gt;So if a token account is owned by the Token Program, no other program can directly edit its balance. They must go through the Token Program's instructions, just like a frontend must go through the correct API endpoint instead of writing directly to the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are rent exemptions?
&lt;/h3&gt;

&lt;p&gt;I mentioned that recently, accounts have been made rent-exempt. What does that actually mean?&lt;/p&gt;

&lt;p&gt;In Solana, every account is like data stored on-chain, and you have to pay some amount to keep it there. That's rent. For a basic account with no extra data, it's roughly 0.00089 SOL. &lt;br&gt;
But it's not a fixed amount, it's calculated based on the size of the data an account holds using the rent calculation formula.&lt;/p&gt;

&lt;p&gt;In older versions of the model, this rent was collected periodically. When an account didn't have enough lamports, the network would reclaim it.But now, instead of paying periodically, you just fund the account with enough SOL to meet the minimum balance threshold. That makes it permanently rent-exempt. The SOL isn't spent, it's held as a deposit. Close the account, and you get it back.&lt;/p&gt;

&lt;p&gt;You can check the rent exemption amount two ways:&lt;/p&gt;

&lt;p&gt;Using the RPC method: &lt;code&gt;getMinimumBalanceForRentExemption&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or through the CLI:&lt;/p&gt;

&lt;p&gt;&lt;code&gt; solana rent &amp;lt;DATA_SIZE&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;When I started this journey, fields like &lt;code&gt;owner, executable, and rent epoch&lt;/code&gt; were a bit abstract to me. I was staring at output I couldn't read.&lt;/p&gt;

&lt;p&gt;Now I can look at any account on Solana and tell you exactly what it is and what it does just from those five fields. That's the account model, and if you've followed along to this point, you can too.&lt;/p&gt;

&lt;p&gt;Everything on Solana is an account. What differs is just the configuration. That's it. That's the whole foundation.&lt;br&gt;
Once that lands, a lot of other Solana concepts start making sense on their own.&lt;/p&gt;

&lt;p&gt;If you want to make it stick, don't just take my word for it. Run this yourself:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;solana account $(solana adress)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then try it on the Token Program, then the System Program. See the same five fields show up every time. That's the moment it clicks.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>What Failed Transactions Taught Me About Solana</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Mon, 11 May 2026 17:44:10 +0000</pubDate>
      <link>https://dev.to/devduchess/what-failed-transactions-taught-me-about-solana-49m0</link>
      <guid>https://dev.to/devduchess/what-failed-transactions-taught-me-about-solana-49m0</guid>
      <description>&lt;p&gt;A few days ago, Solana transactions still felt abstract to me.&lt;/p&gt;

&lt;p&gt;I understood the general idea:&lt;br&gt;
sign a transaction -&amp;gt; send it to the network -&amp;gt; it gets processed.&lt;/p&gt;

&lt;p&gt;But during this phase of the challenge, things started becoming more concrete.&lt;/p&gt;

&lt;p&gt;Especially when I began inspecting transaction confirmations, experimenting with transfer tooling, and intentionally triggering failed transactions.&lt;/p&gt;

&lt;p&gt;That’s when Solana stopped feeling like “crypto magic” and started feeling more like engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Initial Mental Model
&lt;/h3&gt;

&lt;p&gt;At first, I thought of transactions almost like simple requests:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;send SOL from A -&amp;gt; B&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that was basically it.&lt;/p&gt;

&lt;p&gt;But after exploring transaction anatomy more deeply, I realized a Solana transaction is much more structured than that.&lt;/p&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a signed request&lt;/li&gt;
&lt;li&gt;containing instructions&lt;/li&gt;
&lt;li&gt;executed by programs&lt;/li&gt;
&lt;li&gt;against accounts&lt;/li&gt;
&lt;li&gt;processed by a distributed validator network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official Solana transaction docs also helped clarify how signatures, instructions, and accounts are structured internally:&lt;br&gt;
&lt;a href="https://solana.com/docs/core/transactions" rel="noopener noreferrer"&gt;https://solana.com/docs/core/transactions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That mental shift changed how I started looking at the system.&lt;/p&gt;

&lt;p&gt;One thing that made transactions easier to understand was comparing them to requests in traditional applications.&lt;/p&gt;

&lt;p&gt;Not exactly the same, but close enough to create a mental bridge.&lt;/p&gt;

&lt;p&gt;You construct a request, send it to a distributed network, wait for processing, and eventually receive confirmation back.&lt;/p&gt;

&lt;p&gt;But unlike a normal API request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transactions require signatures&lt;/li&gt;
&lt;li&gt;they expire after some time&lt;/li&gt;
&lt;li&gt;validators participate in confirming them&lt;/li&gt;
&lt;li&gt;and failure can still cost money&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Failed Transactions Changed My Perspective
&lt;/h3&gt;

&lt;p&gt;I intentionally triggered an insufficient funds error expecting the transaction to simply reject without consequences.&lt;/p&gt;

&lt;p&gt;Instead, I learned something important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;failed transactions can still consume fees.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That caught me off guard.&lt;/p&gt;

&lt;p&gt;Even though the transfer failed, validators still processed the transaction and performed work, so fees were still deducted.&lt;/p&gt;

&lt;p&gt;That completely changed how I thought about blockchain transactions.&lt;/p&gt;

&lt;p&gt;In most frontend workflows, a failed action usually just returns an error.&lt;/p&gt;

&lt;p&gt;Here, failure still has a cost.&lt;/p&gt;

&lt;p&gt;And suddenly concepts like validation, simulation, balance checks and transaction design started feeling much more important.&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%2F24a8fvfwbroe4vgvoz60.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%2F24a8fvfwbroe4vgvoz60.png" alt="Screenshot of a failed transaction output" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirmation States Made More Sense Than I Expected
&lt;/h3&gt;

&lt;p&gt;Another concept that stood out to me was transaction confirmation.&lt;/p&gt;

&lt;p&gt;I learned that transactions move through stages like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processed&lt;/li&gt;
&lt;li&gt;Confirmed&lt;/li&gt;
&lt;li&gt;Finalized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, these looked like simple status labels.&lt;/p&gt;

&lt;p&gt;But they actually represent increasing confidence from the network.&lt;/p&gt;

&lt;p&gt;A validator first processes the transaction, then the network reaches supermajority agreement.&lt;/p&gt;

&lt;p&gt;Then enough blocks are added afterward that reversal becomes practically impossible.&lt;/p&gt;

&lt;p&gt;I still don’t fully understand concepts like polling and finality yet, but this was the first time transaction processing started feeling like a real distributed system instead of just a black box.&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%2Fi8nj4pnfmbqs8qpw9cwi.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%2Fi8nj4pnfmbqs8qpw9cwi.png" alt="Screenshot of the progress of a transaction being finalized on the terminal using solana CLI" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  One Detail That Really Stuck With Me
&lt;/h4&gt;

&lt;p&gt;Another thing that genuinely surprised me was learning this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Creating a wallet does not automatically create an on-chain account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The address exists immediately because it’s derived from a keypair.&lt;/p&gt;

&lt;p&gt;But the actual account only exists on-chain once it’s funded.&lt;/p&gt;

&lt;p&gt;That distinction felt subtle at first, but it changed how I think about accounts on Solana.&lt;/p&gt;

&lt;p&gt;Storage and state feel much more explicit here than I initially expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Changed for Me
&lt;/h3&gt;

&lt;p&gt;The more I explored transactions, the less Solana felt like “just sending crypto.”&lt;/p&gt;

&lt;p&gt;It started feeling more like interacting with a distributed system with strict rules around state, validation, execution and confirmation.&lt;/p&gt;

&lt;p&gt;I also became much more aware of the complexity underneath even simple transfers.&lt;/p&gt;

&lt;p&gt;Before this, a transaction felt like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;click send and wait&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validators processing requests&lt;/li&gt;
&lt;li&gt;confirmation stages&lt;/li&gt;
&lt;li&gt;execution rules&lt;/li&gt;
&lt;li&gt;signatures&lt;/li&gt;
&lt;li&gt;fees&lt;/li&gt;
&lt;li&gt;and failure handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;all happening underneath something that initially looked simple.&lt;/p&gt;

&lt;p&gt;I still don’t fully understand every part of the system yet, but transactions no longer feel abstract to me anymore.&lt;/p&gt;

&lt;p&gt;And honestly, that’s probably the biggest shift from this week.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>solana</category>
    </item>
    <item>
      <title>Day 16 of my 100 Days of Solana</title>
      <dc:creator>Elizabeth Afolabi</dc:creator>
      <pubDate>Thu, 07 May 2026 02:11:51 +0000</pubDate>
      <link>https://dev.to/devduchess/day-16-of-my-100-days-of-solana-2bga</link>
      <guid>https://dev.to/devduchess/day-16-of-my-100-days-of-solana-2bga</guid>
      <description>&lt;p&gt;Today I sent a SOL transfer on devnet and inspected the transaction on Solana Explorer.&lt;/p&gt;

&lt;p&gt;One thing that really stood out to me was the &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;--allow-unfunded-recipient flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It helped me understand that on Solana, creating a wallet address does not automatically mean an on-chain account exists.&lt;/p&gt;

&lt;p&gt;The address exists immediately after generating a keypair, but the actual account only exists once it’s funded.&lt;/p&gt;

&lt;p&gt;So in this transaction, I wasn’t just sending SOL;  the network was also creating the recipient account on-chain.&lt;/p&gt;

&lt;p&gt;Very different from Ethereum’s “just send” model.&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%2F08bltlgb2vqpirp4cdum.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%2F08bltlgb2vqpirp4cdum.png" alt="Screenshot of the inspected transaction on solana explorer" width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>solana</category>
    </item>
  </channel>
</rss>
