<?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: Mubarak Yakubu</title>
    <description>The latest articles on DEV Community by Mubarak Yakubu (@mubaraqabba).</description>
    <link>https://dev.to/mubaraqabba</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%2F3922593%2F1c246f7c-3ea4-4a1a-bc90-f3d4c5d24ea7.png</url>
      <title>DEV Community: Mubarak Yakubu</title>
      <link>https://dev.to/mubaraqabba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mubaraqabba"/>
    <language>en</language>
    <item>
      <title>My Solana Program Security Checklist</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:09:15 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/my-solana-program-security-checklist-3jle</link>
      <guid>https://dev.to/mubaraqabba/my-solana-program-security-checklist-3jle</guid>
      <description>&lt;p&gt;A pre-deploy checklist I run top to bottom before any Anchor program goes to mainnet. Built from the bugs I reproduced myself.&lt;/p&gt;

&lt;p&gt;This checklist is for Anchor developers about to deploy to mainnet. Run it top to bottom before every deploy. It's built from the bugs I reproduced myself over the last few days: forged accounts, arithmetic overflows, and CPI trust failures. Each item is a yes-or-no check. If you can't answer yes, stop and fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Account Validation&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every deserialized account has its owner verified. Typed &lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; does this automatically. Raw &lt;code&gt;AccountInfo&lt;/code&gt; or &lt;code&gt;UncheckedAccount&lt;/code&gt; does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Account types are distinguished by their 8-byte discriminator, so one account type cannot be passed where another is expected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any &lt;code&gt;remaining_accounts&lt;/code&gt; passed in are validated before use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any PDA is verified with &lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump,&lt;/code&gt; not just accepted by address.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; In February 2022, $326M left the Wormhole bridge because the program trusted a forged account without verifying it was the real one. The fix was to check the owner before reading the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Authority and Signer Checks&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every privileged instruction confirms the expected signer with &lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; or &lt;code&gt;has_one / constraint&lt;/code&gt; checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No instruction trusts a pubkey without also checking its signer flag. Comparing a public key only proves someone knew a public key. Public keys are public.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;UncheckedAccount&lt;/code&gt; for an authority is a red flag. If you see it, you have no signer check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Admin-only instructions check the admin against stored state AND require the signer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The vault's owner is set on first deposit and never silently overwritten.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Comparing a public key without checking the signer flag lets an attacker pass any public key. The instruction must require a signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Arithmetic Safety&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every balance or supply change uses &lt;code&gt;checked_add&lt;/code&gt;, &lt;code&gt;checked_sub&lt;/code&gt;, or &lt;code&gt;checked_mul&lt;/code&gt;, never raw &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;*&lt;/code&gt; on untrusted values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No silent cast that could truncate (e.g., &lt;code&gt;u64&lt;/code&gt; to &lt;code&gt;u32&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overflow and underflow return a named error, not a panic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Property tests prove that a deposit never shrinks a balance and a withdrawal never increases it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; A &lt;code&gt;u64&lt;/code&gt; that overflows wraps silently to a tiny number. Without checked math, a deposit of 2 lamports into a balance one lamport below &lt;code&gt;u64::MAX&lt;/code&gt; wraps the balance back to 1. The attacker destroys the vault with a tiny deposit.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. CPI Safety&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every CPI verifies the target program ID is the one expected, not whatever account the caller supplied.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Program&amp;lt;'info, T&amp;gt;&lt;/code&gt; is used where possible, so Anchor checks the program ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Accounts are reloaded after a CPI if their data is read again. The callee may have changed the account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signer seeds used for PDA-signed CPIs match the seeds in the &lt;code&gt;#[account(seeds = ...)]&lt;/code&gt; constraint exactly. One byte off and the runtime rejects the PDA.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No CPI trusts the callee to validate its own accounts. The caller is responsible for passing the right ones.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; A CPI that accepts whatever program ID the caller supplies can be redirected to an attacker's program. The attacker's program returns success and the caller believes the work was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Account Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Closed accounts are emptied and marked so they cannot be revived or reused within the same transaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No instruction allows reinitializing an already-initialized account. &lt;code&gt;init&lt;/code&gt; fails on existing accounts. &lt;code&gt;init_if_needed&lt;/code&gt; succeeds silently. Use it deliberately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Closing an account returns the rent to the correct wallet. Never to an arbitrary signer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;close = authority&lt;/code&gt; is used, not manual lamport draining.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; An account that is closed but not marked can be reinitialized and reused. An account that is reinitialized loses its old state and can be repurposed by an attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;6. Pre-Deploy Hygiene&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cargo audit&lt;/code&gt; has been run and reports no known advisories in dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adversarial tests pass, not just the happy path. Every defense has a test that proves it works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Property tests pass. Rules like "a deposit never shrinks a balance" hold for random inputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fuzz tests pass. Trident has run the program through thousands of random instruction sequences without a panic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;anchor keys sync&lt;/code&gt; has been run. The program ID in &lt;code&gt;lib.rs&lt;/code&gt; matches &lt;code&gt;Anchor.toml&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;overflow-checks = true&lt;/code&gt; flag is set under &lt;code&gt;[profile.release]&lt;/code&gt; in the workspace root &lt;code&gt;Cargo.toml&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;grep -rn "UncheckedAccount\|AccountInfo\|/// CHECK" programs/*/src&lt;/code&gt; returns only accounts you have manually verified.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The best defense is a test that proves it works. A constraint you cannot see fail is a constraint you are only hoping is there. Run the checklist before every deploy, not just the first one.&lt;/p&gt;

