<?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: Russell Oje</title>
    <description>The latest articles on DEV Community by Russell Oje (@russell_oje).</description>
    <link>https://dev.to/russell_oje</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%2F3846695%2F5f9b12d1-01cf-4a2a-ab89-b52838094707.jpg</url>
      <title>DEV Community: Russell Oje</title>
      <link>https://dev.to/russell_oje</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/russell_oje"/>
    <language>en</language>
    <item>
      <title>The Missing Operations Manual for An On-Chain Autonomous Agent</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Fri, 24 Jul 2026 21:43:23 +0000</pubDate>
      <link>https://dev.to/russell_oje/the-missing-operations-manual-for-our-on-chain-autonomous-agent-31ia</link>
      <guid>https://dev.to/russell_oje/the-missing-operations-manual-for-our-on-chain-autonomous-agent-31ia</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Over this concluded week, we built an autonomous Solana devnet agent that checks wallet balances, executes transfers, and works within strict, code-level safety boundaries. Below is the comprehensive runbook and architectural breakdown of the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Architectural Lessons
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model Adaptation:&lt;/strong&gt; The model responds dynamically to tool outputs. When a transaction is blocked by a policy rejection, the LLM adapts by splitting the amount into smaller, valid chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn Limits are Essential:&lt;/strong&gt; A runaway loop could result in high API usage costs. Enforcing a turn limit ensures the agent exits if it gets stuck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety Lives Outside the Prompt:&lt;/strong&gt; Prompt engineering alone cannot guarantee safe behavior. Every transaction must pass through the policy engine before reaching the blockchain, regardless of what the model decides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability Builds Trust:&lt;/strong&gt; Detailed logs made it easy to verify every reasoning step, every tool call, and every policy decision. Without them, debugging autonomous behavior would have been significantly harder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomy is More Than Automation:&lt;/strong&gt; The agent doesn't execute a fixed script. Instead, it observes its environment, reasons over tool outputs, adjusts its plan when necessary, and stops when the goal becomes impossible or is successfully completed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  End-to-End Request Flow
&lt;/h3&gt;

&lt;p&gt;The following diagram shows how a plain-English request travels through the stack until it becomes a confirmed Solana devnet transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Natural Language Goal
        │
        ▼
+---------------------------+
|      OpenAI Agent Loop    |
|  Decides next tool call   |
+---------------------------+
        │
        ▼
+---------------------------+
|       MCP Server          |
| Exposes available tools   |
+---------------------------+
        │
        ▼
+---------------------------+
|      Read/Write Tools     |
| get_balance / transfer    |
+---------------------------+
        │
        ▼
+---------------------------+
|      Policy Engine        |
| Deny by default           |
+---------------------------+
      │              │
Allowed│              │Denied
      ▼              ▼
+---------------------------+
|     Solana Devnet         |
| Transaction confirmed     |
+---------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent itself never communicates directly with Solana. Every write request must pass through the policy layer first, making the policy engine the single gatekeeper for all fund movements.&lt;/p&gt;

&lt;h3&gt;
  
  
  System Components
&lt;/h3&gt;