&lt;p&gt;This checklist is a living document. Run it before every mainnet deploy, top to bottom. If you find something it misses, open an issue or reach out. Security is a community effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;This post draws from Days 75-82 of #100DaysOfSolana.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rust</category>
      <category>webdev</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>CPIs on Solana: From Confusion to Confidence</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Fri, 04 Sep 2026 16:29:14 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/cpis-on-solana-from-confusion-to-confidence-3p2g</link>
      <guid>https://dev.to/mubaraqabba/cpis-on-solana-from-confusion-to-confidence-3p2g</guid>
      <description>&lt;p&gt;On Day 71, I thought CPI was a magic black box. I knew it stood for Cross-Program Invocation, but I didn't understand why the runtime needed a program ID, accounts, and sometimes signer seeds. After working through this arc, I realized the pattern is always the same. The program ID says which program to call, the accounts say what data to read or write, and the signer seeds say who's authorized. Whether you're moving SOL through the System Program, minting tokens through Token-2022, or calling your own counter program, the CpiContext shape never changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The mental model&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A CPI is how one program calls another. Think of it like a function call, but with three explicit pieces.&lt;/p&gt;

&lt;p&gt;The program ID tells the runtime which program you're calling. This is like a function name in a library.&lt;/p&gt;

&lt;p&gt;The accounts are the data the callee needs. This is like passing arguments, but the arguments are accounts on-chain.&lt;/p&gt;

&lt;p&gt;The signer authority is who is authorizing this call. This is either a real wallet (the signature flows through) or a PDA (the program signs with seeds).&lt;/p&gt;

&lt;p&gt;The same CpiContext struct bundles all three. You build it once and pass it to the helper function. The runtime handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A real code snippet&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the smallest CPI I wrote this week. The caller program calls the counter program's &lt;em&gt;increment&lt;/em&gt; instruction.&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;bump&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;Bump&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;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.counter_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;Increment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tally&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.tally&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="nn"&gt;cpi&lt;/span&gt;&lt;span class="p"&gt;::&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;cpi_ctx&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;The first argument is the program ID. &lt;code&gt;counter_program.key()&lt;/code&gt; tells the runtime which program to call.&lt;/p&gt;

&lt;p&gt;The second argument is the accounts struct. &lt;code&gt;Increment { tally: ... }&lt;/code&gt; provides the account the counter program needs.&lt;/p&gt;

&lt;p&gt;The signer authority is implied here. The user signed the outer transaction, and that signature flows through to the CPI automatically.&lt;/p&gt;

&lt;p&gt;If the signer was a PDA, I would need &lt;code&gt;.with_signer(signer_seeds)&lt;/code&gt; to prove the program owns the PDA. But for a regular wallet, the signature just works.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cpi::increment&lt;/code&gt; helper does the actual invocation. If the counter program's increment fails, this function returns an error and the entire transaction rolls back.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What tripped me up&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The error that tripped me up most was &lt;code&gt;ConstraintSeeds&lt;/code&gt;. I changed one byte in the signer seeds — from &lt;code&gt;b"vault"&lt;/code&gt; to &lt;code&gt;b"voult"&lt;/code&gt; — and the transaction failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Program log: AnchorError caused by account: vault. Error Code: ConstraintSeeds.

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

&lt;/div&gt;



&lt;p&gt;The error meant the runtime re-derived the PDA address from my seeds and got a different address than the account I passed. The seeds must match exactly. One byte off and the runtime refuses to sign.&lt;/p&gt;

&lt;p&gt;The fix was simple: match the seeds in the CPI to the seeds in the &lt;code&gt;#[account(seeds = ...)]&lt;/code&gt; constraint. If they match, the runtime treats the PDA as a signer. If they don't, you get &lt;code&gt;ConstraintSeeds&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Closing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CPIs are how Solana programs compose. The pattern is always the same: program ID, accounts, signer. Whether you're calling the System Program, Token-2022, or your own code, the &lt;code&gt;CpiContext&lt;/code&gt; shape doesn't change.&lt;/p&gt;

&lt;p&gt;If you're stuck, check the three pieces. The program ID tells the runtime where to go. The accounts tell it what data to touch. The signer tells it who authorized the call. When all three line up, the CPI works.&lt;/p&gt;

&lt;p&gt;This post draws from Days 71-75 of #100DaysOfSolana.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Understanding Solana PDAs: A Web2 Developer's Guide</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sun, 09 Aug 2026 17:00:10 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/understanding-solana-pdas-a-web2-developers-guide-12b8</link>
      <guid>https://dev.to/mubaraqabba/understanding-solana-pdas-a-web2-developers-guide-12b8</guid>
      <description>&lt;p&gt;On Solana, programs are stateless. If your program needs to remember something per user, per game, or per configuration, it needs a deterministic address it can find again later without storing it anywhere. PDAs are that address.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What a PDA actually is&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In Web2, a database primary key is computed from a row's logical identity — user_id, order_id, something the system already knows. You never look up a row by a random UUID you can't derive. PDAs work the same way, except the database is the entire Solana account model, the key derivation is a hash, and the program ID is baked in so only your program can sign for it.&lt;/p&gt;

&lt;p&gt;Unlike a primary key, a PDA is not stored in a table. It's derived on demand and may or may not have an account at that address yet. The program doesn't know if the account exists until it tries to read it. That's why init and init_if_needed exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Anatomy of a derivation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the canonical pattern from my counter 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;The &lt;code&gt;seeds&lt;/code&gt; array is the core of PDA derivation. It contains two parts: a static seed prefix &lt;code&gt;b"counter"&lt;/code&gt; and a dynamic seed &lt;code&gt;user.key().as_ref()&lt;/code&gt;. The static seed creates a namespace — all counters share this prefix. The dynamic seed separates one user's counter from another's.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bump&lt;/code&gt; is the byte that makes the address off-curve. Anchor computes it during derivation and stores it on the account. The &lt;code&gt;init&lt;/code&gt; constraint tells Anchor to create the account at the derived address. &lt;code&gt;payer = user&lt;/code&gt; says the user pays the rent. &lt;code&gt;space&lt;/code&gt; reserves enough bytes for the account data.&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;seeds&lt;/code&gt; you choose determine who can access what. In my counter program, I used &lt;code&gt;[b"counter", user.key().as_ref()]&lt;/code&gt;. This gives every wallet its own PDA. If I had used only &lt;code&gt;[b"counter"]&lt;/code&gt;, every wallet would get the same PDA — which is useful for a global config but disastrous for a per-user counter.&lt;/p&gt;