&lt;p&gt;Our agentic stack consists of five modular parts working in unison:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agent Loop (&lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/blob/main/week_14/day_96/agent-workflow.mjs" rel="noopener noreferrer"&gt;agent-workflow.mjs&lt;/a&gt;):&lt;/strong&gt; The core reasoning engine powered by OpenAI that takes a natural language goal, decides on tool calls, and runs them up to a turn limit (&lt;code&gt;MAX_TURNS = 12&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read/Write Tools:&lt;/strong&gt; Thin Javascript wrappers around Solana Web3 RPC calls that expose &lt;code&gt;get_balance&lt;/code&gt; and &lt;code&gt;transfer_sol&lt;/code&gt; to the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Server (&lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/blob/main/week_14/day_94/server.ts" rel="noopener noreferrer"&gt;server.ts&lt;/a&gt;):&lt;/strong&gt; Connects our tools to external clients (like VS Code Copilot via &lt;code&gt;.vscode/mcp.json&lt;/code&gt;) using standard stdin/stdout protocols.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Engine (&lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/blob/main/week_14/day_95/policy.mjs" rel="noopener noreferrer"&gt;policy.mjs&lt;/a&gt;):&lt;/strong&gt; Enforces deny-by-default rules, checking every proposed spend against a recipient allowlist and budget caps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-Chain Solana Devnet:&lt;/strong&gt; The settlement layer where transaction signatures are verified and recorded.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tool Reference
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Tool: &lt;code&gt;get_balance&lt;/code&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; &lt;code&gt;address&lt;/code&gt; (string, base58 Solana address)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns:&lt;/strong&gt; Stringified JSON containing the account address and balance in lamports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side Effects:&lt;/strong&gt; None (read-only).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Tool: &lt;code&gt;transfer_sol&lt;/code&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; &lt;code&gt;to&lt;/code&gt; (string, base58 recipient address), &lt;code&gt;lamports&lt;/code&gt; (number, amount to transfer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns:&lt;/strong&gt; JSON object containing transaction status (&lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;denied&lt;/code&gt;, or &lt;code&gt;failed&lt;/code&gt;), transaction signature, or failure reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side Effects:&lt;/strong&gt; Debits the agent's wallet and credits the recipient wallet on-chain if approved.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Log Analysis of a Real Run
&lt;/h3&gt;

&lt;p&gt;Here is the log analysis from our Day 96 trial runs, showcasing how the agent behaves under normal, restricted, and impossible scenarios.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario A: Normal Goal-Seeking Run (0.2 SOL Target)
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Initial savings wallet holds 896,000 lamports. Target is 200,000,000 lamports (0.2 SOL).&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn 1:&lt;/strong&gt; Model calls &lt;code&gt;get_balance&lt;/code&gt; for both operating and savings wallets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 2:&lt;/strong&gt; Calculates the difference (&lt;code&gt;199,104,000&lt;/code&gt; lamports) and calls &lt;code&gt;transfer_sol&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Check:&lt;/strong&gt; Approved (under single-transfer cap of 0.25 SOL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 3:&lt;/strong&gt; Model calls &lt;code&gt;get_balance&lt;/code&gt; again, verifies the savings wallet holds exactly &lt;code&gt;200,000,000&lt;/code&gt; lamports, and outputs the final report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1roeq3ygypc3t7ew7n6f.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%2F1roeq3ygypc3t7ew7n6f.png" alt="Trial1" width="799" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Trying again when the target has been met is properly handled.&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%2F4h4duzqeumjuxuhyqynz.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%2F4h4duzqeumjuxuhyqynz.png" alt="Trial1b" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario B: Multi-Step Transfer Under Cap (0.4 SOL Target)
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Initial savings wallet holds 0.2 SOL. Per-transfer cap is set to 50,000,000 lamports.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn 1:&lt;/strong&gt; Model checks balances, determines a deficiency of &lt;code&gt;200,000,000&lt;/code&gt; lamports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 2:&lt;/strong&gt; Proposes transferring &lt;code&gt;200,000,000&lt;/code&gt; lamports. The policy engine blocks it for exceeding the per-transfer limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turns 3-6:&lt;/strong&gt; The agent adjusts and executes four sequential transfers of &lt;code&gt;50,000,000&lt;/code&gt; lamports. All four are approved and confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 7:&lt;/strong&gt; Verifies the new balance of &lt;code&gt;400,000,000&lt;/code&gt; lamports and exits successfully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5x1wrl3jzdh1bbvsdq2h.gif" 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%2F5x1wrl3jzdh1bbvsdq2h.gif" alt="Trial2" width="600" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario C: Impossible Goal (5 SOL Target)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn 1:&lt;/strong&gt; Agent checks balances: operating wallet has 1.55 SOL, savings has 0.4 SOL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 2:&lt;/strong&gt; Agent recognizes it needs 4.6 SOL but only has 1.55 SOL in its operating wallet. It aborts immediately and reports that the goal is impossible to meet, preventing unnecessary transaction attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Funbx0hgat7lcg4waznl5.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%2Funbx0hgat7lcg4waznl5.png" alt="Trial3" width="799" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage of Policy as Guardrails
&lt;/h3&gt;

&lt;p&gt;Although the language model is responsible for planning the next action, it has no authority to move funds on its own.&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%2Fnd8x8uf8d2rmzyi1gj27.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%2Fnd8x8uf8d2rmzyi1gj27.png" alt="Guard Policy" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every transfer request passes through the policy engine before reaching the blockchain.&lt;/p&gt;

&lt;p&gt;Current policy rules include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default action:&lt;/strong&gt; Deny every transfer unless explicitly permitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipient Validation:&lt;/strong&gt; Only approved wallet addresses can receive funds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transfer Cap:&lt;/strong&gt; Reject any transfer that exceeds the configured maximum amount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget Enforcement:&lt;/strong&gt; Reject transfers that would exceed the configured spending budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input Validation:&lt;/strong&gt; Reject malformed addresses or invalid transfer amounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the model's perspective, a denied transfer simply becomes another tool response.&lt;/p&gt;

&lt;p&gt;Instead of forcing execution, the model must either adjust its plan or conclude that the goal cannot be completed.&lt;/p&gt;

&lt;p&gt;This separation of responsibilities creates the core invariant of the entire system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The prompt can change. The model can change. The reasoning path can change. No transaction moves funds without passing the policy engine.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Lessons Learnt and Surprises
&lt;/h3&gt;

&lt;p&gt;The biggest surprise throughout this Arc 14 was how naturally the model adapted to changing circumstances.&lt;/p&gt;

&lt;p&gt;When a transfer exceeded the configured policy limit, the model didn't repeatedly make the same invalid request. Instead, it interpreted the denial as new information and adjusted its strategy by splitting the transfer into smaller approved chunks.&lt;/p&gt;

&lt;p&gt;Another interesting observation was that two runs with the same prompt did not always produce identical tool-call sequences. The reasoning process varied slightly, but the end result remained consistent because every action was constrained by the same policy layer.&lt;/p&gt;

&lt;p&gt;Perhaps the most important lesson was realizing that the policy engine, and not the prompt, is the real source of safety. Prompt instructions can influence behavior, but executable rules are what ultimately determine whether funds can move.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Way Forward
&lt;/h3&gt;

&lt;p&gt;Over this Arc, the challenges reinforced an architectural principle that extends well beyond Solana:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reasoning should decide what to do. Policy should decide whether it is allowed. The blockchain should record what actually happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Separating those responsibilities makes the system easier to audit, easier to extend, and significantly safer than relying on prompt instructions alone.&lt;/p&gt;

&lt;p&gt;While this implementation is intentionally limited to devnet, the same layered architecture can serve as a foundation for more capable on-chain autonomous agents in the future.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article summarizes the agent workflow completed during Arc 14 (Days 92–98) of #100DaysOfSolana.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Check out the &lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/tree/main/week_14" rel="noopener noreferrer"&gt;Week 14 Workspace&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>ai</category>
      <category>beginners</category>
      <category>web3</category>
    </item>
    <item>
      <title>The Architecture of Guarded Blockchain Agents using Code-Enforced Policies</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:25:31 +0000</pubDate>
      <link>https://dev.to/russell_oje/the-architecture-of-guarded-blockchain-agents-using-code-enforced-policies-4m66</link>
      <guid>https://dev.to/russell_oje/the-architecture-of-guarded-blockchain-agents-using-code-enforced-policies-4m66</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Large Language Models can reason, write code, and make decisions, but they also hallucinate, get confused, and can be persuaded to break rules. Prompt engineering helps guide an AI's behavior, but it is not a security mechanism. When connecting an LLM to a live blockchain wallet, the real protection must come from code-enforced policies that explicitly define what actions are allowed and reject everything else by default. Here is how we design a robust, code-enforced, deny-by-default policy layer to protect on-chain assets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Illusion of Prompt Security
&lt;/h2&gt;

&lt;p&gt;When developers first build AI agents, they often try to secure them through the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"You are a helpful assistant. You have a wallet, but you must never transfer more than 0.1 SOL at a time, and you must never send funds to any address not listed below..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a dangerous anti-pattern. Prompts are instructions, not bounds. Through jailbreaking, role-play attacks, or simple adversarial persuasion, an LLM can be manipulated into ignoring its system prompt. If the agent's tools have direct, unrestricted access to sign and send transactions, a successful prompt injection means your wallet can be drained instantly.&lt;/p&gt;

&lt;p&gt;To build secure on-chain agents, we must separate the &lt;strong&gt;Reasoning Engine&lt;/strong&gt; (the LLM) from the &lt;strong&gt;Execution and Policy Engine&lt;/strong&gt; (our code). The model decides what it &lt;em&gt;wants&lt;/em&gt; to do; our code decides what is &lt;em&gt;allowed&lt;/em&gt; to be signed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Guarded Agent Architecture
&lt;/h2&gt;

&lt;p&gt;A secure agentic transaction flow operates like a restricted banking API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       User Request (Goal)
              |
              v
     +-----------------+
     |   Agent Loop    | &amp;lt;--- LLM reasons and outputs a tool call (e.g., transfer_sol)
     +-----------------+
              |
              v
     +-----------------+
     |   MCP Server /  |
     |   Tool Handler  | &amp;lt;--- Resolves the tool request
     +-----------------+
              |
              v
     +-----------------+
     |  Policy Engine  | &amp;lt;--- Deny-by-default checks (Allowlist, per-tx caps, session budgets)
     +-----------------+
              |
        Approved?
         /      \
      Yes        No
      /            \
     v              v
+----------+   +----------+
| Sign &amp;amp;   |   | Block Tx |
| Send Tx  |   | &amp;amp; Return |
|          |   | Error    |
+----------+   +----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this architecture, the LLM never sees or touches the wallet's private key. The private key resides strictly within the backend execution process. When the agent requests a transfer, the parameters are passed to a hard-coded validator. If the parameters violate any policy, the transaction is rejected immediately before it is even compiled.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Pillars of Agentic Wallet Policy
&lt;/h2&gt;

&lt;p&gt;A production-grade agent wallet policy should enforce three layers of protection:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Deny-by-Default Allowlisting
&lt;/h3&gt;

&lt;p&gt;Never let an agent send funds to an arbitrary address. The policy should contain a hard-coded set or read-only database of approved recipient addresses. If the target address is not in the set, the execution halts.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Per-Transfer Cap
&lt;/h3&gt;

&lt;p&gt;Limit the maximum amount of lamports that can be moved in a single transaction. This prevents a single compromised or runaway model turn from transferring the entire wallet balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Session / Cumulative budget
&lt;/h3&gt;

&lt;p&gt;A per-transfer cap alone is insufficient; an LLM could run in a loop and execute twenty 0.1 SOL transactions to drain 2 SOL. A cumulative session cap tracks the sum of all successful transfers in the current run and blocks further action once the threshold is crossed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons from the Field
&lt;/h2&gt;

&lt;p&gt;As we ran experiments with this architecture, several critical insights emerged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLMs are adaptive, not deterministic.&lt;/strong&gt; The same prompt may produce different—but still valid—tool-call sequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policies are more important than prompts.&lt;/strong&gt; Prompt engineering cannot guarantee safety; executable policy checks can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomy requires observability.&lt;/strong&gt; Detailed logs made it possible to understand every reasoning step and verify that safety rules were enforced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small constraints encourage better planning.&lt;/strong&gt; Lower transfer limits forced the model to decompose larger goals into valid sequential actions instead of repeatedly failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Devnet is the right proving ground.&lt;/strong&gt; Autonomous agents should earn trust in a sandbox before interacting with assets of real value.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>100daysofsolana</category>
      <category>beginners</category>
      <category>web3</category>
    </item>
    <item>
      <title>The Ultimate Integration Checklist to Deploy A Solana Program To Mainnet</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 18 Jul 2026 10:28:12 +0000</pubDate>
      <link>https://dev.to/russell_oje/the-ultimate-integration-checklist-to-deploy-a-solana-program-to-mainnet-3h0i</link>
      <guid>https://dev.to/russell_oje/the-ultimate-integration-checklist-to-deploy-a-solana-program-to-mainnet-3h0i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Moving a program to Solana mainnet-beta is the ultimate staging-to-production moment. Mistakes here are not free: they cost real SOL, and a single wrong click can lock up your authority or freeze your bytecode forever. Here is the hands-on launch checklist I wrote after taking my vault program live.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Background Picture
&lt;/h2&gt;

&lt;p&gt;In Web2 development, a bad deployment is resolved by a quick git rollback or a server restart. On Solana, the blockchain is your server. Your bytecode is deployed into accounts that stay live forever, funded by real-world rent. The wallet that executes the initial deploy silently inherits permanent upgrade authority over that address. &lt;/p&gt;

&lt;p&gt;Under launch pressure, relying on memory is a recipe for disaster. That is why astronauts, surgeons, and similarly, smart contract engineers, use checklists. This guide groups the exact steps we walked through across Days 85 to 89 to take our vault program from staging to production, ensuring nothing is missed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Pre-Flight (Staging on Devnet)
&lt;/h2&gt;

&lt;p&gt;Before committing real value, you must run through staging checks on devnet where failures cost nothing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;All local and integration tests pass perfectly against the final binary.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;The build is compiled verifiably.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Compiling with &lt;code&gt;anchor build --verifiable&lt;/code&gt; ensures that the resulting bytecode on-chain can later be matched and verified against your open-source repository.
&amp;gt; [!IMPORTANT]
&amp;gt; Once you generate a verifiable build, do not run &lt;code&gt;cargo build-sbf&lt;/code&gt; or a simple &lt;code&gt;anchor build&lt;/code&gt;, as variations in local environment compilation can alter the binary hash and break verification.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Program keys are fully synchronized.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Sync the program IDs in your workspace with &lt;code&gt;anchor keys sync&lt;/code&gt;. Recompile if any changes are made so that &lt;code&gt;declare_id!&lt;/code&gt; and the generated key files match.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;☑ &lt;strong&gt;The deploy wallet is funded.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the rent estimator to see how much SOL the program account will require:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;solana rent &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &amp;lt; target/deploy/vault.so&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;Ensure the deploy keypair holds enough devnet (and eventually mainnet) SOL to cover this rent plus transaction fees.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 2: The Deploy (Promoting to Mainnet-Beta)
&lt;/h2&gt;

&lt;p&gt;This is the point of no return. Double-check your networks and fee parameters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;The Solana CLI is pointing to the correct environment.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Confirm with &lt;code&gt;solana config get&lt;/code&gt; that the endpoint is set to &lt;code&gt;https://api.mainnet-beta.solana.com&lt;/code&gt; (or your private RPC endpoint).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;☑ &lt;strong&gt;A dedicated RPC endpoint is configured.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The public mainnet RPC nodes are heavily rate-limited. Deploys consist of dozens of sequential transactions; always deploy via a dedicated RPC (e.g., Helius, QuickNode) wrapped in quotes:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;anchor program deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provider&lt;/span&gt;.cluster &lt;span class="s2"&gt;"https://mainnet.helius-rpc.com/?api-key=YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--with-compute-unit-price&lt;/span&gt; 10000 &lt;span class="nt"&gt;--use-rpc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;☑ &lt;strong&gt;Priority fees and TPU routing are enabled.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passing &lt;code&gt;--with-compute-unit-price 10000&lt;/code&gt; (in micro-lamports per CU) and &lt;code&gt;--use-rpc&lt;/code&gt; ensures your transactions land safely even during periods of heavy mainnet congestion.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;☑ &lt;strong&gt;Buffer recovery strategy is prepared.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a deploy stalls halfway due to expired blockhashes, your SOL is not lost. It is resting in a temporary buffer. Do not start over! Identify and resume it:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List stranded buffers&lt;/span&gt;
solana program show &lt;span class="nt"&gt;--buffers&lt;/span&gt;
&lt;span class="c"&gt;# Reclaim buffer rent if abandoning&lt;/span&gt;
solana program close &lt;span class="o"&gt;[&lt;/span&gt;BUFFER_ADDRESS]
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Phase 3: Authority &amp;amp; Verification (Securing the Contract)
&lt;/h2&gt;

&lt;p&gt;Once the code is on-chain, secure the keys that control it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;Verify upgrade authority immediately.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;solana program show [YOUR_PROGRAM_ID]&lt;/code&gt; and inspect the &lt;code&gt;Authority&lt;/code&gt; field.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Determine the long-term upgrade authority.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Keypair:&lt;/strong&gt; Keep it only for developer testing. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multisig (Squads):&lt;/strong&gt; The recommended production path. Transfer authority to a multi-signature safe so upgrades require approval from multiple team members (&lt;em&gt;Coming soon...&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutable (&lt;code&gt;--final&lt;/code&gt;):&lt;/strong&gt; Freeze the program permanently. Note that this is a one-way door; you can never fix a bug or add a feature once you freeze it (&lt;em&gt;Coming soon...&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;☑ &lt;strong&gt;Publish the IDL on-chain.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make your program self-documenting by publishing the Interface Definition Language schema:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;anchor idl init &lt;span class="nt"&gt;-f&lt;/span&gt; target/idl/vault.json &lt;span class="o"&gt;[&lt;/span&gt;YOUR_PROGRAM_ID] &lt;span class="nt"&gt;--provider&lt;/span&gt;.cluster &lt;span class="s2"&gt;"https://mainnet.helius-rpc.com/?api-key=YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;☑ &lt;strong&gt;Generate and export the typed client.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run Codama to output autocomplete-ready TypeScript helpers for your team or frontend:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;npx codama run js
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Phase 4: Frontend Integration &amp;amp; Going Live
&lt;/h2&gt;

&lt;p&gt;Your program is secure and documented; now connect it to the user interface.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;Point the frontend to the new mainnet program ID.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Update the configuration in &lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/tree/main/my-frontend/src/providers.tsx" rel="noopener noreferrer"&gt;src/providers.tsx&lt;/a&gt; to target Mainnet-Beta.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Test wallet discovery with the Wallet Standard.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Verify that browser-extension wallets (Phantom, Solflare, Backpack) discover the app smoothly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Verify error classification is active.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Ensure that &lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/tree/main/my-frontend/src/walletErrors.ts" rel="noopener noreferrer"&gt;walletErrors.ts&lt;/a&gt; is wired to handle user cancellations, insufficient funds, and network failures gracefully instead of throwing raw stack traces.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lessons Learnt and Surprises
&lt;/h2&gt;

&lt;p&gt;The biggest surprise during this process was how easily a deploy can stall. Without adding priority fees (&lt;code&gt;--with-compute-unit-price&lt;/code&gt;) and using a custom RPC, the public endpoint rate-limiting would fail after 10–20 transaction writes, leaving half-finished bytecode stranded in buffers. Understanding buffer management commands and knowing that the rent SOL could be reclaimed via &lt;code&gt;solana program close&lt;/code&gt; was a massive relief.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post summarizes the production launch workflow completed during Week 13 (Days 85–91) of #100DaysOfSolana.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Check out the &lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/tree/main/week_13" rel="noopener noreferrer"&gt;Week 13 Workspace&lt;/a&gt; and the React frontend implementation at &lt;a href="https://ruxy1212.github.io/solana-100-days-mlh/my-frontend/" rel="noopener noreferrer"&gt;my-frontend&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building a Bulletproof Program in Solana: A Hands-On Security Checklist</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:40:52 +0000</pubDate>
      <link>https://dev.to/russell_oje/building-a-bulletproof-program-in-solana-a-hands-on-security-checklist-55om</link>
      <guid>https://dev.to/russell_oje/building-a-bulletproof-program-in-solana-a-hands-on-security-checklist-55om</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;On public blockchains, there are no friendly QA teams to catch your slip-ups before mainnet. The first entity to find a vulnerability in your code should be you. Here is the hands-on security checklist I built after auditing, testing, and intentionally breaking my own Anchor programs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Background Picture
&lt;/h2&gt;

&lt;p&gt;Transitioning from a traditional web2 backend to writing Solana programs requires a complete shift in security mindset. On Solana, the execution model is completely different: programs are stateless, so the runtime allows any arbitrary account to be passed into any instruction, and arithmetic overflows wrap silently in release builds.&lt;/p&gt;

&lt;p&gt;Under pressure, it is easy to forget routine checks. That is why safety-critical industries use checklists. This checklist translates theoretical security concepts into concrete, verifiable rules that you can run down line by line before any contract touches mainnet.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Account Validation (The "Owner Question")
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;Every account's owner is validated.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Account types are guarded by distinct 8-byte discriminators.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Solana, if you read an account's data without checking who owns it, an attacker can pass a fake account containing whatever bytes they want. This exact class of vulnerability resulted in the $326 million Wormhole bridge hack.&lt;/p&gt;

&lt;p&gt;To understand the mechanics, I rebuilt this vulnerability in a sandbox using LiteSVM. The vulnerable program accepted an &lt;code&gt;UncheckedAccount&lt;/code&gt; and deserialized 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="c1"&gt;// VULNERABLE: we read this account's bytes without checking who owns it.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;data&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.config&lt;/span&gt;&lt;span class="nf"&gt;.try_borrow_data&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Config&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&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="o"&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 there was no owner validation, an attacker could set up a custom account owned by the System Program, write a custom config layout with their own public key as the admin, and bypass the authorization checks.&lt;/p&gt;

&lt;p&gt;The fix is declarative. By changing the account type to Anchor's typed wrapper, Anchor validates the owner and discriminator before execution:&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="c1"&gt;// SECURE: Account&amp;lt;'info, T&amp;gt; automatically checks the discriminator and program ownership.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;config&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;Config&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;h2&gt;
  
  
  2. Authority &amp;amp; Signer Checks (The "Signer Question")
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;Privileged actions require a verified signature, not just a matching key.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Anchor constraints (&lt;code&gt;has_one&lt;/code&gt; or custom &lt;code&gt;constraint&lt;/code&gt;) enforce logical ownership.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just because a public key is passed into your instruction and matches a storage field does not mean that the wallet owner authorized the action. You must verify the signature flag:&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;Withdraw&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;mut,&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"vault"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;authority&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="n"&gt;has_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Enforces vault.authority == authority.key()&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;vault&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;Vault&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="c1"&gt;// Verifies the authority signed the transaction&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;Every math operation uses checked arithmetic (&lt;code&gt;checked_add&lt;/code&gt;, &lt;code&gt;checked_sub&lt;/code&gt;, &lt;code&gt;checked_mul&lt;/code&gt;).&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;All mathematical assumptions are proven using property-based testing.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rust compiles with overflow checks enabled in debug builds, but in release mode (by default on mainnet), overflows wrap around silently. To ensure safety, we must write pure math functions and stress-test them.&lt;/p&gt;

&lt;p&gt;Instead of guessing inputs, we can use property-based testing (&lt;code&gt;proptest&lt;/code&gt;) to throw thousands of random numbers at our math module:&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;apply_deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balance&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="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;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&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;balance&lt;/span&gt;&lt;span class="nf"&gt;.checked_add&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="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[cfg(test)]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;apply_deposit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;proptest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&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="nd"&gt;proptest!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;#[test]&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;deposit_never_shrinks_a_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nn"&gt;any&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&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="k"&gt;in&lt;/span&gt; &lt;span class="nn"&gt;any&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&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;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;apply_deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balance&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="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="n"&gt;new_balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;prop_assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;prop_assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="nf"&gt;.checked_add&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="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Invariant Fuzzing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;State invariants are tested through multi-step instruction sequences.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While unit tests prove isolated logic, vulnerabilities often arise from unexpected interaction sequences. Fuzzing frameworks like &lt;strong&gt;Trident&lt;/strong&gt; allow you to set up guided, automated instruction flows (like running &lt;code&gt;deposit&lt;/code&gt; continuously) to ensure your invariants hold:&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;#[flow]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;deposit_never_shrinks&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="k"&gt;self&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;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.trident.get_account_with_type&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vault&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;vault_pda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="py"&gt;.balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.trident&lt;/span&gt;&lt;span class="nf"&gt;.random_from_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1_000_000u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Process random deposit instruction...&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.trident.get_account_with_type&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vault&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;vault_pda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&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;v&lt;/span&gt;&lt;span class="py"&gt;.balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"deposit shrank the balance"&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="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;FuzzTest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fuzz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fuzzing campaign was configured using FuzzTest::fuzz(1000, 100). So rather than executing 1,000 individual instructions, the framework generated approximately 1,000 randomized instruction sequences, each containing up to 100 operations. This resulted in roughly 100,000 instruction executions, allowing the program invariants to be validated across a wide variety of automatically generated transaction sequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Adversarial Testing with LiteSVM
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;☑ &lt;strong&gt;The test suite includes tests designed explicitly to rob the vault.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;☑ &lt;strong&gt;Assertions check for specific error codes, preventing false positive passes.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A robust security verification flow doesn't just check the happy path; it acts as a pessimist. Write dedicated test cases that mimic penetration attacks—trying to withdraw without a signature, swapping PDA inputs, or attempting to trigger underflows.&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;wrong_signer_is_rejected&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="nf"&gt;setup_svm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Craft transaction with invalid signer, send, and verify error code&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;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="c1"&gt;// Assert the exact Anchor error (e.g. 2006 for ConstraintHasOne)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By enforcing specific error code matches, you ensure the transaction failed for the exact security boundary you defined, rather than an unrelated execution error. &lt;/p&gt;




&lt;p&gt;One final, but very important check, was to ensure that while these bulletproof methods were in place, our program would not lock out &lt;strong&gt;legitimate&lt;/strong&gt; actors.&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%2Ffwrqmlocwazhhpp3teva.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%2Ffwrqmlocwazhhpp3teva.png" alt="Legitimate Test" width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post summarizes what was learnt across Arc12 (Days 78–84) of #100DaysOfSolana. We moved from auditing state accounts, hardening instructions, writing adversarial tests in LiteSVM, to fuzzing code paths with Trident and reproducing real-world vulnerability patterns.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/tree/main/week12" rel="noopener noreferrer"&gt;See the complete Arc12 advanced testing repository on Github.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>CPI in Solana: A Function Call with a Guest List</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Mon, 06 Jul 2026 16:13:04 +0000</pubDate>
      <link>https://dev.to/russell_oje/cpi-in-solana-a-function-call-with-a-guest-list-41l1</link>
      <guid>https://dev.to/russell_oje/cpi-in-solana-a-function-call-with-a-guest-list-41l1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Five days ago, CPI was three letters I had started to recognize in the docs. Now I have watched programs call the System Program, Token-2022, and even my own code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Background Picture
&lt;/h2&gt;

&lt;p&gt;A cross-program invocation is exactly what it sounds like: your program pauses, calls an instruction in a different program, waits for it to return, and then picks up where it left off. That description sounds simple, but the first time you stare at a &lt;code&gt;CpiContext&lt;/code&gt; you do not feel simplicity; you feel three unfamiliar arguments and a missing mental model.&lt;/p&gt;

&lt;p&gt;Every CPI is a function call with a guest list. Your program is the host. The program you are calling is the venue. The guest list is the set of accounts that venue needs to do its job. Before it lets anyone in, the runtime checks three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The program being called — identified by its program ID.&lt;/strong&gt; When you write &lt;code&gt;CpiContext::new(ctx.accounts.system_program.key(), ...)&lt;/code&gt;, that first argument is the ID of the program you are delegating to. The runtime verifies it is a real deployed program.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The accounts that program needs — which your program has to pass through.&lt;/strong&gt; The program you are calling cannot see your entire account list. It only sees what you hand it inside the struct you attach to the context. If you forget an account, the inner program gets a lamport discrepancy or an unexpected signer error, not a helpful message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The signer authority — either a real keypair or a PDA your program signs for.&lt;/strong&gt; This is the one that took me the longest to understand, so it gets its own section below.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Code: Smallest Working CPI
&lt;/h2&gt;

&lt;p&gt;Here is the deposit handler from the vault program on Day 73. It is the smallest CPI I have that is doing something real:&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;deposit&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;Deposit&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_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.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="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.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="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_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;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;Deposit&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;user&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(&lt;/span&gt;
        &lt;span class="nd"&gt;mut,&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"vault"&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="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;vault&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;Three things to notice. First, &lt;code&gt;system_program.key()&lt;/code&gt; is the target program ID — the first argument to &lt;code&gt;CpiContext::new&lt;/code&gt;. Second, &lt;code&gt;Transfer { from, to }&lt;/code&gt; is the guest list, the exact accounts the System Program needs to move lamports. Third, the user is a &lt;code&gt;Signer&lt;/code&gt;, so the runtime already has their signature from the outer transaction; it is automatically forwarded into the inner one. No extra work needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signer Seeds: How a Program Proves It Is the PDA
&lt;/h2&gt;

&lt;p&gt;The deposit CPI above is an easy case. The user signed the outer transaction, so the System Program happily accepts their authority over the &lt;code&gt;from&lt;/code&gt; account.&lt;/p&gt;

&lt;p&gt;The withdraw case is different. The &lt;code&gt;from&lt;/code&gt; account is the vault PDA, i.e. an address with no private key. Nobody could have signed the transaction on its behalf. This is where &lt;code&gt;invoke_signed&lt;/code&gt; (and Anchor's &lt;code&gt;.with_signer(...)&lt;/code&gt;) comes in.&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;The seeds array in &lt;code&gt;.with_signer(...)&lt;/code&gt; is the program saying: &lt;em&gt;"I can reproduce the derivation that created this address. Here are the ingredients."&lt;/em&gt; The runtime re-derives the PDA from those seeds plus the calling program's ID and checks that it matches the account you are claiming authority over. If it does, the CPI proceeds as if the vault signed it. If it does not, the transaction fails before any lamports move.&lt;/p&gt;

&lt;p&gt;This is the moment &lt;code&gt;invoke_signed&lt;/code&gt; clicked for me: the seeds inside your handler have to match the seeds inside your &lt;code&gt;#[account(seeds = ..., bump)]&lt;/code&gt; constraint exactly. Those two places are talking about the same address. When they match, you get a vault your program can spend from. When they differ, you get an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Little Observations
&lt;/h2&gt;

&lt;p&gt;On Day 75, I broke the signer seeds on purpose. I changed &lt;code&gt;b"vault"&lt;/code&gt; to &lt;code&gt;b"vault!"&lt;/code&gt; in the &lt;code&gt;.with_signer(...)&lt;/code&gt; call while the constraint on the accounts struct still said &lt;code&gt;b"vault"&lt;/code&gt;. The runtime error was:&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%2F1ktneqd1y3h3icue8gff.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%2F1ktneqd1y3h3icue8gff.png" alt="WrongInvocation" width="800" height="134"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cross Program Invocation with Unauthorized Signer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a short sentence but it is a precise diagnosis. The System Program looked at the vault account in the Transfer struct, expected a valid signature on its behalf; either a real keypair in the transaction or a valid &lt;code&gt;invoke_signed&lt;/code&gt; derivation, and found neither. The seeds mismatch meant the re-derived address did not match the vault, so the program's implicit signature was rejected. The fix was to put &lt;code&gt;b"vault"&lt;/code&gt; back in both places.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;CpiContext&lt;/code&gt; pattern scales without modification. On Day 72, the same three-piece structure called &lt;code&gt;token_interface::mint_to&lt;/code&gt; against the Token-2022 program. On Day 74, it called an &lt;code&gt;increment&lt;/code&gt; instruction in a second program I wrote, with &lt;code&gt;declare_program!(counter)&lt;/code&gt; generating the CPI client from the counter's IDL automatically.&lt;/p&gt;

&lt;p&gt;One pattern, three targets, identical shape each time. That is the thing worth internalizing. Once the pattern is solid, picking a new CPI target is mostly reading the target program's accounts struct and mapping it to the guest list.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post draws from Days 71–75 of #100DaysOfSolana. Day 71 was my very first first CPI. Day 73 was the PDA-signed vault. Day 74 was program-to-program. Day 75 was deliberately breaking all three.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/ruxy1212/solana-100-days-mlh/tree/main/week11" rel="noopener noreferrer"&gt;See the complete Arc11 flow on Github&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Solana: The Byte Tax</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 04 Jul 2026 22:19:32 +0000</pubDate>
      <link>https://dev.to/russell_oje/solana-the-byte-tax-35n9</link>
      <guid>https://dev.to/russell_oje/solana-the-byte-tax-35n9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Why does every feature you bolt onto a Solana Token come with a price tag?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In every Web2 stack I've worked in, adding a column to a table is basically free. You run a migration, maybe wait a minute for it to backfill, and move on. Storage is cheap, and nobody itemizes what a single boolean field costs you.&lt;/p&gt;

&lt;p&gt;On Solana, I found out the hard way that &lt;em&gt;every&lt;/em&gt; feature I bolt onto a token has a &lt;strong&gt;literal&lt;/strong&gt;, &lt;strong&gt;permanent&lt;/strong&gt;, &lt;strong&gt;one-time&lt;/strong&gt; price tag, and even more fascinating, you can watch that price change in real SOL as you add extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rent Isn't a Metaphor Here
&lt;/h2&gt;

&lt;p&gt;Proportional to how many bytes of data it stores, every account on Solana, including a token's mint account, has to hold enough SOL to stay "rent-exempt". Token Extensions live inside that same account, packed into a Type-Length-Value buffer bolted onto the base mint. So more extensions means more bytes, which implies a bigger one-time deposit before the account can exist at all.&lt;/p&gt;

&lt;p&gt;I built three different mints in Arc 6 of the &lt;code&gt;100DaysOfSolana&lt;/code&gt; and measured them side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mint&lt;/th&gt;
&lt;th&gt;Extensions Enabled&lt;/th&gt;
&lt;th&gt;Account Size (bytes)&lt;/th&gt;
&lt;th&gt;Rent Cost (SOL)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interest-bearing only&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-extension&lt;/td&gt;
&lt;td&gt;Interest-bearing, Transfer fees, Metadata Pointer, Metadata&lt;/td&gt;
&lt;td&gt;599&lt;/td&gt;
&lt;td&gt;~0.005060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance-gated&lt;/td&gt;
&lt;td&gt;Default Account State (Frozen)&lt;/td&gt;
&lt;td&gt;171&lt;/td&gt;
&lt;td&gt;~0.002081&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly a 2.4x jump in both size and cost just from stacking three extra behaviors onto one mint. Meanwhile, the "default frozen" mint, despite sounding like the heaviest compliance feature of the three, turned out to be one of the cheapest, smallest accounts I created all Epoch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Actually Matters
&lt;/h2&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 single command above (the one that produced my 599-byte mint), reads like a feature checklist: fees, yield, and metadata, all in one line. In Web2, shipping that many "nice to haves" costs you engineering time, not a line item you pay before the feature goes live. On Solana, the cost is upfront, denominated in SOL, and visible the moment you run &lt;code&gt;solana account $MINT --output json&lt;/code&gt; and read the space field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Part Yet
&lt;/h2&gt;

&lt;p&gt;The correlation is almost too clean: every extension adds a predictable number of bytes, and every byte has a knowable SOL cost. There's no hidden "it depends on load" the way cloud storage pricing gets murky. You can price out a token's entire feature set before writing a single line of code, the same way you'd budget infrastructure before choosing a stack.&lt;/p&gt;

&lt;p&gt;It reframed extensions for me as a real trade-off instead of a free checkbox. There's no such thing as a free feature flag on a mint account. You decide what you actually need, because you're the one paying the rent-exempt deposit for whatever you don't cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Forward
&lt;/h2&gt;

&lt;p&gt;Say you design a schema, choose any product that you've done in the past. But the caveat is that every field you added to the schema, will cost you real money, forever, and upfront; which of your product's "nice to have" fields would you cut first? I want to hear about the most bloated schema you've ever inherited. Let's see how it stacks up against a 599-byte mint account.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Solana: Can You Own Something You're Not Allowed to Sell?</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 04 Jul 2026 22:12:24 +0000</pubDate>
      <link>https://dev.to/russell_oje/solana-can-you-own-something-youre-not-allowed-to-sell-1p39</link>
      <guid>https://dev.to/russell_oje/solana-can-you-own-something-youre-not-allowed-to-sell-1p39</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Solana's Soulbound Credentials, explained for Web2 Devs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ownership on a blockchain is supposed to be the whole pitch: whoever holds the private key holds the asset, full stop, no admin panel required. So what happens when I build a token that you can hold... but can never sell, trade, or give away — &lt;em&gt;and that I can delete from your wallet without asking you?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's exactly what I built in Arc 6 of the &lt;code&gt;#100DaysOfSolana&lt;/code&gt; using two Token Extensions stacked on the same mint (three, actually), and it forced me to rethink what "ownership" even means once code, not policy, is the one enforcing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Extensions That Make This Possible
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Non-Transferable&lt;/strong&gt; locks a token to the account it was minted into. Forever. Not "until the marketplace updates its terms" — the transfer instruction itself is rejected by the protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent Delegate&lt;/strong&gt; grants one authority (in this case, me, the issuer) the power to burn or move the token out of any holder's account without their signature. No multisig, no support ticket, no "please confirm this action."&lt;/p&gt;

&lt;p&gt;Combined with a &lt;strong&gt;Metadata&lt;/strong&gt; extension, this is enough to build a revocable credential entirely from CLI flags; think about a certification, a compliance badge, or even a membership pass:&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; 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-permanent-delegate&lt;/span&gt; &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;&lt;code&gt;--decimals 0&lt;/code&gt; because a credential is a whole thing. You either hold it, or you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Rules, Not Just Trusting Them
&lt;/h2&gt;

&lt;p&gt;I minted one credential to a second wallet, then tried to move it to a third wallet using the second wallet's own keypair:&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 &lt;span class="nv"&gt;$MINT&lt;/span&gt; 1 &lt;span class="nv"&gt;$THIRD_PARTY&lt;/span&gt; &lt;span class="nt"&gt;--owner&lt;/span&gt; ~/recipient-wallet.json &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;Rejected. Not "denied by my backend" — rejected by the Token-2022 program itself, because the non-transferable rule is written into the mint's on-chain data.&lt;/p&gt;

&lt;p&gt;Then I did the uncomfortable part. Using my permanent delegate authority, I burned that same credential straight out of the recipient's account, without requesting a signature from them:&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 burn &lt;span class="nv"&gt;$RECIPIENT_TOKEN_ACCOUNT_ADDRESS&lt;/span&gt; 1 &lt;span class="nt"&gt;--owner&lt;/span&gt; ~/.config/solana/id.json &lt;span class="nt"&gt;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. Instantly. No dispute process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outside the Theory
&lt;/h2&gt;

&lt;p&gt;This pattern already exists in production. A good example is Paxos, which uses the Permanent Delegate extension on its USDP stablecoin specifically to claw back funds tied to illegal activity, satisfying a regulatory requirement it operates under. That's a real, audited, non-hypothetical use of &lt;em&gt;"I can take this back from you"&lt;/em&gt; living directly inside a currency that moves real money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Part Yet
&lt;/h2&gt;

&lt;p&gt;I expected "soulbound" logic to require deploying a custom contract or program that intercepts every transfer instruction and checks a condition. It doesn't. The protocol understands non-transferability and delegated revocation natively — no custom smart contract, no audit surface beyond the extensions I opted into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Forward
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable question I can't fully answer myself: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if I can revoke your credential without your signature, is it actually &lt;em&gt;yours&lt;/em&gt;, or is it just a permission slip with extra ceremony? - Is this meaningfully different from a company revoking your enterprise SSO seat the day you're offboarded, or is it even worse, since it's permanent and public?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I want the Web2 argument here. Tell me where I'm wrong in the comments.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
      <category>100daysofsolana</category>
    </item>
    <item>
      <title>What Solana's Transfer Fee Extension Taught Me About Trustless Payments</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sat, 04 Jul 2026 22:04:34 +0000</pubDate>
      <link>https://dev.to/russell_oje/what-solanas-transfer-fee-extension-taught-me-about-trustless-payments-403h</link>
      <guid>https://dev.to/russell_oje/what-solanas-transfer-fee-extension-taught-me-about-trustless-payments-403h</guid>
      <description>&lt;p&gt;Every web2 payment flow I've ever shipped has the same shape: a request hits my server, my server calculates the amount and fees, and then my server updates the database. The fee only exists because my code enforces it. If someone finds a way around my API, say, using a direct database write, or a race condition, or even a webhook that never fires, then the fee just doesn't happen.&lt;/p&gt;

&lt;p&gt;In Epoch 2 of &lt;code&gt;#100DaysOfSolana&lt;/code&gt;, I built a token where this entire problem disappears. It disappears, not because I wrote better backend code, but because there was no backend to bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Web2 Way:
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fees Live in Application Code
&lt;/h3&gt;

&lt;p&gt;Think about how a typical marketplace fee works. A buyer pays $100, your Stripe webhook fires, your server calculates a 5% platform fee, and your server moves $95 to the seller's payout and the $5 fee to your treasury. That logic lives entirely in your application layer. It is correct as long as nobody modifies to your database directly, nobody manipulates your webhook, and your cron job never double-fires.&lt;/p&gt;

&lt;p&gt;That's a lot of "as long as."&lt;/p&gt;

&lt;h2&gt;
  
  
  How Solana Does It:
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Baking the Fee into the Currency Itself
&lt;/h3&gt;

&lt;p&gt;Solana's Token Extensions Program (Token-2022) lets you attach a &lt;code&gt;TransferFeeConfig&lt;/code&gt; directly to a token's mint account. Once it's there, the fee isn't a suggestion your application makes, but a rule the network itself enforces on every single transfer, for every wallet, FOREVER.&lt;/p&gt;

&lt;p&gt;Here's the exact command I used to create a mint with a 1% fee and a fee cap:&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; 100 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfer-fee-maximum-fee&lt;/span&gt; 1000000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decimals&lt;/span&gt; 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No webhook. No cron job. No server at all. The 100 basis points (1%) is stored in the mint's own account data, and the Token-2022 program checks it on every transfer instruction before the transaction is even allowed to land.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching It Actually Refuse to Be Bypassed
&lt;/h3&gt;

&lt;p&gt;The part that changed how I think about this wasn't creating the mint, but it was when I tried to move tokens around it. When I sent 1,000 tokens to a second wallet, I had to tell the CLI what fee I expected:&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 &lt;span class="nv"&gt;$MINT&lt;/span&gt; 1000 &lt;span class="nv"&gt;$RECIPIENT&lt;/span&gt; &lt;span class="nt"&gt;--expected-fee&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that number doesn't match what the protocol calculates, the transfer fails outright. There's no code path where the fee "doesn't happen." The 10 tokens land in a withheld balance sitting inside the recipient's own token account, visible, auditable, and untouched until the mint's withdraw authority explicitly harvests 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 withdraw-withheld-tokens &lt;span class="nv"&gt;$MY_TOKEN_ACCOUNT&lt;/span&gt; &lt;span class="nv"&gt;$RECIPIENT_TOKEN_ACCOUNT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could see the withheld amount sitting there before I swept it. In Web2 terms, it's like the database enforcing your business rule for you at the storage engine level, instead of trusting your application code to get it right every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outside the Theory
&lt;/h2&gt;

&lt;p&gt;This isn't just a devnet toy "in theory". There's a meme-adjacent token called BERN, which uses the Transfer Fee extension so that every transfer automatically skims a percentage split between burning BONK, burning BERN, and rewarding holders. &lt;/p&gt;

&lt;p&gt;Regulated stablecoin issuers also use the same Token-2022 toolbox for very different reasons. Another interesting use case is Paxos, where they specifically enabled the Permanent Delegate extension on their USDP stablecoin so it can claw back funds used for illegal purposes, meeting strict regulatory requirements it operates under.&lt;/p&gt;

&lt;p&gt;Same program, wildly different use cases, both enforced at the protocol layer instead of the application layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Part Yet
&lt;/h2&gt;

&lt;p&gt;I stacked the Transfer Fee extension with an Interest-Bearing rate on the same mint later in the week, and both behaviors lived independently in the same account's data. The fee deducts on transfer, and the interest accrues on display; neither one needs to know the other exists. Composability at the protocol level means that, I get a "middleware" for my currency, without writing a single line of middleware. Cool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Forward
&lt;/h2&gt;

&lt;p&gt;If a fee genuinely could not be bypassed, no matter who touched the raw ledger, what would you build with that guarantee? &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A treasury that funds itself? &lt;/li&gt;
&lt;li&gt;Or a royalty stream that survives even if the marketplace's website goes down? &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tell me the fee-based idea you'd never trust a Web2 backend to enforce correctly.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>A Mental Dive into Solana PDAs</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:49:02 +0000</pubDate>
      <link>https://dev.to/russell_oje/a-mental-dive-into-solana-pdas-5fi4</link>
      <guid>https://dev.to/russell_oje/a-mental-dive-into-solana-pdas-5fi4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;On Solana, programs are stateless. So if your program needs to remember something per user, per config, per anything, it needs a deterministic address it can find again later without storing it anywhere. PDAs are that address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Background Picture
&lt;/h2&gt;

&lt;p&gt;From the web2 world, a PDA (or Program Derived Address) is like a database primary key you compute from the row's logical identity. But instead of &lt;code&gt;INSERT INTO counters (user_id) RETURNING id&lt;/code&gt;, you run a hash over &lt;code&gt;["counter", user_pubkey, program_id]&lt;/code&gt; and that hash &lt;em&gt;is&lt;/em&gt; the address or PDA. It requires no lookup table and no coordination. The client and the program independently arrive at the same address every time.&lt;/p&gt;

&lt;p&gt;But there's a twist here, that must be noted: PDAs are not necessarily similar to rows in a table. Meaning that the address may exist, i.e. there is a live account with data at that location, or it may not. So deriving an address does not create an account. That is a separate step, and it costs rent (a small SOL deposit, held as collateral for the bytes you occupy on every validator). The derivation is just arithmetic.&lt;/p&gt;

&lt;p&gt;Another observation to note is that the program ID is baked into the hash. The same seeds in a different program produce a completely different address. This implies that a PDA belongs to exactly one program, and only that program can authorize writes to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Derivation
&lt;/h2&gt;

&lt;p&gt;On Day 64, I ran a standalone script to make the determinism tangible. The core of it was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bump&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findProgramAddressSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;counter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
  &lt;span class="nx"&gt;programId&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the same call twice gave the identical address. Running it with &lt;code&gt;"alice"&lt;/code&gt; as a second seed gave something completely different, which is the whole point of PDAs.&lt;/p&gt;

&lt;p&gt;When the same seeds moved into the counter program itself, they looked like this:&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;Key pieces to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;b"counter"&lt;/code&gt;&lt;/strong&gt; — a static byte string that namespaces this address. Without it, different account types with the same user key would collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;user.key().as_ref()&lt;/code&gt;&lt;/strong&gt; — the signer's public key as a byte slice. This is what gives every wallet its own address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bump&lt;/code&gt;&lt;/strong&gt; — this is a range from 0 - 255, more on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;space = 8 + Counter::INIT_SPACE&lt;/code&gt;&lt;/strong&gt; — the &lt;code&gt;8&lt;/code&gt; is Anchor's discriminator, a prefix that identifies which account type this is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the Seeds Matter
&lt;/h2&gt;

&lt;p&gt;Day 68 especially, was deliberately breaking things, and it was my most instructive day this week.&lt;/p&gt;

&lt;p&gt;Compare these two seed arrays:&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;span class="c1"&gt;// per-user PDA&lt;/span&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="c1"&gt;// global PDA&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first gives every wallet its own isolated counter, while the second gives every wallet the &lt;em&gt;same&lt;/em&gt; address, with a first-write-wins collision where whoever calls &lt;code&gt;init_counter&lt;/code&gt; first owns the account for everyone. This is not a bug in either case; it is a design choice, just like the config singleton is intentionally global:&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"config"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="n"&gt;bump&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's usually only one config per program, derived from a fixed seed, and initialized once. Calling &lt;code&gt;init&lt;/code&gt; on an existing account will return a runtime error, so the singleton guarantee is enforced for free.&lt;/p&gt;

&lt;p&gt;The near-miss variants from the collision experiment on Day 68 drove the point home. &lt;code&gt;"counter"&lt;/code&gt;, &lt;code&gt;"counters"&lt;/code&gt;, &lt;code&gt;"counter\0"&lt;/code&gt;, and &lt;code&gt;"Counter"&lt;/code&gt; each produce a completely unrelated address. There is no fuzzy matching, one byte difference, entirely different account namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 'Bump' Key Piece
&lt;/h2&gt;

&lt;p&gt;The bump, is the byte (0-255) that makes the whole thing work, even though it seems easy to hand-wave past it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;findProgramAddressSync&lt;/code&gt; hashes the seeds together with the program ID and a trailing byte, the bump, which starts at 255 and counting down to 0. Most public keys happen to fall on the ed25519 elliptic curve, which means they could theoretically have a private key. The runtime needs your PDA to be off that curve, so no one can generate a private key for it. The first bump value that produces an off-curve address is the &lt;strong&gt;canonical bump&lt;/strong&gt;, and it is always the same for a given seed + program ID combination.&lt;/p&gt;

&lt;p&gt;The canonical bump is the only safe one to use. Anchor finds it for you during &lt;code&gt;init&lt;/code&gt; and Anchor also stores it for you (i.e. &lt;code&gt;bump&lt;/code&gt; in the constraint and &lt;code&gt;counter.bump&lt;/code&gt; in the struct). So that on subsequent instructions, you pass the stored bump back:&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;mut,&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&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="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;Storing and re-passing is important here also, because re-deriving the canonical bump every time costs compute. Storing it once at init and re-using it is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Picture
&lt;/h2&gt;

&lt;p&gt;Across the five days, this is how the counter account went through a complete lifecycle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derive&lt;/strong&gt; — &lt;code&gt;findProgramAddressSync&lt;/code&gt; on the client, same arithmetic in the program. Both sides agree on the address before any transaction is sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize&lt;/strong&gt; — &lt;code&gt;init&lt;/code&gt; allocates the account, sets aside the rent-exempt deposit (lamports — Solana's smallest unit of currency), and populates the fields. This is the moment the address goes from "a possible location" to "a live account."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutate&lt;/strong&gt; — subsequent &lt;code&gt;increment&lt;/code&gt; calls re-derive the address from the signer's key and reject the transaction at the constraint layer if anything does not match. No defensive code needed inside the handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Close&lt;/strong&gt; — Close the PDA account and reclaim rent. Day 67 added this, and it took one attribute:&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;mut,&lt;/span&gt;
    &lt;span class="nd"&gt;close&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;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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.bump&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&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="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;Where &lt;code&gt;close = user&lt;/code&gt; drains the lamports to the user's wallet and zeros the account data. When a Solana account's balance hits zero, the runtime removes it at the end of the transaction, without any separate delete syscall. A subsequent &lt;code&gt;getAccountInfo&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, which means that the rent deposit comes back in full, minus the transaction fee.&lt;/p&gt;

&lt;p&gt;NB: This is not "delete from a table." The account disappears from the account model entirely. There is no tombstone, no soft-delete flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Little Observations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The program ID is part of the derivation.&lt;/strong&gt; Same seeds with different program —&amp;gt; different address. A PDA is not portable across programs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDAs cannot sign transactions on their own.&lt;/strong&gt; Only the program can authorize writes to a PDA, by presenting the same seeds internally. This is called signing with "signer seeds" and it happens inside the program, not in the client transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed design is access control.&lt;/strong&gt; Including the user's pubkey in the seeds gives each wallet its own isolated namespace. Omitting it and every wallet shares one account. Both are valid choices, but you have to be deliberate. I saw this clearly when the spoof attempt where Wallet A trying to increment Wallet B's counter, and was rejected before the handler even ran, purely because the re-derived address did not match the supplied account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;init_if_needed&lt;/code&gt; is a footgun.&lt;/strong&gt; It initializes the account if it does not exist, silently does nothing if it does. That silent-do-nothing behavior is exactly the kind of thing that hides bugs in test suites. Reach for it deliberately and always test the already-initialized path explicitly.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Anchor Constraints as an Entire Auth Layer</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Mon, 22 Jun 2026 16:46:13 +0000</pubDate>
      <link>https://dev.to/russell_oje/anchor-constraints-as-an-entire-auth-layer-154h</link>
      <guid>https://dev.to/russell_oje/anchor-constraints-as-an-entire-auth-layer-154h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This week I made the jump from being a &lt;em&gt;consumer&lt;/em&gt; of Solana programs to being a &lt;em&gt;builder&lt;/em&gt; of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me walk you through what I built, what I tested, and the one experiment that made everything click.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Program: A Counter with an Owner
&lt;/h2&gt;

&lt;p&gt;The running example for the week was a counter program. Simple on the surface — one &lt;code&gt;Pubkey&lt;/code&gt; (the owner) and one &lt;code&gt;u64&lt;/code&gt; (the count) — but surprisingly rich in what it teaches.&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;#[derive(InitSpace)]&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;Counter&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;Pubkey&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;count&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;#[account]&lt;/code&gt; stamps an 8-byte discriminator onto the front of every serialised &lt;code&gt;Counter&lt;/code&gt;. That discriminator is how the program can later verify "yes, this account was created by me and nothing else." &lt;code&gt;#[derive(InitSpace)]&lt;/code&gt; auto-calculates the byte size so you don't have to count fields by hand. One macro, zero arithmetic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraints Are Like Middleware
&lt;/h2&gt;

&lt;p&gt;In a Web2 backend you might write a route guard like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authority_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Anchor, you write this instead:&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 the &lt;code&gt;increment&lt;/code&gt; handler runs, verify that the &lt;code&gt;authority&lt;/code&gt; field stored inside the &lt;code&gt;counter&lt;/code&gt; account on-chain matches the &lt;code&gt;authority&lt;/code&gt; key passed in this transaction. If they don't match, the transaction fails before your Rust code ever executes. You didn't write an &lt;code&gt;if&lt;/code&gt; statement. You declared a rule.&lt;/p&gt;

&lt;p&gt;Similarly, the &lt;code&gt;init&lt;/code&gt; constraint on &lt;code&gt;Initialize&lt;/code&gt; does the heavy lifting of creating the account:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One attribute makes a CPI to the System Program, allocates the right number of bytes, funds the rent from &lt;code&gt;authority&lt;/code&gt;'s wallet, assigns the account to your program, and refuses to run if the account already exists. You write one line. Anchor writes the boilerplate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing with LiteSVM
&lt;/h2&gt;

&lt;p&gt;Once the program writes state, you need a way to prove it works. In the earlier epochs, I observed that waiting for devnet confirmations was slow and flaky, which brings &lt;strong&gt;LiteSVM&lt;/strong&gt; into the picture, as it is an in-process Solana virtual machine that can run your compiled &lt;code&gt;.so&lt;/code&gt; binary against a fresh ledger in the same Rust test process.&lt;/p&gt;

&lt;p&gt;The happy-path test looks like a normal Rust unit test, but it executes a &lt;em&gt;real&lt;/em&gt; Solana transaction:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State even persists across transactions within one &lt;code&gt;LiteSVM&lt;/code&gt; instance — just like a real cluster — so a test that calls &lt;code&gt;initialize&lt;/code&gt; and then &lt;code&gt;increment&lt;/code&gt; is a genuine end-to-end simulation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Including Failure Tests
&lt;/h2&gt;

&lt;p&gt;A happy-path test proves the program &lt;em&gt;works&lt;/em&gt;. A failure test proves the program &lt;em&gt;refuses&lt;/em&gt;. Both are necessary.&lt;/p&gt;

&lt;p&gt;I wrote two:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Wrong authority is rejected&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;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 for a different signer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Double-initialization is rejected&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="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;first_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;"first initialize should succeed"&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;.expire_blockhash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// make the second tx genuinely new&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;second_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;"initializing the same counter twice should fail"&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;expire_blockhash()&lt;/code&gt; call is subtle but important. Without it, both transactions are byte-for-byte identical and LiteSVM rejects the second as a duplicate signature — the &lt;em&gt;wrong&lt;/em&gt; error. Expiring the blockhash ensures the failure comes from the &lt;code&gt;init&lt;/code&gt; constraint, not the duplicate check.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mutation Testing Experiment
&lt;/h2&gt;

&lt;p&gt;On friday I deliberately broke the program in three ways, one at a time, and watched the tests catch each regression:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug introduced&lt;/th&gt;
&lt;th&gt;Test that failed&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Removed &lt;code&gt;has_one = authority&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;increment_fails_when_wrong_authority_signs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constraint gone, unauthorized call now succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed &lt;code&gt;checked_add(1)&lt;/code&gt; to &lt;code&gt;checked_add(2)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;initialize_then_increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Count was 2, expected 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commented out &lt;code&gt;counter.authority = ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;initialize_then_increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On-chain authority was all-zeros; &lt;code&gt;has_one&lt;/code&gt; failed at increment time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third bug was the most instructive. The &lt;code&gt;initialize&lt;/code&gt; transaction itself succeeded, i.e. the account was created and rent was paid. The bug only surfaced at &lt;code&gt;increment&lt;/code&gt;, when the runtime compared the stored (all-zero) authority to the real signer and found a mismatch. The error pointed &lt;em&gt;downstream&lt;/em&gt; of the cause. The tests caught it; the logs explained it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Take Home
&lt;/h2&gt;

&lt;p&gt;Anchor constraints are not just convenience sugar. They are a &lt;strong&gt;declarative authorization layer&lt;/strong&gt; that runs before your handler, cannot be bypassed by a careless refactor, and produces clear error messages when violated. Combined with LiteSVM's in-process test runner and a suite that includes both happy-path and failure tests, you get a feedback loop that's fast enough to run on every save and trustworthy enough to catch real bugs.&lt;/p&gt;

&lt;p&gt;The mutation experiments this week weren't busywork. They were the proof. Every assertion that lit up red when I broke something is an assertion I now trust when it stays green.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>rust</category>
      <category>anchor</category>
      <category>web3</category>
    </item>
    <item>
      <title>Trilogy in Solana: Transfer Fee, Interest-Bearing, and Nontransferable</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Sun, 14 Jun 2026 23:18:09 +0000</pubDate>
      <link>https://dev.to/russell_oje/trilogy-in-solana-transfer-fee-interest-bearing-and-nontransferable-5ddk</link>
      <guid>https://dev.to/russell_oje/trilogy-in-solana-transfer-fee-interest-bearing-and-nontransferable-5ddk</guid>
      <description>&lt;p&gt;Token-2022 is the upgraded SPL token program on Solana, bringing native extensions that allow a single mint to opt into advanced behaviors without custom programs. This week, I explored three powerful extensions: Transfer Fees, Interest-Bearing tokens, and Non-Transferable tokens. Think of these extensions as middleware for your tokens—enforced at the protocol level.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Transfer Fee Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mint Address:&lt;/strong&gt; &lt;code&gt;FeoMWHjNJgJM6iEQUnUjsJsdPtP3Cu9m99iiuz9RhALG&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I configured this token to skim a fee on every transfer. This is perfect for protocol treasuries or community currencies where a small percentage of every transaction supports the ecosystem.&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;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkThT93YfkdY59uL35t4Z5Xl &lt;span class="nt"&gt;--transfer-fee&lt;/span&gt; 100 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command sets a 1% fee (100 basis points) with a maximum cap of 5000 lamports.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Interest-Bearing Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mint Address:&lt;/strong&gt; &lt;code&gt;6vZ9eXTTtGCpwRVLGg6uuyLXxk2yYEykroBTNybf6wT3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This extension allows a token to accrue interest over time. Crucially, it doesn't mint new supply; instead, the UI-displayed amount updates based on the on-chain rate.&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;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkThT93YfkdY59uL35t4Z5Xl &lt;span class="nt"&gt;--interest-rate&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: Remember that the rate is stored on-chain, but the principal balance remains the same until updated. It's a clever way to handle yield display natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Non-Transferable Mint&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mint Address:&lt;/strong&gt; &lt;code&gt;3PWCwuiPauNhYxwArDGaRizKLpJHETLS7RNRqHL1AQvm&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Finally, I built a "Soul-bound" token. Once minted to an account, it cannot be transferred or moved. I tested this by attempting a transfer and received a satisfyingly firm error: &lt;code&gt;Program log: Transfer is disabled for this mint: custom program error: 0x25&lt;/code&gt;.&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;--program-id&lt;/span&gt; TokenzQdBNbLqP5VEhdkThT93YfkdY59uL35t4Z5Xl &lt;span class="nt"&gt;--enable-non-transferable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Take Home&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;What caught my attention this week is how simple it is to compose these behaviors. Native extensions remove the need for "wrapped" versions of tokens to get simple features like fees or interest. I’m excited to see how these primitives will be used in real products for loyalty programs.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>beginners</category>
      <category>web3</category>
    </item>
    <item>
      <title>NFTs in Solana: Another Week with Token Extensions</title>
      <dc:creator>Russell Oje</dc:creator>
      <pubDate>Mon, 08 Jun 2026 18:11:03 +0000</pubDate>
      <link>https://dev.to/russell_oje/nfts-in-solana-another-week-with-token-extensions-2h5f</link>
      <guid>https://dev.to/russell_oje/nfts-in-solana-another-week-with-token-extensions-2h5f</guid>
      <description>&lt;p&gt;This week, I stepped away from the usual Extension system from last week, to creating NFTs, using only Solana's native Token Extensions (Token-2022). It turns out, you can build a fully functional, grouped, and mutable NFT collection entirely at the protocol level.&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%2Fjmdgirioo0h0z5zs7orh.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%2Fjmdgirioo0h0z5zs7orh.png" alt="Day 44 NFT" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Mental Model: What is a Solana NFT?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Coming from a Web2 background, I used to think of an NFT as a complex smart contract. On Solana, using Token Extensions, an NFT is simpler and more elegant: it's a token mint with &lt;strong&gt;supply 1&lt;/strong&gt;, &lt;strong&gt;decimals 0&lt;/strong&gt;, and a few specific extensions that handle metadata and grouping.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I Built This Week&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I spent the last few days moving from a single 1-of-1 token to a full-blown collection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The 1-of-1 Mint&lt;/strong&gt;: I started by creating a mint with the &lt;code&gt;Metadata&lt;/code&gt; extension, ensuring it had zero decimals and a supply of exactly one.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Metadata Extension&lt;/strong&gt;: Instead of an external metadata account, I stamped the name, symbol, and URI (pointing to a JSON on a GitHub Gist) directly onto the mint.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Group and Member Extensions&lt;/strong&gt;: I created a collection mint and then linked my individual NFTs to it using the &lt;code&gt;Group&lt;/code&gt; and &lt;code&gt;Member&lt;/code&gt; extensions. This creates a "foreign key" relationship natively on the blockchain.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Surprising Part: Live Mutation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The most eye-opening moment was realizing how "alive" this data is. As long as you hold the update authority, you can mutate the metadata in real-time with a single CLI command.&lt;/p&gt;

&lt;p&gt;On Friday, I renamed my NFT, updated the metadata's JSON uri (to update the image icon) and added custom fields like &lt;code&gt;rarity: legendary&lt;/code&gt; directly from my terminal:&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;# Adding a custom metadata field live on devnet&lt;/span&gt;
spl-token update-metadata &lt;span class="nv"&gt;$MINT_ADDRESS&lt;/span&gt; rarity &lt;span class="s2"&gt;"legendary"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watching Solana Explorer update the "legendary" tag seconds after my transaction confirmed felt like running an &lt;code&gt;UPDATE&lt;/code&gt; statement on a production database, but the "database" is a global public network.&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%2Fe8yuii8hnmktn8v5vp3w.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%2Fe8yuii8hnmktn8v5vp3w.png" alt="Update NFT" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Week Digest&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Using Token Extensions for NFTs feels like the "bare metal" way to build. It's fast, cost-effective, and removes the dependency on third-party programs for basic collection management. I’m excited to see how these primitives will be used to build more complex "programmable" NFTs that react to on-chain events.&lt;/p&gt;

&lt;p&gt;Follow along on my &lt;code&gt;#100DaysOfSolana&lt;/code&gt; journey!&lt;/p&gt;

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