&lt;p&gt;I tested this on Day 68. The per-user derivation gave different addresses for different wallets. The global derivation gave the same address for every wallet. Same program ID, same derivation function, different seeds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Per-user counter PDAs:
  Wallet A PDA: 3ZQj5KhoHaoDb9g1LoFpDZ5tAxMwA4ZumnUzrxmMPFfg
  Wallet B PDA: DgfkXgoFoHDRUGKiHvduDHrjuofRJKoFyJ13SqWqMgFs
  Same address? false

Global counter PDA (no wallet in seeds):
  Derived from A: 2ESHd2ZG3Yjh3kUZdSLGhweaVPfLaqs1qqqF4Hgh4Ndn
  Derived from B: 2ESHd2ZG3Yjh3kUZdSLGhweaVPfLaqs1qqqF4Hgh4Ndn
  Same address? true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;What the bump buys you&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The canonical bump is the first bump value that produces an off-curve address. &lt;code&gt;find_program_address&lt;/code&gt; returns it along with the PDA. Anchor stores it for you when you write &lt;code&gt;bump&lt;/code&gt; in the constraint.&lt;/p&gt;

&lt;p&gt;You should re-pass the stored bump on subsequent instructions rather than re-deriving every time. Re-derivation is expensive (it hashes the seeds with 256 possible bump values). Storing the bump is free — it's just one byte on the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The full lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Over the course of this arc, I walked through the entire lifecycle of a PDA account:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Derive the address — Use &lt;code&gt;find_program_address&lt;/code&gt; with seeds and program ID. No account exists yet, just the address.&lt;/li&gt;
&lt;li&gt;Initialize the account — Use &lt;code&gt;init&lt;/code&gt; in the accounts struct. This creates the account at the PDA, pays rent from the user, and stores the bump.&lt;/li&gt;
&lt;li&gt;Mutate the data — Call instructions like &lt;code&gt;increment&lt;/code&gt;. The seeds constraint re-derives the address and verifies it matches the passed account.&lt;/li&gt;
&lt;li&gt;Close the account — Use &lt;code&gt;close = user&lt;/code&gt; to drain lamports back to the user and mark the account as closed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Closing is not "delete from a table." It's zero the data, transfer the lamports, and mark it for garbage collection at the end of the transaction. The runtime removes it from the next slot's account state.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I would tell past me&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A few things I wish I had known on Day 64:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The program ID is part of the derivation. The same seeds in a different program produce a different address. This is why PDAs are program-specific.&lt;/li&gt;
&lt;li&gt;PDAs cannot sign transactions on their own. Only programs can sign on their behalf using the same seeds. The program signs for the PDA, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;init_if_needed&lt;/code&gt; is convenient and also a footgun. Use it deliberately when you want idempotent creation, not by default. If you use it everywhere, you lose the ability to know whether an account was just created or already existed.&lt;/li&gt;
&lt;li&gt;The bump is not optional. Without it, you can't re-derive the address. Store it on the account and pass it back on every instruction that uses the PDA.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;PDAs are not complicated. They are deterministic addresses derived from seeds, a program ID, and a bump. They replace keypair tracking and let your program own state without a private key. The pattern is always the same: derive, initialize, mutate, close.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, start here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://solana.com/docs/core/pda" rel="noopener noreferrer"&gt;Program Derived Addresses &lt;/a&gt; — the official Solana docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.anchor-lang.com/docs/pdas" rel="noopener noreferrer"&gt;PDAs in Anchor&lt;/a&gt; — Anchor's guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.rs/anchor-lang/latest/anchor_lang/" rel="noopener noreferrer"&gt;Anchor crate docs&lt;/a&gt; — the source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My code is on &lt;a href="https://github.com/Mubaraqq/mlh_100daysofsolana/tree/main" rel="noopener noreferrer"&gt;GitHub.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My First Anchor Program: A Counter with Tests That Actually Catch Bugs</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:33:46 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/my-first-anchor-program-a-counter-with-tests-that-actually-catch-bugs-10jl</link>
      <guid>https://dev.to/mubaraqabba/my-first-anchor-program-a-counter-with-tests-that-actually-catch-bugs-10jl</guid>
      <description>&lt;p&gt;I spent 3 days building a counter program on Solana. It does one thing — count. But the real value wasn't the program. It was the tests that proved it worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The accounts struct:&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;#[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;Initialize&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(&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;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="nd"&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;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;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;This struct defines what accounts the initialize instruction needs. The &lt;code&gt;counter&lt;/code&gt; account is being created (&lt;code&gt;init&lt;/code&gt;), the &lt;code&gt;authority&lt;/code&gt; pays for it (&lt;code&gt;payer&lt;/code&gt;) and becomes its owner, and the &lt;code&gt;system_program&lt;/code&gt; is required because creating accounts is a System Program operation. The &lt;code&gt;8 + Counter::INIT_SPACE&lt;/code&gt; is 8 bytes for Anchor's discriminator plus the size of the Counter data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The initialize handler:&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="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;The handler is short. &lt;code&gt;ctx.accounts&lt;/code&gt; gives you typed access to the accounts passed in. You set the authority to the wallet that signed the transaction, set count to 0 and return success. Anchor handles the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The increment handler:&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="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;The increment handler adds 1 to the count. The &lt;code&gt;checked_add&lt;/code&gt; prevents overflow — if the count hits u64 max, the transaction fails cleanly instead of panicking. The constraint in the accounts struct guarantees only the authority can call it.&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;The &lt;code&gt;has_one = authority&lt;/code&gt; constraint is where the security lives. Before the handler runs, Anchor checks that &lt;code&gt;counter.authority&lt;/code&gt; matches the &lt;code&gt;authority&lt;/code&gt; signing the transaction. If it doesn't, the transaction fails before your code executes. Declarative authorization — checked at the protocol level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The happy-path test:&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;program_id&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="n"&gt;ID&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;so_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;concat!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;env!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CARGO_MANIFEST_DIR"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"/../../target/deploy/counter.so"&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;.add_program_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;so_path&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;authority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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;svm&lt;/span&gt;&lt;span class="nf"&gt;.airdrop&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;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&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;counter_kp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;init_ix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instruction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accounts&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="nn"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt; &lt;span class="p"&gt;{&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;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="n"&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="n"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;.to_account_metas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;data&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="nn"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Initialize&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="nf"&gt;.data&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;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_signed_with_payer&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;init_ix&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;Some&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;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&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="n"&gt;authority&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="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="nf"&gt;.latest_blockhash&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;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;inc_ix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Instruction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accounts&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="nn"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Increment&lt;/span&gt; &lt;span class="p"&gt;{&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;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="n"&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;span class="nf"&gt;.to_account_metas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;data&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="nn"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Increment&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="nf"&gt;.data&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;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_signed_with_payer&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;inc_ix&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;Some&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;authority&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&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="n"&gt;authority&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;.latest_blockhash&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;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 program works: initialize creates a counter with count 0, increment changes it to 1, and the final state is exactly what we expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure test:&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="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;svm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setup_svm_with_program&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;authority_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;authority_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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;svm&lt;/span&gt;&lt;span class="nf"&gt;.airdrop&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;authority_a&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&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;.airdrop&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;authority_b&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&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;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Keypair&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;init_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_initialize_tx&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;svm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_id&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;authority_a&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&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;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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bad_tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_increment_tx&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;svm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;program_id&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;authority_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="nf"&gt;.pubkey&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;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 proves the authorization works: authority_a creates the counter, authority_b tries to increment it, and the transaction fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking it on purpose:&lt;/strong&gt;&lt;br&gt;
I planted a bug on purpose. I changed &lt;code&gt;checked_add(1)&lt;/code&gt; to &lt;code&gt;checked_add(2)&lt;/code&gt; in the increment handler. The happy-path test failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assertion `left == right` failed
  left: 2
 right: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the test catching a real regression. Without that assertion, the program would silently store the wrong number and the suite would still be green. That's why the tests matter.&lt;/p&gt;

&lt;p&gt;Next, I'd add more instructions — decrement, reset, maybe a way to transfer ownership. The pattern is the same: define the instruction, define the accounts, write the test, break it, fix it. The counter is just the first step.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned Minting NFTs on Solana with Token Extensions</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Fri, 03 Jul 2026 02:04:17 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/what-i-learned-minting-nfts-on-solana-with-token-extensions-3b4k</link>
      <guid>https://dev.to/mubaraqabba/what-i-learned-minting-nfts-on-solana-with-token-extensions-3b4k</guid>
      <description>&lt;p&gt;An NFT on Solana is just a token with supply 1, decimals 0 and metadata. Everything else is optional.&lt;/p&gt;

&lt;p&gt;Before this week, I thought Solana NFTs required Metaplex. Turns out you can mint one with just the Token Extensions program, a GitHub Gist and a few CLI commands.&lt;/p&gt;

&lt;p&gt;On Solana, an NFT is not a special type. It's a token mint with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decimals: 0 (can't be split)&lt;/li&gt;
&lt;li&gt;Supply: 1 (only one exists)&lt;/li&gt;
&lt;li&gt;Metadata extension (name, symbol, image URI)&lt;/li&gt;
&lt;li&gt;Mint authority disabled (can't mint more)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. The rest is metadata, collections, and off-chain JSON.&lt;/p&gt;

&lt;p&gt;Everything else you see in wallets and marketplaces—images, attributes, collection pages—is built on top of this foundation.&lt;/p&gt;

&lt;p&gt;Over three days, I minted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A standalone NFT with metadata (name, symbol, image, attributes)&lt;/li&gt;
&lt;li&gt;A collection mint with the Group extension (max size 3)&lt;/li&gt;
&lt;li&gt;Two member NFTs linked to the collection via the Member extension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All on devnet. All with the Token-2022 program. No separate metadata program needed.&lt;/p&gt;

&lt;p&gt;Here's how the collection + member flow works:&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 collection mint with group extension&lt;/span&gt;
spl-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-group&lt;/span&gt;

&lt;span class="c"&gt;# Initialize group with max size 3&lt;/span&gt;
spl-token initialize-group &amp;lt;COLLECTION_MINT&amp;gt; 3

&lt;span class="c"&gt;# Create member mint with member extension&lt;/span&gt;
spl-token &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-member&lt;/span&gt;

&lt;span class="c"&gt;# Link member to collection&lt;/span&gt;
spl-token initialize-member &amp;lt;MEMBER_MINT&amp;gt; &amp;lt;COLLECTION_MINT&amp;gt;

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

&lt;/div&gt;


&lt;p&gt;Metadata is mutable by default. The update authority can change name, symbol, URI and custom fields at any time.&lt;/p&gt;

&lt;p&gt;This is useful for fixing errors. But it also means you trust the update authority not to rug you.&lt;/p&gt;

&lt;p&gt;I mutated my NFT live on devnet:&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;# Update name&lt;/span&gt;
spl-token update-metadata &amp;lt;MINT&amp;gt; name &lt;span class="s2"&gt;"Field Notes"&lt;/span&gt;

&lt;span class="c"&gt;# Add custom field&lt;/span&gt;
spl-token update-metadata &amp;lt;MINT&amp;gt; rarity legendary

&lt;span class="c"&gt;# Remove custom field&lt;/span&gt;
spl-token update-metadata &amp;lt;MINT&amp;gt; rarity &lt;span class="nt"&gt;--remove&lt;/span&gt;

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

&lt;/div&gt;


&lt;p&gt;On-chain updates are instant. Off-chain image caching is not.&lt;/p&gt;

&lt;p&gt;The name updated immediately in Solana Explorer. The new image may took longer to appear in Phantom. That asymmetry is something you have to design for.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;What I'd build next&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A complete NFT drop with a fixed collection size, automated minting, and on-chain royalties.&lt;/p&gt;

&lt;p&gt;Token Extensions support transfer fees. Royalties can be enforced at the protocol level without a separate marketplace integration. No middleware. No trust. Just math.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;The takeaway&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Solana NFTs are not complicated. They're just tokens with a few extensions enabled.&lt;/p&gt;

&lt;p&gt;Metaplex is one way to build them. Token Extensions is another. Both work. One is lighter, more composable, and lives entirely inside the mint account.&lt;/p&gt;

&lt;p&gt;If you're coming from Web2, this might feel unfamiliar. But the mental shift is worth it.&lt;/p&gt;

&lt;p&gt;Try it yourself. Mint one on devnet. Change its name. Add a custom field. Send it to a friend. The tools are all there.&lt;/p&gt;

&lt;p&gt;This post is part of #100DaysOfSolana. Follow along or jump in any day.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://solana.com/solutions/token-extensions" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;solana.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Official Solana documentation on token extensions and their use cases&lt;br&gt;&lt;/p&gt;

&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
&lt;div class="c-embed__content"&gt;
  &lt;div class="c-embed__body flex items-center justify-between"&gt;
    &lt;a href="https://solana.com/docs/tokens/extensions/metadata" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
      &lt;span class="mr-2"&gt;solana.com&lt;/span&gt;
      

    &lt;/a&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Technical reference for storing NFT metadata directly on mint accounts &lt;br&gt;&lt;/p&gt;

&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
&lt;div class="c-embed__content"&gt;
  &lt;div class="c-embed__body flex items-center justify-between"&gt;
    &lt;a href="https://solana.com/vi/docs/tokens/extensions/dynamic-metadata-nft" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
      &lt;span class="mr-2"&gt;solana.com&lt;/span&gt;
      

    &lt;/a&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Official guide for building dynamic NFTs with on-chain metadata &lt;br&gt;&lt;/p&gt;

&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
&lt;div class="c-embed__content"&gt;
    &lt;div class="c-embed__cover"&gt;
      &lt;a href="https://www.metaplex.com/docs/solana/spl-tokens-and-token-programs" class="c-link align-middle" rel="noopener noreferrer"&gt;
        &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.metaplex.com%2Fdocs%2Fapi%2Fog%3Ftitle%3DSPL%2BTokens%2Band%2BToken%2BPrograms%26description%3DLearn%2Bhow%2BSPL%2Btokens%2Bwork%2Bon%2BSolana%252C%2Bincluding%2Bthe%2BToken%2BProgram%252C%2Bmint%2Baccounts%252C%2Btoken%2Baccounts%252C%2Band%2Bhow%2BMetaplex%2Bbuilds%2Bon%2Btop%2Bof%2Bthe%2Btoken%2Bstandard.%26product%3Dsolana" height="630" class="m-0" width="1200"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;div class="c-embed__body"&gt;
    &lt;h2 class="fs-xl lh-tight"&gt;
      &lt;a href="https://www.metaplex.com/docs/solana/spl-tokens-and-token-programs" rel="noopener noreferrer" class="c-link"&gt;
        SPL Tokens and Token Programs | Understanding Solana Tokens
      &lt;/a&gt;
    &lt;/h2&gt;
      &lt;p class="truncate-at-3"&gt;
        Learn how SPL tokens work on Solana, including the Token Program, mint accounts, token accounts, and how Metaplex builds on top of the token standard.
      &lt;/p&gt;
    &lt;div class="color-secondary fs-s flex items-center"&gt;
        &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.metaplex.com%2Fdocs%2Ffavicon.png" width="256" height="256"&gt;
      metaplex.com
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comparison of original SPL Token, Token-2022, and Metaplex standards &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>solana</category>
      <category>nft</category>
      <category>web3dev</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>You Already Understand Token Extensions.</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 22 Jun 2026 17:29:04 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/you-already-understand-token-extensions-1h6m</link>
      <guid>https://dev.to/mubaraqabba/you-already-understand-token-extensions-1h6m</guid>
      <description>&lt;p&gt;Because you've built payment flows, loyalty systems and credentials in Web2.&lt;/p&gt;

&lt;p&gt;In Web2, charging a 1% transaction fee means building middleware. Verifying a user's identity before they can transact means writing KYC logic. Issuing a credential that can't be traded means designing a database with access controls.&lt;/p&gt;

&lt;p&gt;On Solana, all of this is handled at the protocol level by Token Extensions.&lt;/p&gt;

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

&lt;p&gt;Token Extensions are optional features you enable when creating a token mint. They're not separate smart contracts. They live in the same account as your token. Once enabled, the runtime enforces them.&lt;/p&gt;

&lt;p&gt;There are two Token Programs on Solana:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original Token Program (Tokenkeg...): Basic mint, transfer, burn&lt;/li&gt;
&lt;li&gt;Token-2022 Program (TokenzQdBN...): Same basics, plus extensions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Extensions I used:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transfer fee&lt;/strong&gt;: Withholds a percentage on every transfer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt;: Stores name, symbol, URI on-chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default frozen&lt;/strong&gt;: Every account starts locked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-transferable&lt;/strong&gt;: Tokens cannot be moved (soulbound)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent delegate&lt;/strong&gt;: Issuer can burn tokens from any wallet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interest-bearing&lt;/strong&gt;: Display balance grows over time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;A real example: Multi-extension token&lt;/strong&gt;&lt;br&gt;
One command, three extensions:&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 &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 500 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interest-rate&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a token with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1% transfer fee (capped at 500 units)&lt;/li&gt;
&lt;li&gt;Interest-bearing display balance&lt;/li&gt;
&lt;li&gt;On-chain metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then add metadata so wallets can display your token properly:&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 initialize-metadata &amp;lt;MINT&amp;gt; &lt;span class="s2"&gt;"ArcCoin"&lt;/span&gt; &lt;span class="s2"&gt;"ARC"&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/metadata.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extensions cannot be added after mint creation. You have to declare everything upfront. In Web2, you'd add a feature later. On Solana, the account size is fixed at creation. No space for new extensions. Plan before you mint.&lt;/p&gt;

&lt;p&gt;The word "permanent" in permanent delegate. I assumed it meant the address could never change. Tried to change it. It worked. "Permanent" means the authority to burn from any wallet, not an immutable address. Took me a failed assumption to learn that.&lt;/p&gt;

&lt;p&gt;Token Extensions aren't separate smart contracts. They live in the mint account. When you create the mint, you're allocating space for extensions. The runtime handles the rest. No middleware, no backend, no bypass.&lt;/p&gt;

&lt;p&gt;Token Extensions are how Solana handles real-world financial logic at the protocol level. Transfer fees, compliance gates, credentials, yield-bearing assets — all built in. You don't write the logic. You just enable the extension.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, start here:&lt;br&gt;
Token Extensions Official Docs&lt;/p&gt;

&lt;p&gt;Or just create a token on devnet and try one. That's what I did.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>solana</category>
      <category>webdev</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Transfer Fees, Metadata, and Soulbound Tokens: A Tour of Solana Token Extensions</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Wed, 17 Jun 2026 17:05:47 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/transfer-fees-metadata-and-soulbound-tokens-a-tour-of-solana-token-extensions-29b6</link>
      <guid>https://dev.to/mubaraqabba/transfer-fees-metadata-and-soulbound-tokens-a-tour-of-solana-token-extensions-29b6</guid>
      <description>&lt;p&gt;In Web2, adding a transfer fee means building middleware. On Solana, it's a flag. I built four tokens in six days. Here's what I learned.&lt;/p&gt;

&lt;p&gt;A few weeks ago, I knew nothing about Solana tokens. I'd used Solana for transfers and read account data, but tokens felt like a black box. I wanted to understand how they actually work, not just copy-paste commands but know what each flag does and why it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Basic Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My first token was barebones. No name. No symbol. Just an address.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-token&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
That's it. One command. A mint account appeared on devnet with a 9-decimal default.&lt;/p&gt;

&lt;p&gt;I created a token account to hold it, then minted 100 tokens to myself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token create-account &amp;lt;MINT&amp;gt;&lt;br&gt;
spl-token mint &amp;lt;MINT&amp;gt; 100&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What surprised me: You can't receive tokens directly into your wallet. You need a token account for each token type. One per token, per wallet.&lt;/p&gt;

&lt;p&gt;The mint is the factory. The token account is your bucket.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Giving It an Identity&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A token without metadata is just an address. Phantom would show a random string. No one would know what it is.&lt;/p&gt;

&lt;p&gt;I needed a name, symbol and URI.&lt;/p&gt;

&lt;p&gt;But the original Token Program (Tokenkeg...) doesn't support metadata on the mint itself. You'd need a separate account.&lt;/p&gt;

&lt;p&gt;So I used Token Extensions Program (Token-2022) at TokenzQdBN.... It stores metadata directly on the mint.&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-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 6

spl-token initialize-metadata &amp;lt;MINT&amp;gt; &lt;span class="s2"&gt;"100daysofsol"&lt;/span&gt; &lt;span class="s2"&gt;"100DSOL"&lt;/span&gt; &amp;lt;URI&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One mint. Multiple extensions possible. No separate accounts.&lt;/p&gt;

&lt;p&gt;The URI points to a JSON file with description and image. Wallet UIs read it automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Adding Transfer Fees&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In Web2, charging a fee on every transaction means building middleware. Handling edge cases. Making sure no one bypasses it.&lt;/p&gt;

&lt;p&gt;On Solana, it's a flag.&lt;/p&gt;

&lt;p&gt;I used Token-2022 again, this time with --transfer-fee-basis-points.&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-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-basis-points&lt;/span&gt; 200 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 5000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metadata&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100 basis points = 1%. I set 200 (2%) with a cap of 5000 displayed tokens.&lt;/p&gt;

&lt;p&gt;Then I minted 1000, transferred 100 to a second wallet, and watched the fee get withheld.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token transfer &amp;lt;MINT&amp;gt; 100 &amp;lt;RECIPIENT&amp;gt; --expected-fee 2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Recipient got 98. 2 tokens withheld in their token account. They couldn't touch it.&lt;/p&gt;

&lt;p&gt;Only the withdrawal authority (me) could collect it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token withdraw-withheld-tokens &amp;lt;MY_ACCOUNT&amp;gt; &amp;lt;RECIPIENT_ACCOUNT&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I swept the fee back. Balance went from 900 to 902.&lt;/p&gt;

&lt;p&gt;The fee logic lives in the mint. Every transfer respects it. No application code required.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Soulbound Tokens&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Not all tokens should be transferable.&lt;/p&gt;

&lt;p&gt;A course completion certificate. A KYC verification. An employee badge. They're credentials, not currency.&lt;/p&gt;

&lt;p&gt;Token-2022 has an extension for this: --enable-non-transferable.&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-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb &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;I minted 10 tokens to myself. Then tried to transfer 5 to a second wallet.&lt;/p&gt;

&lt;p&gt;It failed.&lt;br&gt;
&lt;em&gt;Program log: Transfer is disabled for this mint&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The blockchain itself rejected the transaction. No client-side check. No middleware. Protocol-level enforcement.&lt;/p&gt;

&lt;p&gt;But I could still burn them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spl-token burn &amp;lt;ACCOUNT&amp;gt; 3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Balance went from 10 to 7. The holder can destroy tokens but cannot send them away.&lt;/p&gt;

&lt;p&gt;This is how credentials should work on-chain. They prove something about the wallet that holds them. Trading them defeats the purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Surprised Me&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;What surprised me most: the protocol doesn't care about intent. It just enforces rules. When I tried to transfer a non-transferable token, the program rejected it without explanation beyond "Transfer is disabled." No appeal. No admin override. The rules are the rules.&lt;/p&gt;

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

&lt;p&gt;I've built tokens. Now I'm moving to programs.&lt;/p&gt;

&lt;p&gt;I want to build something that does more than transfer value—something that enforces custom logic on-chain. A game. A marketplace. A DAO.&lt;/p&gt;

&lt;p&gt;Follow along. I'm documenting everything.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>ai</category>
    </item>
    <item>
      <title>Solana Accounts for Web2 Developers (You Already Understand Files)</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:41:13 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/solana-accounts-for-web2-developers-you-already-understand-files-k90</link>
      <guid>https://dev.to/mubaraqabba/solana-accounts-for-web2-developers-you-already-understand-files-k90</guid>
      <description>&lt;p&gt;I spent the past month building on Solana. Sent transfers. Decoded raw bytes. Inspected accounts until my terminal turned into a wall of hex.&lt;/p&gt;

&lt;p&gt;Along the way, I had to unlearn some Web2 habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;One model. No exceptions.&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Your wallet is an account. The program that moves your SOL is an account. The token you just bought? Also an account.&lt;/p&gt;

&lt;p&gt;Solana doesn't have special types. Every account has the same five fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lamports (SOL balance, 1 SOL = 1 billion lamports)&lt;/li&gt;
&lt;li&gt;Data (raw bytes. empty for wallets)&lt;/li&gt;
&lt;li&gt;Owner (the program that can modify this account)&lt;/li&gt;
&lt;li&gt;Executable (true if this account holds code)&lt;/li&gt;
&lt;li&gt;Rent epoch (ignore this. it's deprecated)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run solana account on any address. Same structure every time.&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="nv"&gt;$ &lt;/span&gt;solana account 8CtdyqtzBd597eDz9PTZHuuT62vvLc6YXdXjVkHnboqj

Public Key: 8CtdyqtzBd597eDz9PTZHuuT62vvLc6YXdXjVkHnboqj
Balance: 3.93896 SOL
Owner: 11111111111111111111111111111111
Executable: &lt;span class="nb"&gt;false
&lt;/span&gt;Rent Epoch: 18446744073709551615

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Who owns what matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The owner field determines control.&lt;/p&gt;

&lt;p&gt;My wallet is owned by the System Program &lt;em&gt;(111...111)&lt;/em&gt;. Only the System Program can deduct SOL from it. I sign. The program executes.&lt;/p&gt;

&lt;p&gt;A token account is owned by the Token Program. Only that program can move tokens.&lt;/p&gt;

&lt;p&gt;In Web2, your app authorizes changes. On Solana, the program that owns the account authorizes changes. Your signature proves you approved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Programs have no memory&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This was the hardest shift.&lt;/p&gt;

&lt;p&gt;In Node.js, my app holds variables in memory. App and state live together.&lt;/p&gt;

&lt;p&gt;On Solana, a program account holds code. Nothing else. No variables. No state.&lt;/p&gt;

&lt;p&gt;State lives in separate data accounts.&lt;/p&gt;

&lt;p&gt;A program reads from its data accounts, does math, writes back. It never stores anything itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In Web2, you store state in variables&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// On Solana, you read from a data account, modify, write back&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counterAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataAccount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counterAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Why? Update the program without losing data. One program, many data accounts. Programs stay pure.&lt;/p&gt;

&lt;p&gt;The Token Program is 36 bytes of code. The mint accounts it controls? Separate accounts. Owned by the Token Program. Code in one place. State in another.&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="nv"&gt;$ &lt;/span&gt;solana account TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

Public Key: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Owner: BPFLoaderUpgradeab1e11111111111111111111111
Executable: &lt;span class="nb"&gt;true
&lt;/span&gt;Length: 36 bytes

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Rent isn't rent&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every account needs a minimum SOL balance based on data size. For a basic wallet with zero data: &lt;em&gt;~0.00089 SOL&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Pay once. Store forever. Close the account, get your SOL back.&lt;/p&gt;

&lt;p&gt;It's a deposit. Not monthly rent.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;You already know this&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A file has a name, contents, and permissions.&lt;/p&gt;

&lt;p&gt;A Solana account has an address, data, and an owner.&lt;/p&gt;

&lt;p&gt;Same pattern. Different environment.&lt;/p&gt;

&lt;p&gt;Took me a few weeks to stop overcomplicating it.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Hit the 1,232-Byte Wall So You Don't Have To</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Mon, 25 May 2026 04:56:21 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/i-hit-the-1232-byte-wall-so-you-dont-have-to-10</link>
      <guid>https://dev.to/mubaraqabba/i-hit-the-1232-byte-wall-so-you-dont-have-to-10</guid>
      <description>&lt;p&gt;I was building a batch transfer tool. Send SOL to 20 recipients in one transaction. Simple, right?&lt;/p&gt;

&lt;p&gt;Wrong.&lt;/p&gt;

&lt;p&gt;The RPC rejected my transaction with: Transaction too large: 1400 &amp;gt; 1232&lt;/p&gt;

&lt;p&gt;I had no idea what 1232 meant. Now I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Limit&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every Solana transaction — transfer, swap, mint, everything — has a hard cap: 1,232 bytes.&lt;/p&gt;

&lt;p&gt;Not 1,232 lines of code. 1,232 bytes&lt;/p&gt;

&lt;p&gt;Where does 1,232 come from? IPv6 packet size. The internet requires every packet to be at least 1,280 bytes. Subtract 48 bytes for network headers. Leftover: 1,232 bytes.&lt;/p&gt;

&lt;p&gt;Solana didn't choose this number. The internet did.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why I Hit the Wall&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My batch transfer had 22 accounts (sender + 20 recipients + system program). Each account key takes 32 bytes. 22 × 32 = 704 bytes just for the addresses.&lt;/p&gt;

&lt;p&gt;Add signature (64 bytes), header (3 bytes), blockhash (32 bytes), instructions (variable). Total: ~1,148 bytes.&lt;/p&gt;

&lt;p&gt;Dangerously close to 1,232. Add one more recipient? Fail. Add a memo? Fail. Add any complexity? Fail.&lt;/p&gt;

&lt;p&gt;I was 84 bytes from the edge. That's one short sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Escape&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I learned about Address Lookup Tables (ALTs).&lt;/p&gt;

&lt;p&gt;An ALT is an on-chain account that stores addresses. Once stored, a transaction references them by a 1-byte index instead of the full 32-byte address.&lt;/p&gt;

&lt;p&gt;Think of it like a URL shortener. You store the long URL once. Everyone else uses the short code.&lt;/p&gt;

&lt;p&gt;Same concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Changed&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My batch transfer went from 22 full addresses to 1 ALT address + 21 one-byte indexes.&lt;/p&gt;

&lt;p&gt;Before: 704 bytes for addresses&lt;br&gt;
After: 32 bytes (ALT) + 21 bytes (indexes) = 53 bytes&lt;/p&gt;

&lt;p&gt;My transaction size dropped from ~1,148 bytes to ~562 bytes.&lt;/p&gt;

&lt;p&gt;Suddenly I had room. Add more recipients. Add a memo. Add error handling. All fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The 1,232-byte limit isn't arbitrary. It's Solana prioritizing network speed over developer convenience.&lt;/p&gt;

&lt;p&gt;Fragmented packets are slower. Solana said no fragmentation. One packet. One transaction. Done.&lt;/p&gt;

&lt;p&gt;This forces constraints:&lt;/p&gt;

&lt;p&gt;List every account upfront&lt;/p&gt;

&lt;p&gt;Batch operations need ALTs&lt;/p&gt;

&lt;p&gt;Complex DeFi requires careful design&lt;/p&gt;

&lt;p&gt;But the network stays fast. 4,000+ transactions per second fast.&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%2Fzxladeqg3c40dvh10p2w.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%2Fzxladeqg3c40dvh10p2w.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>webdev</category>
      <category>solanadev</category>
      <category>mlh</category>
    </item>
    <item>
      <title>Two Weeks of Solana: Familiar but Different</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sun, 17 May 2026 06:35:08 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/two-weeks-of-solana-familiar-but-different-163m</link>
      <guid>https://dev.to/mubaraqabba/two-weeks-of-solana-familiar-but-different-163m</guid>
      <description>&lt;p&gt;I came into this with small Web3 experience. Most of the early lessons felt normal, familiar. But the "everything is an account" model on Solana took extra attention. It's not like a traditional database at all.&lt;/p&gt;

&lt;p&gt;What surprised me: the publicity and decentralized aspects. No API keys. No permission to read. Just an RPC endpoint and an address. Compared to traditional APIs, it's easier and faster. No restrictions. No rate limits. Just query and get data.&lt;/p&gt;

&lt;p&gt;What's next? Nothing specific I'm unsure about yet. I want to start building a real project on Solana using my Web2 expertise. Two weeks in. Ready for the next phase.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>beginners</category>
      <category>blacklivesmatter</category>
    </item>
    <item>
      <title>Solana Identity for Web2 Developers: You Already Understand Keypairs.</title>
      <dc:creator>Mubarak Yakubu</dc:creator>
      <pubDate>Sun, 10 May 2026 00:10:42 +0000</pubDate>
      <link>https://dev.to/mubaraqabba/solana-identity-for-web2-developers-you-already-understand-keypairs-20i</link>
      <guid>https://dev.to/mubaraqabba/solana-identity-for-web2-developers-you-already-understand-keypairs-20i</guid>
      <description>&lt;p&gt;If you've ever generated an SSH key pair, you already understand Solana identity.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ssh-keygen -t ed25519&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That command gives you two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;id_ed25519.pub (public key) → put this on servers&lt;/li&gt;
&lt;li&gt;id_ed25519 (private key) → stays on your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you SSH into a server, you prove your identity by signing a challenge with your private key. The server verifies the signature using your public key. Your private key never leaves your machine.&lt;/p&gt;

&lt;p&gt;Solana works exactly the same way. But instead of one server, the entire network verifies your signature. Instead of just SSH access, your keypair gives you ownership over tokens, programs, and data across every app on Solana.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a Solana address actually is&lt;/strong&gt;&lt;br&gt;
A Solana address is a 32-byte Ed25519 public key, encoded in Base58. Example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Compare that to a Web2 username stored in a database. A company granted you that username. They can revoke it, change it, or lock you out. Your Solana address needs no permission from anyone. It exists because the math says it exists.&lt;/p&gt;

&lt;p&gt;On Solana, the only person who can sign transactions for an address is the holder of the private key. No company. No admin panel. No "forgot password" flow.&lt;/p&gt;

&lt;p&gt;This is liberating and terrifying. Liberating because no one can take your assets. Terrifying because if you lose your private key, no one can give it back.&lt;/p&gt;

&lt;p&gt;That's why wallets exist: to manage private keys securely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What on-chain identity enables&lt;/strong&gt;&lt;br&gt;
A keypair isn't just for logging in. Everything you do on Solana ties back to your address:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token ownership → your address holds NFTs or fungible tokens&lt;/li&gt;
&lt;li&gt;Program interactions → you call smart contracts&lt;/li&gt;
&lt;li&gt;Governance → your tokens vote on DAO proposals&lt;/li&gt;
&lt;li&gt;Reputation → other addresses see your on-chain history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because it's cryptographic and self-custodied, it works across every application on the network without asking permission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mental shift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Web2 identity is borrowed. Solana identity is owned.&lt;/p&gt;

&lt;p&gt;When you "Sign in with Google," you're asking Google to vouch for you. When you sign a transaction with your Solana wallet, you prove your identity directly to the network.&lt;/p&gt;

&lt;p&gt;This is the shift that unlocks everything else in Web3. No intermediaries. Just math.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
