<?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: 100 Days of Solana</title>
    <description>The latest articles on DEV Community by 100 Days of Solana (100daysofsolana).</description>
    <link>https://dev.to/100daysofsolana</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%2Forganization%2Fprofile_image%2F12963%2F62b6ce6d-db04-444b-88d1-4bea32484faa.png</url>
      <title>DEV Community: 100 Days of Solana</title>
      <link>https://dev.to/100daysofsolana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/100daysofsolana"/>
    <language>en</language>
    <item>
      <title>100 Days of Solana is a wrap!</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:33:05 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/100-days-of-solana-is-a-wrap-1hak</link>
      <guid>https://dev.to/100daysofsolana/100-days-of-solana-is-a-wrap-1hak</guid>
      <description>&lt;p&gt;Over the past 100 days, we’ve gone from the basics of wallets and transactions to deploying programs on Solana devnet.&lt;/p&gt;

&lt;p&gt;Some people completed every challenge. Others joined for a week, an arc, or whenever time allowed. However you took part, congratulations! You learned something new and built on Solana.&lt;/p&gt;

&lt;p&gt;And if you missed the live run, you haven’t missed your chance. All 100 challenges are still available, and you can &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;start from Day 1 whenever suits you&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is 100 Days of Solana?
&lt;/h2&gt;

&lt;p&gt;You may be curious about blockchain development but find that many tutorials assume you already understand the terminology, use cases, and other fundamentals.&lt;/p&gt;

&lt;p&gt;100 Days of Solana was designed to provide a clearer route in: from “I’m a web or mobile developer and this looks interesting” to “I can build and deploy something real on Solana.”&lt;/p&gt;

&lt;p&gt;Here’s how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 daily challenges&lt;/li&gt;
&lt;li&gt;Most challenges introduce one focused idea in around 10 to 20 minutes.&lt;/li&gt;
&lt;li&gt;Each week has a theme that introduces and reinforces a new concept.&lt;/li&gt;
&lt;li&gt;It’s free to take part.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the whole thing is designed so that you can still make progress if you complete just a couple or every challenge from a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we covered
&lt;/h2&gt;

&lt;p&gt;If you start at Day 1, here’s how the journey unfolds.&lt;/p&gt;

&lt;h3&gt;
  
  
  First: learn to speak Solana
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a wallet — a cryptographic keypair that acts as your identity — and send your first transactions&lt;/li&gt;
&lt;li&gt;Read live data directly from the network&lt;/li&gt;
&lt;li&gt;Get to grips with Solana’s account model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Next: create digital assets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create your own tokens and NFTs, which are built from accounts with clearly defined roles and rules&lt;/li&gt;
&lt;li&gt;Configure features such as transfer fees, interest, and transfer restrictions&lt;/li&gt;
&lt;li&gt;See how your Solana program enforces those rules on every transfer&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Then: write your own programs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Learn the basics of Solana programs&lt;/li&gt;
&lt;li&gt;Learn enough Rust to build with Anchor, a framework that provides some of the structure Rails or Express brings to Web2 development&lt;/li&gt;
&lt;li&gt;Design state with program-derived addresses, or PDAs: predictable addresses calculated from known values&lt;/li&gt;
&lt;li&gt;Call other programs in much the same way that one service calls another service’s API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  After that: think like an attacker
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Treat every input as hostile, because anyone on a public network can send instructions to your program&lt;/li&gt;
&lt;li&gt;Audit your code and write tests that actively try to break it&lt;/li&gt;
&lt;li&gt;Use property testing and fuzzing to find edge cases you might never write by hand&lt;/li&gt;
&lt;li&gt;Reproduce a real Solana exploit, understand why it worked, and then fix the vulnerability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Finally: ship what you built
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deploy to devnet, Solana’s free staging environment&lt;/li&gt;
&lt;li&gt;Publish your program’s interface, roughly the on-chain equivalent of an OpenAPI schema&lt;/li&gt;
&lt;li&gt;Build a frontend that connects to a user’s wallet and asks them to approve transactions while their private keys remain under their control&lt;/li&gt;
&lt;li&gt;Give an AI agent tightly limited access to a wallet through a deny-by-default policy layer&lt;/li&gt;
&lt;li&gt;Bring everything together in a capstone project of your own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By Day 100, you’ll have gone from creating your first wallet to deploying your own program, with an understanding of the major layers in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shout-outs
&lt;/h2&gt;

&lt;p&gt;One of the best things about 100 Days of Solana was the community that formed around it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/t/100daysofsolana"&gt;People published hundreds of posts here on DEV&lt;/a&gt;, while the event Discord became a place to compare approaches, untangle tooling problems, review code, and celebrate first deployments.&lt;/p&gt;

&lt;p&gt;If you’re starting the challenges now, those posts are a valuable companion along the way. Whatever error you’re staring at, there’s a good chance someone has already encountered it and written about what they learned.&lt;/p&gt;

&lt;p&gt;Thank you to everyone who wrote, demoed, answered a question, or reviewed someone else’s code. You made 100 Days of Solana far better than the challenges could have been on their own.&lt;/p&gt;

&lt;p&gt;And a special thank you to Vincent Jande, a graduate of MLH’s Solana Fellows program, who helped reivew and create content throughout the 100 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue your journey
&lt;/h2&gt;

&lt;p&gt;Whether you’ve just shipped your first Solana project at the end of the 100 days or you’re about to begin day 1, there are several ways to keep going:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://solana.com/developers/bootcamp" rel="noopener noreferrer"&gt;Solana Developer Bootcamp&lt;/a&gt;&lt;/strong&gt; — a free, project-based video course from the Solana team for developers who want to go deeper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ghw.mlh.com/" rel="noopener noreferrer"&gt;Global Hack Week&lt;/a&gt;&lt;/strong&gt; — MLH’s free monthly online hackathon, where you can use your Solana skills in a new project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mlh.io" rel="noopener noreferrer"&gt;MLH events&lt;/a&gt;&lt;/strong&gt; — find an in-person or digital hackathon and build alongside other developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scheduled 100 days may be over but there's always more to explore.&lt;/p&gt;

&lt;p&gt;Happy hacking!&lt;/p&gt;

</description>
      <category>web3</category>
      <category>blockchain</category>
      <category>100daysofsolana</category>
      <category>solana</category>
    </item>
    <item>
      <title>Arc 14 Catch-up: Agentic Solana</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:59:11 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-14-catch-up-agentic-solana-4ap2</link>
      <guid>https://dev.to/100daysofsolana/arc-14-catch-up-agentic-solana-4ap2</guid>
      <description>&lt;p&gt;An AI agent with access to a wallet sounds powerful.&lt;/p&gt;

&lt;p&gt;It also sounds like it could be risky.&lt;/p&gt;

&lt;p&gt;A language model can misunderstand a request, choose the wrong tool, repeat an action, or confidently pursue a goal that should never have been allowed in the first place. Giving it unrestricted signing authority would turn those mistakes into transactions.&lt;/p&gt;

&lt;p&gt;Arc 14 was about building something more useful than that.&lt;/p&gt;

&lt;p&gt;Across Days 92–98, we gave an agent access to Solana data, then gradually introduced tools, a wallet, an MCP server, a policy engine and an autonomous workflow.&lt;/p&gt;

&lt;p&gt;At every stage, the same boundary held:&lt;/p&gt;

&lt;p&gt;The model could decide what it wanted to do.&lt;/p&gt;

&lt;p&gt;Code decided what it was allowed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent began with read-only tools
&lt;/h2&gt;

&lt;p&gt;The first challenge kept the stakes low.&lt;/p&gt;

&lt;p&gt;We built a small agent loop using Claude and a set of Solana devnet tools. The agent could answer natural-language questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much SOL does this wallet hold?&lt;/li&gt;
&lt;li&gt;Who owns this account?&lt;/li&gt;
&lt;li&gt;Is this account executable?&lt;/li&gt;
&lt;li&gt;How much data does it store?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model never connected to Solana directly.&lt;/p&gt;

&lt;p&gt;It did not construct arbitrary RPC requests or receive unrestricted network access. The application defined a small set of tools, described what each one did, and decided how to execute them.&lt;/p&gt;

&lt;p&gt;Claude could inspect those descriptions and choose which tool to call, but the surrounding application remained in control.&lt;/p&gt;

&lt;p&gt;The result still felt conversational.&lt;/p&gt;

&lt;p&gt;We asked a question in plain English.&lt;/p&gt;

&lt;p&gt;The agent selected a balance tool.&lt;/p&gt;

&lt;p&gt;The application called the RPC endpoint.&lt;/p&gt;

&lt;p&gt;The result went back to the model.&lt;/p&gt;

&lt;p&gt;The model explained it to us.&lt;/p&gt;

&lt;p&gt;Behind that interaction was a simple loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user gives the agent a goal or question.&lt;/li&gt;
&lt;li&gt;The model decides whether it needs a tool.&lt;/li&gt;
&lt;li&gt;The application validates and executes the tool call.&lt;/li&gt;
&lt;li&gt;The result goes back to the model.&lt;/li&gt;
&lt;li&gt;The loop continues until the model can answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model supplied the judgement.&lt;/p&gt;

&lt;p&gt;The application supplied the capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool definitions shaped what the agent could do
&lt;/h2&gt;

&lt;p&gt;Each tool had four important parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A description telling the model when it should be used.&lt;/li&gt;
&lt;li&gt;An input schema defining what the model could request.&lt;/li&gt;
&lt;li&gt;An implementation deciding what actually happened.&lt;/li&gt;
&lt;li&gt;An output giving the model evidence for its next step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those details mattered.&lt;/p&gt;

&lt;p&gt;A vague description could lead the model to choose the wrong tool.&lt;/p&gt;

&lt;p&gt;A loose schema could allow malformed or ambiguous requests.&lt;/p&gt;

&lt;p&gt;An implementation that trusted every argument could turn a model mistake into a system problem.&lt;/p&gt;

&lt;p&gt;Even the read-only tools needed boundaries.&lt;/p&gt;

&lt;p&gt;A balance tool should accept a valid Solana address.&lt;/p&gt;

&lt;p&gt;An account-inspection tool should return useful structured metadata.&lt;/p&gt;

&lt;p&gt;Errors should come back in a form the model could reason about without hiding the original detail from the developer.&lt;/p&gt;

&lt;p&gt;The prompt described how we wanted the agent to behave.&lt;/p&gt;

&lt;p&gt;The tool definitions established what it could actually ask the application to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we gave the agent a wallet
&lt;/h2&gt;

&lt;p&gt;Day 93 raised the stakes.&lt;/p&gt;

&lt;p&gt;The agent received its own devnet wallet and two new abilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check its balance.&lt;/li&gt;
&lt;li&gt;Send SOL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turned it from a read-only assistant into a system capable of changing on-chain state.&lt;/p&gt;

&lt;p&gt;The wallet belonged to the application, not to the model.&lt;/p&gt;

&lt;p&gt;Its private key remained inside the process that built and signed transactions. It was never inserted into the prompt, returned in a tool result or exposed through the model context.&lt;/p&gt;

&lt;p&gt;The model did not need the key.&lt;/p&gt;

&lt;p&gt;It only needed a tool that accepted a proposed recipient and amount.&lt;/p&gt;

&lt;p&gt;The application could then decide whether to build and sign the transaction.&lt;/p&gt;

&lt;p&gt;This separation matters because model context is not a safe place for secrets.&lt;/p&gt;

&lt;p&gt;Prompts, tool calls, logs and responses may be stored, inspected or passed between components. A private key placed there could be leaked accidentally or repeated in generated output.&lt;/p&gt;

&lt;p&gt;Keeping the key inside the signing layer meant the model could request an action without possessing the credential required to perform it.&lt;/p&gt;

&lt;p&gt;The agent could ask to send 0.05 SOL to an address.&lt;/p&gt;

&lt;p&gt;Only the application could turn that request into a signed transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transfer limit lived in code
&lt;/h2&gt;

&lt;p&gt;We did not rely on the prompt to limit how much SOL the agent could send.&lt;/p&gt;

&lt;p&gt;The transfer tool enforced a hard cap.&lt;/p&gt;

&lt;p&gt;If the proposed amount exceeded that limit, the application rejected the request before building the transaction.&lt;/p&gt;

&lt;p&gt;We could have told the model never to send more than 0.1 SOL. It would probably follow that instruction most of the time.&lt;/p&gt;

&lt;p&gt;But a prompt can be misunderstood, overridden by conflicting instructions or applied inconsistently.&lt;/p&gt;

&lt;p&gt;A code-level check behaves differently.&lt;/p&gt;

&lt;p&gt;If the amount is too high, the transfer does not happen.&lt;/p&gt;

&lt;p&gt;It does not matter how persuasive the request is.&lt;/p&gt;

&lt;p&gt;It does not matter how confident the model sounds.&lt;/p&gt;

&lt;p&gt;It does not matter whether the proposed action appears to support the wider goal.&lt;/p&gt;

&lt;p&gt;The signing layer refuses.&lt;/p&gt;

&lt;p&gt;That is where rules involving money, permissions and irreversible actions belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  One transfer cap was not enough
&lt;/h2&gt;

&lt;p&gt;A per-transfer limit protects against one large transaction.&lt;/p&gt;

&lt;p&gt;It does not stop an agent from making many smaller ones.&lt;/p&gt;

&lt;p&gt;An agent limited to 0.1 SOL per transfer could still attempt ten transfers and spend 1 SOL overall.&lt;/p&gt;

&lt;p&gt;That showed why validation inside the transfer tool was only the beginning.&lt;/p&gt;

&lt;p&gt;The wider system also needed to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which recipients were allowed?&lt;/li&gt;
&lt;li&gt;How much could be sent in one transaction?&lt;/li&gt;
&lt;li&gt;How much could be spent across the whole session?&lt;/li&gt;
&lt;li&gt;How many tool calls could the agent make?&lt;/li&gt;
&lt;li&gt;How long could the loop continue?&lt;/li&gt;
&lt;li&gt;What should happen after a failed transaction?&lt;/li&gt;
&lt;li&gt;Which actions needed to be logged?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those decisions belonged in a dedicated policy layer rather than being scattered through prompts and tool implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP made the tools reusable
&lt;/h2&gt;

&lt;p&gt;Day 94 moved the Solana tools into a Model Context Protocol server.&lt;/p&gt;

&lt;p&gt;Until then, the tools had lived inside one application.&lt;/p&gt;

&lt;p&gt;That worked, but it tied the capabilities to a particular agent implementation.&lt;/p&gt;

&lt;p&gt;An MCP server made them reusable.&lt;/p&gt;

&lt;p&gt;It exposed tools for tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading a wallet balance.&lt;/li&gt;
&lt;li&gt;Inspecting on-chain vault state.&lt;/li&gt;
&lt;li&gt;Calling guarded program instructions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any compatible AI client could discover and use those tools through the same protocol.&lt;/p&gt;

&lt;p&gt;The server described what was available, accepted structured requests and returned structured results.&lt;/p&gt;

&lt;p&gt;This was similar to the role the IDL played in Arc 13.&lt;/p&gt;

&lt;p&gt;The IDL made a Solana program discoverable to software.&lt;/p&gt;

&lt;p&gt;MCP made agent-facing capabilities discoverable to AI clients.&lt;/p&gt;

&lt;p&gt;The useful part was not simply that another client could call the tools.&lt;/p&gt;

&lt;p&gt;The wallet key and safety rules stayed on the server.&lt;/p&gt;

&lt;p&gt;A new client did not receive direct access to either.&lt;/p&gt;

&lt;p&gt;It received the same controlled interface as every other client.&lt;/p&gt;

&lt;p&gt;That meant we could change the model, desktop client or agent framework without rebuilding the Solana integration or moving authority into the new client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server remained the trust boundary
&lt;/h2&gt;

&lt;p&gt;MCP made tools easier to discover and reuse.&lt;/p&gt;

&lt;p&gt;It did not make every client trustworthy.&lt;/p&gt;

&lt;p&gt;A compatible client could request a tool call, but the server still had to validate it.&lt;/p&gt;

&lt;p&gt;The client might be misconfigured.&lt;/p&gt;

&lt;p&gt;The model might choose the wrong tool.&lt;/p&gt;

&lt;p&gt;A user might request an action outside the intended scope.&lt;/p&gt;

&lt;p&gt;A request might contain an invalid address or an excessive amount.&lt;/p&gt;

&lt;p&gt;The server remained responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validating tool inputs.&lt;/li&gt;
&lt;li&gt;Applying policy checks.&lt;/li&gt;
&lt;li&gt;Protecting private keys.&lt;/li&gt;
&lt;li&gt;Building and signing transactions.&lt;/li&gt;
&lt;li&gt;Returning useful errors.&lt;/li&gt;
&lt;li&gt;Recording what happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool results were untrusted too.&lt;/p&gt;

&lt;p&gt;On-chain text and metadata can be written by other people. A memo, token name or decoded account field returned to the model could contain instructions intended to influence its behaviour.&lt;/p&gt;

&lt;p&gt;The application could not treat that content as trusted guidance merely because it came back from a tool.&lt;/p&gt;

&lt;p&gt;Even if hostile data changed the model’s reasoning, the same policy rules still controlled what could be signed. A poisoned response could affect what the model proposed, but it could not expand the allowlist, raise the spending cap or reset the session budget.&lt;/p&gt;

&lt;p&gt;That carried forward the security lesson from Arc 12.&lt;/p&gt;

&lt;p&gt;Untrusted input is not limited to transaction accounts. It also includes the data an agent reads and brings into its context.&lt;/p&gt;

&lt;h2&gt;
  
  
  We built a deny-by-default policy engine
&lt;/h2&gt;

&lt;p&gt;Day 95 turned the simple transfer cap into a proper policy layer.&lt;/p&gt;

&lt;p&gt;A proposed transfer was denied unless it passed every rule.&lt;/p&gt;

&lt;p&gt;The policy checked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the recipient was a valid Solana address.&lt;/li&gt;
&lt;li&gt;Whether the recipient appeared on an allowlist.&lt;/li&gt;
&lt;li&gt;Whether the amount stayed below the per-transfer cap.&lt;/li&gt;
&lt;li&gt;Whether the total session spend remained within budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application tracked cumulative spend outside the model context.&lt;/p&gt;

&lt;p&gt;The agent did not get to remember, reset or reinterpret its own remaining budget. The policy engine calculated that from application-owned session state.&lt;/p&gt;

&lt;p&gt;This was a deny-by-default design.&lt;/p&gt;

&lt;p&gt;The system did not search for a reason to reject the transfer.&lt;/p&gt;

&lt;p&gt;It required the transfer to satisfy every condition for approval.&lt;/p&gt;

&lt;p&gt;If any check failed, the transaction was not signed.&lt;/p&gt;

&lt;p&gt;A missing allowlist entry resulted in denial.&lt;/p&gt;

&lt;p&gt;A malformed address resulted in denial.&lt;/p&gt;

&lt;p&gt;An amount above the cap resulted in denial.&lt;/p&gt;

&lt;p&gt;An exhausted session budget resulted in denial.&lt;/p&gt;

&lt;p&gt;Unexpected input did not fall through into execution.&lt;/p&gt;

&lt;p&gt;It stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent and permission were separate questions
&lt;/h2&gt;

&lt;p&gt;The model’s job was to reason about the goal.&lt;/p&gt;

&lt;p&gt;The policy engine’s job was to decide whether a proposed action fitted the rules.&lt;/p&gt;

&lt;p&gt;The agent might correctly calculate that a wallet was short by 0.4 SOL.&lt;/p&gt;

&lt;p&gt;That did not mean it had permission to transfer 0.4 SOL.&lt;/p&gt;

&lt;p&gt;The amount might exceed the per-transfer cap.&lt;/p&gt;

&lt;p&gt;The destination might not be allowlisted.&lt;/p&gt;

&lt;p&gt;The remaining session budget might be only 0.2 SOL.&lt;/p&gt;

&lt;p&gt;The correct outcome could therefore be a refusal even when the model’s reasoning was sound.&lt;/p&gt;

&lt;p&gt;That was an important property of the system.&lt;/p&gt;

&lt;p&gt;Its safety did not depend on the model making the right security decision every time.&lt;/p&gt;

&lt;p&gt;The model could be mistaken, overconfident, manipulated by the user or influenced by hostile tool output.&lt;/p&gt;

&lt;p&gt;The policy should produce the same result for the same proposed action and the same session state.&lt;/p&gt;

&lt;p&gt;Model behaviour was variable.&lt;/p&gt;

&lt;p&gt;Policy behaviour was meant to be predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Denials were normal system behaviour
&lt;/h2&gt;

&lt;p&gt;A rejected action was not necessarily an error.&lt;/p&gt;

&lt;p&gt;Sometimes it was the system working exactly as designed.&lt;/p&gt;

&lt;p&gt;If the agent proposed sending funds to an address outside the allowlist, the policy engine denied the request and returned the reason.&lt;/p&gt;

&lt;p&gt;The agent could then decide what to do next.&lt;/p&gt;

&lt;p&gt;It might tell the user that the action was not permitted.&lt;/p&gt;

&lt;p&gt;It might ask for a different recipient.&lt;/p&gt;

&lt;p&gt;It might propose a smaller transfer.&lt;/p&gt;

&lt;p&gt;It might conclude that the goal could not be completed under the current limits.&lt;/p&gt;

&lt;p&gt;This was better than treating every denial as an exception that crashed the workflow.&lt;/p&gt;

&lt;p&gt;The policy result became information the agent could reason over.&lt;/p&gt;

&lt;p&gt;Approved actions could proceed to signing.&lt;/p&gt;

&lt;p&gt;Denied actions came back with a structured explanation.&lt;/p&gt;

&lt;p&gt;The agent could adapt to that decision, but it could not override it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The autonomous workflow brought everything together
&lt;/h2&gt;

&lt;p&gt;Day 96 combined the agent loop, wallet tools and policy engine in a goal-driven workflow.&lt;/p&gt;

&lt;p&gt;The example goal was to make sure a savings wallet held at least a specified amount of SOL.&lt;/p&gt;

&lt;p&gt;The agent needed to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the savings wallet balance.&lt;/li&gt;
&lt;li&gt;Compare it with the target.&lt;/li&gt;
&lt;li&gt;Calculate any shortfall.&lt;/li&gt;
&lt;li&gt;Check the funding wallet.&lt;/li&gt;
&lt;li&gt;Propose a transfer.&lt;/li&gt;
&lt;li&gt;Pass the proposal through policy.&lt;/li&gt;
&lt;li&gt;Submit an approved transaction.&lt;/li&gt;
&lt;li&gt;Verify the new balance.&lt;/li&gt;
&lt;li&gt;Record what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was more than one tool call followed by an answer.&lt;/p&gt;

&lt;p&gt;The agent had to inspect the current state, decide whether action was needed, act within its limits and then check the result.&lt;/p&gt;

&lt;p&gt;That final verification mattered.&lt;/p&gt;

&lt;p&gt;A transaction signature shows that a transaction was submitted.&lt;/p&gt;

&lt;p&gt;It does not by itself prove that the intended goal was reached.&lt;/p&gt;

&lt;p&gt;The agent read the on-chain state again and compared it with the target.&lt;/p&gt;

&lt;p&gt;That closed the loop:&lt;/p&gt;

&lt;p&gt;Observe.&lt;/p&gt;

&lt;p&gt;Reason.&lt;/p&gt;

&lt;p&gt;Act.&lt;/p&gt;

&lt;p&gt;Verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normal, constrained and impossible scenarios behaved differently
&lt;/h2&gt;

&lt;p&gt;We tested the workflow under several conditions.&lt;/p&gt;

&lt;p&gt;In the normal case, the target wallet was below its minimum balance, the funding wallet had enough SOL, and the required transfer passed policy.&lt;/p&gt;

&lt;p&gt;The agent calculated the shortfall, requested the transfer and verified the new balance.&lt;/p&gt;

&lt;p&gt;In a constrained case, the goal was achievable only within the transfer cap and remaining session budget.&lt;/p&gt;

&lt;p&gt;The agent had to work within those limits rather than simply choosing the most direct action.&lt;/p&gt;

&lt;p&gt;In an impossible case, the policy or available funds made the target unreachable.&lt;/p&gt;

&lt;p&gt;The recipient might not be allowlisted.&lt;/p&gt;

&lt;p&gt;The shortfall might exceed the session budget.&lt;/p&gt;

&lt;p&gt;The funding wallet might not contain enough SOL.&lt;/p&gt;

&lt;p&gt;The agent could reason correctly and still be unable to complete the goal.&lt;/p&gt;

&lt;p&gt;That was not a failure of the system.&lt;/p&gt;

&lt;p&gt;The correct behaviour was to stop, explain the constraint and preserve the policy boundary.&lt;/p&gt;

&lt;p&gt;“Unable to proceed” is sometimes the right result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn limits prevented endless loops
&lt;/h2&gt;

&lt;p&gt;An agent loop needs a stopping condition.&lt;/p&gt;

&lt;p&gt;Without one, the model could continue calling tools, retrying failed actions or revisiting the same reasoning indefinitely.&lt;/p&gt;

&lt;p&gt;Arc 14 included explicit turn limits so the application could stop the workflow after a fixed number of steps.&lt;/p&gt;

&lt;p&gt;That protected against accidental loops and limited the amount of time, tokens and network activity one request could consume.&lt;/p&gt;

&lt;p&gt;A turn limit is a simple control, but it addresses a real feature of agent systems.&lt;/p&gt;

&lt;p&gt;The model decided what to do next.&lt;/p&gt;

&lt;p&gt;The application decided how long it was allowed to keep deciding.&lt;/p&gt;

&lt;p&gt;Other systems might also use time limits, tool-call limits or approval gates for particular actions.&lt;/p&gt;

&lt;p&gt;The exact control can vary.&lt;/p&gt;

&lt;p&gt;The important part is that the agent cannot grant itself unlimited time or unlimited attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured logs made the workflow inspectable
&lt;/h2&gt;

&lt;p&gt;Autonomous behaviour is hard to trust when all we can see is the final answer.&lt;/p&gt;

&lt;p&gt;The workflow therefore wrote structured logs for each important step.&lt;/p&gt;

&lt;p&gt;Those logs could record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The goal the agent received.&lt;/li&gt;
&lt;li&gt;Which tool it selected.&lt;/li&gt;
&lt;li&gt;The arguments it proposed.&lt;/li&gt;
&lt;li&gt;The relevant policy decision.&lt;/li&gt;
&lt;li&gt;Why an action was approved or denied.&lt;/li&gt;
&lt;li&gt;The transaction signature.&lt;/li&gt;
&lt;li&gt;The result of the verification step.&lt;/li&gt;
&lt;li&gt;The final outcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This created a trace of how the system moved from the original goal to the result.&lt;/p&gt;

&lt;p&gt;The logs were useful for debugging, but their value went further.&lt;/p&gt;

&lt;p&gt;They showed whether the agent had interpreted the goal correctly.&lt;/p&gt;

&lt;p&gt;They revealed whether the policy engine had applied the expected rule.&lt;/p&gt;

&lt;p&gt;They made repeated actions visible.&lt;/p&gt;

&lt;p&gt;They provided evidence that a successful workflow had verified its work.&lt;/p&gt;

&lt;p&gt;They also made denials easier to explain.&lt;/p&gt;

&lt;p&gt;Without logs, we might know only that the transfer did not happen.&lt;/p&gt;

&lt;p&gt;With logs, we could see that the recipient was not on the allowlist or that the remaining budget was too low.&lt;/p&gt;

&lt;p&gt;Autonomous systems need this kind of evidence.&lt;/p&gt;

&lt;p&gt;A final response is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation forced us to describe the boundary
&lt;/h2&gt;

&lt;p&gt;Day 97 moved from building the system to explaining it.&lt;/p&gt;

&lt;p&gt;We wrote a public technical walkthrough covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent loop.&lt;/li&gt;
&lt;li&gt;The available tools.&lt;/li&gt;
&lt;li&gt;The MCP server.&lt;/li&gt;
&lt;li&gt;The wallet boundary.&lt;/li&gt;
&lt;li&gt;The policy engine.&lt;/li&gt;
&lt;li&gt;The tool contracts.&lt;/li&gt;
&lt;li&gt;The overall architecture.&lt;/li&gt;
&lt;li&gt;Logs from a real run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important distinction was between variable and invariant behaviour.&lt;/p&gt;

&lt;p&gt;The model’s reasoning was variable.&lt;/p&gt;

&lt;p&gt;It might choose different tools, phrase its explanation differently or take another path towards the same goal.&lt;/p&gt;

&lt;p&gt;The policy layer was invariant.&lt;/p&gt;

&lt;p&gt;The same proposed action and session state should produce the same approval decision regardless of how the model explained its intention.&lt;/p&gt;

&lt;p&gt;That made the architecture easier to understand.&lt;/p&gt;

&lt;p&gt;The agent was not safe because the model had been instructed to behave.&lt;/p&gt;

&lt;p&gt;It was safe because the model could reach the signing capability only through code that enforced fixed rules.&lt;/p&gt;

&lt;p&gt;The documentation needed to show exactly where that boundary lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool contracts mattered as much as prompts
&lt;/h2&gt;

&lt;p&gt;A public walkthrough of an agent system can easily become a long explanation of prompting.&lt;/p&gt;

&lt;p&gt;Prompts mattered, but they were only one part of this arc.&lt;/p&gt;

&lt;p&gt;The stronger material was in the tool contracts and policy rules.&lt;/p&gt;

&lt;p&gt;Which fields did a transfer request require?&lt;/p&gt;

&lt;p&gt;Which validation happened before policy?&lt;/p&gt;

&lt;p&gt;Which policy result came back after denial?&lt;/p&gt;

&lt;p&gt;Where did the private key live?&lt;/p&gt;

&lt;p&gt;Where was cumulative session spend stored?&lt;/p&gt;

&lt;p&gt;Which component built the transaction?&lt;/p&gt;

&lt;p&gt;How did the workflow confirm success?&lt;/p&gt;

&lt;p&gt;How were tool calls and policy decisions logged?&lt;/p&gt;

&lt;p&gt;How was untrusted tool output kept from bypassing the signing rules?&lt;/p&gt;

&lt;p&gt;Those details explained what the system could actually guarantee.&lt;/p&gt;

&lt;p&gt;A prompt described intended behaviour.&lt;/p&gt;

&lt;p&gt;A tool contract defined the action the model could request.&lt;/p&gt;

&lt;p&gt;The policy engine defined which requests could proceed.&lt;/p&gt;

&lt;p&gt;The signer controlled what reached the network.&lt;/p&gt;

&lt;p&gt;The logs showed what occurred.&lt;/p&gt;

&lt;p&gt;That was the full safety story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final demo showed both action and refusal
&lt;/h2&gt;

&lt;p&gt;Day 98 turned the workflow into a short recorded demonstration.&lt;/p&gt;

&lt;p&gt;The demo began with a natural-language goal.&lt;/p&gt;

&lt;p&gt;The agent inspected balances, selected tools, proposed an action and passed it through policy.&lt;/p&gt;

&lt;p&gt;An approved transaction appeared on devnet and could be verified on-chain.&lt;/p&gt;

&lt;p&gt;But the demo also needed to show a denial.&lt;/p&gt;

&lt;p&gt;That was just as important as the successful transfer.&lt;/p&gt;

&lt;p&gt;A system that can move funds is easy to demonstrate.&lt;/p&gt;

&lt;p&gt;A system that can refuse an unsafe or disallowed request is more convincing.&lt;/p&gt;

&lt;p&gt;The denial showed that the policy engine remained in control even when the model proposed the action.&lt;/p&gt;

&lt;p&gt;The most useful demo therefore included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The original goal.&lt;/li&gt;
&lt;li&gt;The agent’s tool calls.&lt;/li&gt;
&lt;li&gt;The policy decision.&lt;/li&gt;
&lt;li&gt;The transaction signature for an approved action.&lt;/li&gt;
&lt;li&gt;The resulting on-chain state.&lt;/li&gt;
&lt;li&gt;A request rejected by policy.&lt;/li&gt;
&lt;li&gt;The reason for the rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made the boundaries visible rather than asking the audience to trust that they existed somewhere in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 14 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 14 was not about handing an AI model a wallet and hoping for the best.&lt;/p&gt;

&lt;p&gt;It was about building an agent inside a system that retained control.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build an agent loop around clearly defined Solana tools.&lt;/li&gt;
&lt;li&gt;Keep network access and private keys outside the model context.&lt;/li&gt;
&lt;li&gt;Treat tool results and on-chain data as untrusted input.&lt;/li&gt;
&lt;li&gt;Enforce transfer limits, allowlists and budgets in application code.&lt;/li&gt;
&lt;li&gt;Package reusable Solana capabilities in an MCP server.&lt;/li&gt;
&lt;li&gt;Apply the same safety rules across different AI clients.&lt;/li&gt;
&lt;li&gt;Run a workflow that observed, acted and verified.&lt;/li&gt;
&lt;li&gt;Track session state outside the model.&lt;/li&gt;
&lt;li&gt;Limit agent turns and record structured execution logs.&lt;/li&gt;
&lt;li&gt;Demonstrate both approved and denied actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central lesson was that autonomy and authority are not the same thing.&lt;/p&gt;

&lt;p&gt;The model can decide which tool might help.&lt;/p&gt;

&lt;p&gt;It can interpret a goal.&lt;/p&gt;

&lt;p&gt;It can calculate a shortfall.&lt;/p&gt;

&lt;p&gt;It can propose a transfer.&lt;/p&gt;

&lt;p&gt;It can explain a denial.&lt;/p&gt;

&lt;p&gt;But it does not hold the private key.&lt;/p&gt;

&lt;p&gt;It does not decide which recipients are permitted.&lt;/p&gt;

&lt;p&gt;It does not set its own spending cap.&lt;/p&gt;

&lt;p&gt;It does not remember or reset its own session budget.&lt;/p&gt;

&lt;p&gt;It does not override the policy engine.&lt;/p&gt;

&lt;p&gt;The model reasons.&lt;/p&gt;

&lt;p&gt;The code holds the authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 14 challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 92:&lt;/strong&gt; Build a read-only Solana agent that answers questions through defined tools&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 93:&lt;/strong&gt; Give the agent a devnet wallet while keeping its key and spending limits in code&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 94:&lt;/strong&gt; Package the Solana tools as a reusable MCP server&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 95:&lt;/strong&gt; Build a deny-by-default policy engine with allowlists and budgets&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 96:&lt;/strong&gt; Run and verify an autonomous wallet-management workflow&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 97:&lt;/strong&gt; Document the agent architecture, tool contracts, policy rules and run logs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 98:&lt;/strong&gt; Record a public demo showing both a successful action and a policy denial&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>blockchain</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Turning Solana Skills Into a Web3 Career</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Tue, 28 Jul 2026 18:05:35 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/turning-solana-skills-into-a-web3-career-3ok</link>
      <guid>https://dev.to/100daysofsolana/turning-solana-skills-into-a-web3-career-3ok</guid>
      <description>&lt;p&gt;There is a gap between being able to do technical work and being hired to do it. The first is a question of skill, and if you have spent months writing Anchor programs, deriving PDAs, making cross-program calls, and hardening code against real exploit classes, you have crossed that line already. The second is a question of evidence, and it is the part most self-taught developers underestimate.&lt;/p&gt;

&lt;p&gt;The reason is structural. Hiring managers and recruiters make decisions with very little information. They cannot inspect what you know, so they rely on proxies: a portfolio they can open, a profile they can read, a message that arrives in their inbox. A developer with moderate skill and excellent evidence will get interviews that a stronger developer with no visible work never hears about. This is not a comment on fairness, it is simply how the process works, and understanding it changes what you do next.&lt;/p&gt;

&lt;p&gt;This post covers two things: the roles that Solana development actually opens up, and the specific work of making your skills legible to the people who hire for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where these skills lead
&lt;/h2&gt;

&lt;p&gt;Solana engineering is not a single job. The work divides into a handful of directions that reward different temperaments, and job titles in this space are used loosely, so each path below includes the titles you are likely to see on a posting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart contract and protocol engineering.&lt;/strong&gt; Writing the on-chain programs themselves: designing account structures, writing instruction handlers, and implementing the logic that moves value. Protocols, DeFi teams, and infrastructure projects hire for this, and it rewards depth in Rust and Anchor more than breadth. An emerging specialization within it is agent-facing work, where software holds a wallet and transacts autonomously within limits a program enforces, and the people who understand both agent design and on-chain authorization are still few. Postings for this work usually read as Smart Contract Engineer, Protocol Engineer, Rust Engineer, or Blockchain Engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web3 full-stack engineering.&lt;/strong&gt; This covers the distance between a deployed program and the person using it: connecting frontends to programs, integrating wallets, and building the payment and consumer flows people actually touch. Teams often find the role harder to fill than pure protocol work, because it asks for someone comfortable on both sides of the boundary, and most candidates are strong on only one. The emphasis shifts toward reliability and user experience, since the people using these products are not developers and will not tolerate a confusing failure. Roles appear as Full-Stack Engineer, dApp Developer, Frontend Engineer, or Product Engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security engineering and auditing.&lt;/strong&gt; Reviewing programs for the vulnerability classes that account for most on-chain losses: owner and signer checks, arithmetic safety, account substitution, and authority confusion. These categories are learnable in a way that many security specialisms are not, and demand is steady because every protocol holding significant value needs review. Bug bounty work is the same skill applied independently, and it is worth knowing as an entry point, because a valid finding is valid regardless of who submits it, which lets you build a reputation before anyone has hired you. Titles here include Smart Contract Auditor, Security Engineer, and Security Researcher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure and developer tooling.&lt;/strong&gt; Running the validators and RPC services applications depend on, indexing chain data into something teams can query, and building the libraries and clients other developers use. This is closer to systems and data engineering than to program development, and it suits people who were more interested in how the network runs than in what gets deployed to it. Open source contributions here are unusually visible to employers. This work is posted as Infrastructure Engineer, Platform Engineer, Data Engineer, or Site Reliability Engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trading systems and on-chain automation.&lt;/strong&gt; Programmatic strategies that read chain state and act on it: market making, arbitrage, liquidation bots, and related searcher work. It rewards on-chain knowledge combined with disciplined off-chain engineering, and it is one of the few areas where independent results can substitute convincingly for employment history, because performance is measurable. Look for Quantitative Developer, Trading Systems Engineer, or Quant Engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer relations and technical education.&lt;/strong&gt; Writing documentation and tutorials, running workshops, and supporting developers building on a protocol. It requires genuine technical depth alongside the ability to explain things clearly, and teams value it because adoption depends on it. If you found that you enjoyed explaining what you were learning, that is a relevant signal. The common titles are Developer Advocate, Developer Relations Engineer, and Technical Writer.&lt;/p&gt;

&lt;p&gt;Employment is also not the only route. Ecosystem foundations fund independent builders through grants, hackathons run frequently and pay meaningfully, and some developers move directly from learning into building something of their own. That path is less predictable than a salary, but it compounds into exactly the portfolio that makes you employable if you later decide you want a role.&lt;/p&gt;

&lt;p&gt;These categories overlap more than the headings suggest. Auditors write programs to test their assumptions, tooling authors need protocol depth, and the strongest builders think adversarially about their own code. The useful takeaway is that the foundation you have built supports any of these directions, and you do not have to choose one permanently in order to start moving toward it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence: building a portfolio that does the work
&lt;/h2&gt;

&lt;p&gt;Once you know which direction interests you, the question becomes how to demonstrate it. The most effective format is a portfolio at a single URL, and the emphasis on the URL is deliberate. A collection of repositories scattered across a GitHub profile requires the reader to do investigative work, and they will not. One link that opens onto organized, described work removes that friction entirely.&lt;/p&gt;

&lt;p&gt;A few principles make the difference between a portfolio that gets read and one that does not.&lt;/p&gt;

&lt;p&gt;Prioritize things that run. A program deployed to mainnet or devnet, presented with a link to its page on an explorer, carries more weight than source code alone, because deployment is proof that the work reached completion. Placing the explorer link beside the repository lets a reviewer confirm in seconds what would otherwise take them ten minutes.&lt;/p&gt;

&lt;p&gt;Include your writing. If you have published explanations of what you built, whether tutorials, breakdowns, or notes on problems you solved, treat them as first-class portfolio items rather than afterthoughts. Technical writing demonstrates that you can communicate about your work, which is a substantial fraction of every engineering job and something most candidate portfolios fail to show.&lt;/p&gt;

&lt;p&gt;Write for a reader in a hurry. A hiring engineer typically spends under a minute on an initial review. Each project needs a one-line description of what it does, a link to see it working, and a link to the code. Lead with your strongest work, because the first item is the one most likely to be read.&lt;/p&gt;

&lt;p&gt;Collect it in one place. A simple personal site, even a single well-structured page, gives you one address to put on your profile, in applications, and in messages. That address is what converts a claim about your ability into something a stranger can verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discoverability: a profile that works when you are not
&lt;/h2&gt;

&lt;p&gt;A portfolio answers the question "is this person capable" for someone who has already found you. LinkedIn addresses the earlier problem of being found at all, since it remains where a large share of technical recruiting begins.&lt;/p&gt;

&lt;p&gt;Your headline carries the most weight, because it appears in search results and beside every message you send. Something specific, along the lines of "Solana and Rust engineer building on-chain programs," tells a recruiter what you are in a single glance. A vague or aspirational headline wastes the most valuable line on the page.&lt;/p&gt;

&lt;p&gt;Profile verification is worth completing. LinkedIn offers identity verification, and the resulting badge signals that you are a consistent, real person. In a field where recruiters encounter a high volume of anonymous and pseudonymous accounts, that signal quietly raises the baseline level of trust before anyone reads a word of your experience.&lt;/p&gt;

&lt;p&gt;Beyond that, the mechanics are straightforward: make the portfolio URL visible rather than buried, and describe your experience in terms of specific artifacts, the programs you shipped, the work you completed, the writing you published. Specific descriptions consistently outperform general ones, because they give a reader something concrete to ask about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approaching the market directly
&lt;/h2&gt;

&lt;p&gt;The step most developers skip is direct outreach, and it tends to be the highest-yield activity available to them. Applying through job boards places your application into a queue alongside hundreds of others, sorted by criteria you cannot see. Contacting a recruiter or an engineer directly puts you in a conversation.&lt;/p&gt;

&lt;p&gt;Effective outreach is short and specific. It states who you are, what you build, links to evidence, and gives a reason you are contacting that particular person or company. The reason matters, because it distinguishes your message from bulk outreach and demonstrates that you did some research. Recruiters in a growing sector are actively searching for qualified candidates, and a message arriving with a working portfolio attached does a meaningful part of their job for them.&lt;/p&gt;

&lt;p&gt;The two channels work best together. A strong profile makes you findable by people conducting searches, and deliberate outreach reaches the people who are not searching yet. Most early roles in this space come from one or the other, and often from the combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic view of the market
&lt;/h2&gt;

&lt;p&gt;The web3 job market is real and expanding, and it is also competitive, as any field worth entering tends to be. Learning the stack does not produce a job offer on its own; it produces the foundation and the raw material for evidence. What distinguishes the developers who find roles from those who do not is rarely raw technical ability at this stage, since candidates at similar experience levels tend to cluster closely. The difference is usually whether they made their work visible and initiated contact.&lt;/p&gt;

&lt;p&gt;That is worth sitting with, because it reframes what remains. The demanding part, becoming someone capable of doing the work, is the part you have been doing for months. Presenting it well is a smaller, more mechanical task, and it is entirely within your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;If you want a concrete first move, collect what you have already built into a single page with live links, then update your headline to describe what you do now rather than what you did before. Those two changes take an afternoon and make every subsequent step more effective.&lt;/p&gt;

&lt;p&gt;For roles, Solana's &lt;a href="https://jobs.solana.com/" rel="noopener noreferrer"&gt;ecosystem job board&lt;/a&gt; lists protocol, tooling, and application positions across the network. If the security path interests you, following audit firms and bounty platforms will show you both the standard of work expected and the openings as they appear.&lt;/p&gt;

&lt;p&gt;Whatever direction you choose, the most transferable thing you have built is not any single technical skill. It is the demonstrated ability to work at something difficult, consistently, over months. That is precisely what the work asks of you once someone is paying for it, and it is worth presenting as deliberately as everything else. &lt;code&gt;#100DaysOfSolana&lt;/code&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>learning</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Arc 13 Catch-up: Deployment</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:51:19 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-13-catch-up-deployment-495d</link>
      <guid>https://dev.to/100daysofsolana/arc-13-catch-up-deployment-495d</guid>
      <description>&lt;p&gt;A program can pass every local test and still be a long way from ready for real users.&lt;/p&gt;

&lt;p&gt;It needs to be deployed to the right network, with sensible controls over who can change it. Other developers need a reliable way to call it. Users need a frontend that can connect to their wallet and submit transactions. When those transactions fail, the app needs to explain what happened.&lt;/p&gt;

&lt;p&gt;Arc 13 covered that last stretch.&lt;/p&gt;

&lt;p&gt;Across Days 85–91, we deployed a program to mainnet-beta, secured its upgrade authority, published its interface, generated a typed client, built a wallet-connected frontend and improved the way it handled transaction failures.&lt;/p&gt;

&lt;p&gt;Then we made the launch public, with an on-chain address anyone could verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment changed the stakes
&lt;/h2&gt;

&lt;p&gt;Until Day 85, most of our work had taken place locally or on devnet.&lt;/p&gt;

&lt;p&gt;Those environments gave us room to experiment. We could redeploy, replace accounts, request test SOL and recover from mistakes without putting real assets at risk.&lt;/p&gt;

&lt;p&gt;Mainnet-beta is different.&lt;/p&gt;

&lt;p&gt;The program becomes public infrastructure. Clients may begin depending on its address and interface. Users can send transactions involving real assets. Any authority left attached to the program now controls production code.&lt;/p&gt;

&lt;p&gt;Mainnet deployment also costs real SOL. The deployment wallet needs enough to create and fund the on-chain program account, with the amount depending on the size of the program.&lt;/p&gt;

&lt;p&gt;The first challenge was therefore about more than learning another command.&lt;/p&gt;

&lt;p&gt;We built the program, configured the CLI for mainnet-beta, funded the deployment wallet and deployed the program to the live network.&lt;/p&gt;

&lt;p&gt;Once it was there, we verified the program ID through Explorer rather than assuming that a successful command meant everything was correct.&lt;/p&gt;

&lt;p&gt;That verification matters.&lt;/p&gt;

&lt;p&gt;A frontend configured with the wrong address may call an older deployment or fail entirely. Documentation pointing to the wrong program creates the same problem for every developer who follows it.&lt;/p&gt;

&lt;p&gt;The deployed address is the identity that clients and users will rely on.&lt;/p&gt;

&lt;p&gt;By the end of the challenge, we had something we had not had before: a public program running on mainnet-beta at an address anyone could inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mainnet deployment was an operational decision
&lt;/h2&gt;

&lt;p&gt;The Web2 comparison throughout the arc was moving a service from staging to production.&lt;/p&gt;

&lt;p&gt;That comparison holds up because deployment changes who can rely on the software and what a mistake can cost.&lt;/p&gt;

&lt;p&gt;A staging service can usually be reset.&lt;/p&gt;

&lt;p&gt;A broken development build can be replaced without affecting users.&lt;/p&gt;

&lt;p&gt;A public interface may already have clients depending on its exact behaviour.&lt;/p&gt;

&lt;p&gt;The deployment command is only one part of that transition.&lt;/p&gt;

&lt;p&gt;We also need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which wallet paid for the deployment?&lt;/li&gt;
&lt;li&gt;Who can replace the program code?&lt;/li&gt;
&lt;li&gt;Is the live program ID recorded correctly?&lt;/li&gt;
&lt;li&gt;Can another developer discover the interface?&lt;/li&gt;
&lt;li&gt;How will users submit transactions?&lt;/li&gt;
&lt;li&gt;What will they see when something fails?&lt;/li&gt;
&lt;li&gt;Is there verifiable evidence of what was deployed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions made Arc 13 feel less like another coding arc and more like the beginning of operating software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upgrade authority was a production credential
&lt;/h2&gt;

&lt;p&gt;When a Solana program is deployed, its upgrade authority initially belongs to the deployment wallet.&lt;/p&gt;

&lt;p&gt;That authority can replace the executable code while keeping the same program ID.&lt;/p&gt;

&lt;p&gt;This is useful during active development. Bugs can be fixed and new functionality added without forcing every client to move to another address.&lt;/p&gt;

&lt;p&gt;It is also a significant amount of power.&lt;/p&gt;

&lt;p&gt;Anyone who controls the upgrade authority can change what the program does.&lt;/p&gt;

&lt;p&gt;That makes it closer to a production credential than a normal development key.&lt;/p&gt;

&lt;p&gt;On Day 86, we inspected the authority attached to our program, transferred it to another key and then transferred it back.&lt;/p&gt;

&lt;p&gt;The exercise made the relationship concrete.&lt;/p&gt;

&lt;p&gt;The program might be visible at a public address, but whoever holds the upgrade authority can still replace the code behind that address.&lt;/p&gt;

&lt;p&gt;Leaving it attached to a personal wallet creates a single point of failure.&lt;/p&gt;

&lt;p&gt;The key could be lost or exposed.&lt;/p&gt;

&lt;p&gt;Its owner could become unavailable.&lt;/p&gt;

&lt;p&gt;A mistaken command could transfer authority to the wrong address.&lt;/p&gt;

&lt;p&gt;For a real production program, the safer approach is usually to place upgrade authority behind a multisig such as Squads.&lt;/p&gt;

&lt;p&gt;An upgrade can then require approval from several authorised people rather than depending on one private key.&lt;/p&gt;

&lt;p&gt;That also gives the team a clearer release process.&lt;/p&gt;

&lt;p&gt;Who proposed the upgrade?&lt;/p&gt;

&lt;p&gt;Who reviewed it?&lt;/p&gt;

&lt;p&gt;How many approvals are required?&lt;/p&gt;

&lt;p&gt;Which build is being deployed?&lt;/p&gt;

&lt;p&gt;A multisig does not guarantee that an upgrade is correct, but it reduces the chance that one compromised wallet or accidental action can change the program.&lt;/p&gt;

&lt;h2&gt;
  
  
  We could also remove the upgrade authority
&lt;/h2&gt;

&lt;p&gt;The other option was to make the program immutable.&lt;/p&gt;

&lt;p&gt;Removing the upgrade authority with &lt;code&gt;--final&lt;/code&gt; permanently prevents the program from being upgraded.&lt;/p&gt;

&lt;p&gt;That can be a strong guarantee for users. The code they inspected cannot later be replaced by an authority holder.&lt;/p&gt;

&lt;p&gt;But the decision cannot be reversed.&lt;/p&gt;

&lt;p&gt;If a vulnerability appears, the program cannot be patched at its existing address. A replacement would need to be deployed, and clients and users would need to move to it.&lt;/p&gt;

&lt;p&gt;That makes immutability a governance and operational decision rather than a final bit of deployment housekeeping.&lt;/p&gt;

&lt;p&gt;Before removing the authority, a team needs confidence in the code, the tests, the interface and the plan for handling future problems.&lt;/p&gt;

&lt;p&gt;For most actively developed programs, moving the authority to a well-managed multisig is a more practical first step.&lt;/p&gt;

&lt;p&gt;Immutability may come later, once the inability to change the program is a feature rather than a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  A program ID was not enough
&lt;/h2&gt;

&lt;p&gt;Deploying the program made it available, but it did not make it easy to use.&lt;/p&gt;

&lt;p&gt;A program address tells us where the code lives.&lt;/p&gt;

&lt;p&gt;It does not tell another developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which instructions the program exposes.&lt;/li&gt;
&lt;li&gt;Which arguments those instructions accept.&lt;/li&gt;
&lt;li&gt;Which accounts each instruction requires.&lt;/li&gt;
&lt;li&gt;Which accounts must sign.&lt;/li&gt;
&lt;li&gt;Which accounts must be writable.&lt;/li&gt;
&lt;li&gt;How program-owned account data is structured.&lt;/li&gt;
&lt;li&gt;Which errors the program may return.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that information, integrating with the program would mean reading its source code or manually reconstructing its instructions and account layouts.&lt;/p&gt;

&lt;p&gt;Day 87 was about publishing the interface.&lt;/p&gt;

&lt;p&gt;We rebuilt the Anchor program, published its IDL and fetched it back from the network.&lt;/p&gt;

&lt;p&gt;The IDL describes the program’s public surface in a machine-readable form.&lt;/p&gt;

&lt;p&gt;The Web2 comparison is an OpenAPI schema.&lt;/p&gt;

&lt;p&gt;An API may be reachable at a URL, but developers still need to know its routes, parameters, request bodies and responses.&lt;/p&gt;

&lt;p&gt;An IDL plays a similar role for an Anchor program.&lt;/p&gt;

&lt;p&gt;It turns a program address into an interface that tools and developers can understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IDL became a contract
&lt;/h2&gt;

&lt;p&gt;Publishing the IDL did more than save another developer from reading our source code.&lt;/p&gt;

&lt;p&gt;It made the program interface explicit.&lt;/p&gt;

&lt;p&gt;That matters once frontends, scripts and other programs begin depending on it.&lt;/p&gt;

&lt;p&gt;Changing an instruction name can break generated clients.&lt;/p&gt;

&lt;p&gt;Changing its arguments can break serialisation.&lt;/p&gt;

&lt;p&gt;Adding a required account can make existing transactions incomplete.&lt;/p&gt;

&lt;p&gt;Changing an account layout can make stored state unreadable to older clients.&lt;/p&gt;

&lt;p&gt;During local development, those changes are easy to treat as implementation details.&lt;/p&gt;

&lt;p&gt;After deployment, they become compatibility decisions.&lt;/p&gt;

&lt;p&gt;The IDL records the interface that other software expects.&lt;/p&gt;

&lt;p&gt;That does not mean the interface can never change. It means changes need to be considered, versioned and communicated like changes to any other public API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codama turned the interface into a typed client
&lt;/h2&gt;

&lt;p&gt;Once we had the IDL, we used Codama to generate a TypeScript client.&lt;/p&gt;

&lt;p&gt;Without a generated client, calling a program can involve a lot of manual work.&lt;/p&gt;

&lt;p&gt;The developer needs to serialise the instruction arguments correctly, provide accounts in the expected order and interpret account data using the right layout.&lt;/p&gt;

&lt;p&gt;A typed client moves much of that work into generated code.&lt;/p&gt;

&lt;p&gt;Instruction names, account requirements and data types become available through normal TypeScript tooling.&lt;/p&gt;

&lt;p&gt;Editors can offer autocomplete.&lt;/p&gt;

&lt;p&gt;The compiler can catch some incorrect arguments before a transaction is submitted.&lt;/p&gt;

&lt;p&gt;Developers do not need to guess how an account should be decoded or how an instruction should be assembled.&lt;/p&gt;

&lt;p&gt;Generated clients do not remove every integration error. A correctly constructed transaction can still fail because the signer lacks authority, an account contains unexpected state or the network has moved on since the transaction was prepared.&lt;/p&gt;

&lt;p&gt;They do remove a large class of avoidable interface mistakes.&lt;/p&gt;

&lt;p&gt;The deployed program gave us an address.&lt;/p&gt;

&lt;p&gt;The IDL described the interface behind it.&lt;/p&gt;

&lt;p&gt;Codama turned that description into code another application could use.&lt;/p&gt;

&lt;h2&gt;
  
  
  We took the wallet flow back to devnet
&lt;/h2&gt;

&lt;p&gt;The next challenge moved from program integration to user interaction.&lt;/p&gt;

&lt;p&gt;For this exercise, we returned to devnet.&lt;/p&gt;

&lt;p&gt;That let us build and test the browser wallet flow with test SOL before applying the same pattern to a production application. It also reinforced an important operational point: the network, RPC endpoint and program address need to be treated as configuration rather than buried throughout the frontend.&lt;/p&gt;

&lt;p&gt;Using the official Solana dApp template, we built a React app that could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discover compatible wallets installed in the browser.&lt;/li&gt;
&lt;li&gt;Ask the user to connect one.&lt;/li&gt;
&lt;li&gt;Display the connected wallet’s devnet address.&lt;/li&gt;
&lt;li&gt;Fetch and show its devnet balance.&lt;/li&gt;
&lt;li&gt;Build and send a small SOL transfer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those operations was new on its own.&lt;/p&gt;

&lt;p&gt;We had generated keys.&lt;/p&gt;

&lt;p&gt;We had read balances.&lt;/p&gt;

&lt;p&gt;We had built transactions.&lt;/p&gt;

&lt;p&gt;We had submitted transfers.&lt;/p&gt;

&lt;p&gt;The difference was that the authority now came from a user’s wallet through a browser interface.&lt;/p&gt;

&lt;p&gt;The app prepared the transaction, but it did not hold the user’s private key.&lt;/p&gt;

&lt;p&gt;The wallet showed the request and asked the user to approve it.&lt;/p&gt;

&lt;p&gt;Only then could the transaction be signed and sent.&lt;/p&gt;

&lt;p&gt;That is the interaction model users expect from a Solana application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wallet Standard avoided wallet-specific integrations
&lt;/h2&gt;

&lt;p&gt;A frontend could integrate separately with Phantom, Solflare, Backpack and every other wallet it wants to support.&lt;/p&gt;

&lt;p&gt;That would create a growing collection of wallet-specific code and assumptions.&lt;/p&gt;

&lt;p&gt;Wallet Standard gives browser wallets a shared interface.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding a list of supported products, the application discovers compatible wallets available in the user’s browser.&lt;/p&gt;

&lt;p&gt;A wallet can advertise its capabilities through that common interface. The application can then connect, request signatures and submit transactions without building a separate integration for every wallet.&lt;/p&gt;

&lt;p&gt;That makes the frontend easier to maintain and gives users more choice.&lt;/p&gt;

&lt;p&gt;A user should not have to install the one wallet the developer happened to choose.&lt;/p&gt;

&lt;p&gt;The application should work with wallets that support the standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting a wallet did not mean controlling it
&lt;/h2&gt;

&lt;p&gt;The frontend work also reinforced an important boundary.&lt;/p&gt;

&lt;p&gt;A connected wallet has not handed its private key to the application.&lt;/p&gt;

&lt;p&gt;The connection gives the app access to public information, such as the selected address, and a way to request approved actions.&lt;/p&gt;

&lt;p&gt;The wallet remains responsible for signing.&lt;/p&gt;

&lt;p&gt;When the user sends a transfer, the app builds the transaction and presents it to the wallet.&lt;/p&gt;

&lt;p&gt;The wallet shows the request.&lt;/p&gt;

&lt;p&gt;The user approves or rejects it.&lt;/p&gt;

&lt;p&gt;The wallet signs only after approval.&lt;/p&gt;

&lt;p&gt;This separation matters for security and user trust.&lt;/p&gt;

&lt;p&gt;The app can guide the interaction, but the wallet controls the authority.&lt;/p&gt;

&lt;p&gt;It also means that a transaction can fail before it reaches the network.&lt;/p&gt;

&lt;p&gt;The user can reject the request.&lt;/p&gt;

&lt;p&gt;The wallet can disconnect.&lt;/p&gt;

&lt;p&gt;The signing window can close.&lt;/p&gt;

&lt;p&gt;The application needs to handle those outcomes just as carefully as an on-chain error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transaction failures needed better explanations
&lt;/h2&gt;

&lt;p&gt;The first version of a wallet flow often handles success and treats every failure in the same way.&lt;/p&gt;

&lt;p&gt;Something went wrong.&lt;/p&gt;

&lt;p&gt;Try again.&lt;/p&gt;

&lt;p&gt;That is not enough for a production application.&lt;/p&gt;

&lt;p&gt;A user who rejected a signing request does not need to be told that the network failed.&lt;/p&gt;

&lt;p&gt;A user with insufficient funds needs a different next step from someone whose transaction expired.&lt;/p&gt;

&lt;p&gt;A disconnected wallet should be reconnected rather than asked to repeat the same transaction immediately.&lt;/p&gt;

&lt;p&gt;Day 89 introduced a reusable error-classification layer around wallet transactions.&lt;/p&gt;

&lt;p&gt;It distinguished between cases such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user cancelling the wallet request.&lt;/li&gt;
&lt;li&gt;The transaction’s blockhash expiring.&lt;/li&gt;
&lt;li&gt;The wallet lacking enough SOL.&lt;/li&gt;
&lt;li&gt;The wallet disconnecting.&lt;/li&gt;
&lt;li&gt;An unknown wallet, RPC or program failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The original error still went to the developer console.&lt;/p&gt;

&lt;p&gt;That preserved the detail needed for debugging.&lt;/p&gt;

&lt;p&gt;The user saw a shorter explanation and, where possible, a useful next step.&lt;/p&gt;

&lt;p&gt;The application needed both views.&lt;/p&gt;

&lt;p&gt;The developer needed the logs, RPC response and program error.&lt;/p&gt;

&lt;p&gt;The user needed to know what happened and what they could do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different failures needed different responses
&lt;/h2&gt;

&lt;p&gt;Some failures happen inside the wallet interaction.&lt;/p&gt;

&lt;p&gt;The user declines to sign.&lt;/p&gt;

&lt;p&gt;The wallet is locked.&lt;/p&gt;

&lt;p&gt;The selected account changes.&lt;/p&gt;

&lt;p&gt;The connection disappears.&lt;/p&gt;

&lt;p&gt;Others happen while preparing or sending the transaction.&lt;/p&gt;

&lt;p&gt;The balance is too low.&lt;/p&gt;

&lt;p&gt;The recent blockhash has expired.&lt;/p&gt;

&lt;p&gt;The RPC endpoint cannot be reached.&lt;/p&gt;

&lt;p&gt;The network rejects the transaction.&lt;/p&gt;

&lt;p&gt;Another group comes from the program itself.&lt;/p&gt;

&lt;p&gt;An account fails an Anchor constraint.&lt;/p&gt;

&lt;p&gt;The signer lacks authority.&lt;/p&gt;

&lt;p&gt;The requested state transition is not allowed.&lt;/p&gt;

&lt;p&gt;Checked arithmetic returns an error.&lt;/p&gt;

&lt;p&gt;These failures may all surface from the same transaction flow, but they do not mean the same thing.&lt;/p&gt;

&lt;p&gt;Classifying them makes the app easier to use and easier to support.&lt;/p&gt;

&lt;p&gt;It also prevents misleading advice.&lt;/p&gt;

&lt;p&gt;Telling someone to reconnect will not fix an insufficient balance.&lt;/p&gt;

&lt;p&gt;Telling them to retry will not help when they deliberately cancelled the request.&lt;/p&gt;

&lt;p&gt;Telling them only that the transaction failed gives them nothing useful at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pieces had to work together
&lt;/h2&gt;

&lt;p&gt;Day 90 continued the production-readiness work ahead of launch.&lt;/p&gt;

&lt;p&gt;By this point, the program was deployed, its interface could be published, a typed client could call it, and a browser wallet could approve transactions.&lt;/p&gt;

&lt;p&gt;The remaining concern was whether those pieces agreed with one another.&lt;/p&gt;

&lt;p&gt;The frontend needed to point to the intended cluster.&lt;/p&gt;

&lt;p&gt;The client needed to match the deployed interface.&lt;/p&gt;

&lt;p&gt;The program ID needed to be correct for the selected environment.&lt;/p&gt;

&lt;p&gt;Wallet and transaction state needed to remain clear when users connected, disconnected or encountered an error.&lt;/p&gt;

&lt;p&gt;That was the wider lesson of the arc.&lt;/p&gt;

&lt;p&gt;Deployment was not one command at the end of development.&lt;/p&gt;

&lt;p&gt;It was the work required to make the program operable, integrable and usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The launch post made the deployment verifiable
&lt;/h2&gt;

&lt;p&gt;The final day asked us to make the launch public.&lt;/p&gt;

&lt;p&gt;We wrote a post explaining what we had built and linked to the deployed program in Explorer.&lt;/p&gt;

&lt;p&gt;That link gave the announcement verifiable proof.&lt;/p&gt;

&lt;p&gt;Readers did not have to take our word for it that the program was live.&lt;/p&gt;

&lt;p&gt;They could inspect the address and confirm that the executable program account existed on mainnet-beta.&lt;/p&gt;

&lt;p&gt;A useful launch post could also include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the program does.&lt;/li&gt;
&lt;li&gt;Why we built it.&lt;/li&gt;
&lt;li&gt;The mainnet program address.&lt;/li&gt;
&lt;li&gt;A link to the source code.&lt;/li&gt;
&lt;li&gt;Instructions for trying the application.&lt;/li&gt;
&lt;li&gt;A screenshot or short demonstration.&lt;/li&gt;
&lt;li&gt;An invitation to test it, contribute or ask questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The aim was not to produce a grand launch campaign.&lt;/p&gt;

&lt;p&gt;It was to mark the point at which the work became available to other people.&lt;/p&gt;

&lt;p&gt;That changes the nature of the project.&lt;/p&gt;

&lt;p&gt;Before launch, the code mainly needs to make sense to its author.&lt;/p&gt;

&lt;p&gt;After launch, users need to understand what it does.&lt;/p&gt;

&lt;p&gt;Developers need to understand how to integrate with it.&lt;/p&gt;

&lt;p&gt;Contributors need to understand how to work on it.&lt;/p&gt;

&lt;p&gt;Operators need to understand how it can be upgraded and supported.&lt;/p&gt;

&lt;p&gt;Publishing the work begins that conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 13 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 13 moved us from building a Solana program to operating one.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy a program to mainnet-beta and verify its live address.&lt;/li&gt;
&lt;li&gt;Treat deployment as an operational decision with real costs.&lt;/li&gt;
&lt;li&gt;Inspect, transfer and secure upgrade authority.&lt;/li&gt;
&lt;li&gt;Understand the trade-off between upgradeability and immutability.&lt;/li&gt;
&lt;li&gt;Publish an Anchor IDL and generate a typed TypeScript client.&lt;/li&gt;
&lt;li&gt;Connect a React application to browser wallets through Wallet Standard.&lt;/li&gt;
&lt;li&gt;Keep private keys inside the wallet while requesting approved transactions.&lt;/li&gt;
&lt;li&gt;Classify transaction failures and give users useful next steps.&lt;/li&gt;
&lt;li&gt;Keep network, program and client configuration aligned.&lt;/li&gt;
&lt;li&gt;Publish a launch backed by verifiable on-chain evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main lesson was that deployment does not finish when the program reaches mainnet.&lt;/p&gt;

&lt;p&gt;A production program needs clear ownership.&lt;/p&gt;

&lt;p&gt;Its upgrade authority needs to be protected.&lt;/p&gt;

&lt;p&gt;Its interface needs to be discoverable.&lt;/p&gt;

&lt;p&gt;Clients need a reliable way to call it.&lt;/p&gt;

&lt;p&gt;Users need a safe way to approve transactions.&lt;/p&gt;

&lt;p&gt;Failures need to be understandable.&lt;/p&gt;

&lt;p&gt;The address in Explorer proves that the program is live.&lt;/p&gt;

&lt;p&gt;Everything around that address determines whether anyone can use it with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 13 challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 85:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f5b90-09c3-9a0f-9241-65ee784cc401" rel="noopener noreferrer"&gt;Deploy your program to mainnet-beta and verify its live address&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 86:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f6a07-6330-cf59-30cf-c1462f1dae4e" rel="noopener noreferrer"&gt;Inspect and secure your program’s upgrade authority&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 87:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f6a15-09ea-be22-530b-7a87c06b6b65" rel="noopener noreferrer"&gt;Publish the IDL and generate a typed TypeScript client&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 88:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f6a18-63e3-12b3-7641-4e363cc15430" rel="noopener noreferrer"&gt;Build and test a wallet-connected React frontend on devnet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 89:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f6a25-966b-8521-f5d6-9989acced80e" rel="noopener noreferrer"&gt;Turn wallet and transaction failures into useful error messages&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 90:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f6a1c-300f-a163-87f2-306ffe54eada" rel="noopener noreferrer"&gt;Bring the deployment, client and frontend pieces together ahead of launch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 91:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f6a2a-78e2-15bb-1bf5-770679d230bc" rel="noopener noreferrer"&gt;Publish the launch with verifiable on-chain proof&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>learning</category>
    </item>
    <item>
      <title>Arc 12 Catch-up: Advanced Testing and Security</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:15:46 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-12-catch-up-advanced-testing-and-security-aeg</link>
      <guid>https://dev.to/100daysofsolana/arc-12-catch-up-advanced-testing-and-security-aeg</guid>
      <description>&lt;p&gt;Security bugs on Solana do not always come from obscure runtime behaviour or complicated cryptography.&lt;/p&gt;

&lt;p&gt;Often, a program trusted the wrong account, accepted the wrong authority, or assumed that two pieces of state belonged together without proving it.&lt;/p&gt;

&lt;p&gt;Arc 12 was about finding those assumptions before an attacker does.&lt;/p&gt;

&lt;p&gt;Across Days 78–84, we audited our own code, tightened account validation, wrote transactions designed to break the program, used property-based testing and fuzzing to search for edge cases, and reproduced a real class of account-substitution exploit.&lt;/p&gt;

&lt;p&gt;The rule running through the whole arc was simple:&lt;/p&gt;

&lt;p&gt;Every account and input is untrusted until the program proves otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  We began by auditing our own code
&lt;/h2&gt;

&lt;p&gt;The first challenge took us back to an Anchor program we had already written.&lt;/p&gt;

&lt;p&gt;Until now, we had mainly tested whether its instructions worked when the client supplied the right accounts. This time, we looked at each instruction from the other direction.&lt;/p&gt;

&lt;p&gt;What prevents the client from supplying the wrong ones?&lt;/p&gt;

&lt;p&gt;For every account in every &lt;code&gt;#[derive(Accounts)]&lt;/code&gt; struct, we asked two questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this account have the correct owner?&lt;/li&gt;
&lt;li&gt;If it authorises an action, has it signed the transaction?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those checks sit underneath much of Solana program security.&lt;/p&gt;

&lt;p&gt;An account can contain data that looks exactly as expected and still be unsafe to trust. It might be the right type of account in the wrong context, another user’s vault, or a token account tied to a different mint.&lt;/p&gt;

&lt;p&gt;It might also be a different account containing data designed to look legitimate.&lt;/p&gt;

&lt;p&gt;Anchor can enforce many of the checks we need.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; requires an account to have signed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; checks that the expected program owns the account and that its data matches the expected Anchor type.&lt;/p&gt;

&lt;p&gt;PDA constraints verify that an account was derived from the required seeds.&lt;/p&gt;

&lt;p&gt;Relationship constraints check that accounts belong together.&lt;/p&gt;

&lt;p&gt;The danger appears when we step outside those protections without adding the missing validation ourselves.&lt;/p&gt;

&lt;p&gt;There are legitimate reasons to use &lt;code&gt;UncheckedAccount&lt;/code&gt;. It does not mean the account is automatically unsafe.&lt;/p&gt;

&lt;p&gt;It means Anchor is not checking it for us.&lt;/p&gt;

&lt;p&gt;Anchor even requires a &lt;code&gt;/// CHECK:&lt;/code&gt; comment explaining why leaving the account unchecked is safe. The responsibility has visibly moved into our code.&lt;/p&gt;

&lt;p&gt;By the end of the audit, we had a method we could apply to any instruction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List every account.&lt;/li&gt;
&lt;li&gt;Write down what the program assumes about it.&lt;/li&gt;
&lt;li&gt;Find the type, constraint or code that enforces that assumption.&lt;/li&gt;
&lt;li&gt;Flag anything the program simply takes on trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Security bugs often appear where the developer knows something should be true but the program never checks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anchor constraints made those assumptions enforceable
&lt;/h2&gt;

&lt;p&gt;The next challenge gave us an intentionally insecure vault withdrawal and asked us to fix it.&lt;/p&gt;

&lt;p&gt;The secure version needed to establish that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The authority had signed.&lt;/li&gt;
&lt;li&gt;The vault belonged to our program.&lt;/li&gt;
&lt;li&gt;The vault was the expected PDA.&lt;/li&gt;
&lt;li&gt;The supplied authority matched the authority stored in the vault.&lt;/li&gt;
&lt;li&gt;Any account being changed was writable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We could perform some of those checks inside the handler. Anchor gives us a better place for most of them: the accounts struct.&lt;/p&gt;

&lt;p&gt;The hardened instruction used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; to require a signature.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Account&amp;lt;'info, Vault&amp;gt;&lt;/code&gt; to check ownership and account type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; to verify the vault PDA.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has_one = authority&lt;/code&gt; to connect the stored authority to the live signer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mut&lt;/code&gt; to require writable access where state would change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anchor evaluates those constraints before the handler runs.&lt;/p&gt;

&lt;p&gt;If the supplied accounts do not satisfy them, the instruction logic never starts.&lt;/p&gt;

&lt;p&gt;The Web2 comparison is validation at the edge of a service. Authentication, schema validation and authorisation happen before untrusted input reaches the code that changes important state.&lt;/p&gt;

&lt;p&gt;Anchor’s account constraints play a similar role.&lt;/p&gt;

&lt;p&gt;They also make the security model easier to review. Someone reading the accounts struct can see which signer is required, how the PDA is derived and which accounts must be related.&lt;/p&gt;

&lt;p&gt;Comments and client-side checks cannot provide the same protection.&lt;/p&gt;

&lt;p&gt;A client can be replaced.&lt;/p&gt;

&lt;p&gt;A comment can become outdated.&lt;/p&gt;

&lt;p&gt;A runtime constraint applies to every transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we tried to break the program
&lt;/h2&gt;

&lt;p&gt;Most tests begin with the expected path.&lt;/p&gt;

&lt;p&gt;A user deposits funds.&lt;/p&gt;

&lt;p&gt;The balance increases.&lt;/p&gt;

&lt;p&gt;The correct authority withdraws.&lt;/p&gt;

&lt;p&gt;The balance decreases.&lt;/p&gt;

&lt;p&gt;Those tests prove that the program works when everyone follows the rules. They say much less about what happens when someone deliberately breaks them.&lt;/p&gt;

&lt;p&gt;Day 80 changed the job of the test suite.&lt;/p&gt;

&lt;p&gt;Using LiteSVM, we wrote transactions that behaved like attacks.&lt;/p&gt;

&lt;p&gt;We tried to withdraw with the wrong authority, substitute another account for the expected PDA, and withdraw more than the vault contained.&lt;/p&gt;

&lt;p&gt;Each test challenged a specific security claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only the recorded authority can withdraw.&lt;/li&gt;
&lt;li&gt;Only the canonical vault PDA is accepted.&lt;/li&gt;
&lt;li&gt;An excessive withdrawal cannot underflow the balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was not enough to assert that the transaction failed.&lt;/p&gt;

&lt;p&gt;A transaction can fail for irrelevant reasons. The setup might be wrong. An account might be missing. The instruction might never reach the validation we wanted to test.&lt;/p&gt;

&lt;p&gt;So we checked the error too.&lt;/p&gt;

&lt;p&gt;A wrong authority should fail with the authority error.&lt;/p&gt;

&lt;p&gt;A substituted PDA should fail its seed constraint.&lt;/p&gt;

&lt;p&gt;An excessive withdrawal should return the intended balance or arithmetic error.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;“The attack did not work” is useful.&lt;/p&gt;

&lt;p&gt;“The intended defence stopped the attack” is much better evidence.&lt;/p&gt;

&lt;p&gt;This matters most for instructions that move assets, change authorities, close accounts, perform arithmetic with user-supplied values, use PDA signing or make Cross-Program Invocations.&lt;/p&gt;

&lt;p&gt;For each one, the test suite should ask uncomfortable questions.&lt;/p&gt;

&lt;p&gt;What happens if the wrong person signs?&lt;/p&gt;

&lt;p&gt;What happens if nobody signs?&lt;/p&gt;

&lt;p&gt;What happens if the account is valid but belongs to someone else?&lt;/p&gt;

&lt;p&gt;What happens if two individually valid accounts have no valid relationship?&lt;/p&gt;

&lt;p&gt;What happens at zero or at the maximum value?&lt;/p&gt;

&lt;p&gt;What happens when instructions run in an order we did not expect?&lt;/p&gt;

&lt;p&gt;The accounts struct gives us the outline of the security model.&lt;/p&gt;

&lt;p&gt;The adversarial tests try to violate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Property tests searched beyond the attacks we imagined
&lt;/h2&gt;

&lt;p&gt;Adversarial tests are still limited by our own ideas.&lt;/p&gt;

&lt;p&gt;We test the wrong signer because we already know signer checks matter.&lt;/p&gt;

&lt;p&gt;We test an excessive withdrawal because we know underflow is possible.&lt;/p&gt;

&lt;p&gt;We test the wrong PDA because we understand account derivation.&lt;/p&gt;

&lt;p&gt;Less obvious failures often appear in awkward values or unexpected combinations of otherwise valid actions.&lt;/p&gt;

&lt;p&gt;Day 81 introduced property-based testing and fuzzing to search a much larger space.&lt;/p&gt;

&lt;p&gt;A normal test begins with a specific example:&lt;/p&gt;

&lt;p&gt;Deposit 10 into a balance of 20 and expect 30.&lt;/p&gt;

&lt;p&gt;A property test begins with a rule:&lt;/p&gt;

&lt;p&gt;A successful deposit must never reduce the balance.&lt;/p&gt;

&lt;p&gt;If the deposit would overflow, the operation must return an error rather than wrap around.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;proptest&lt;/code&gt;, we expressed that rule in pure Rust and let the framework generate many combinations of values.&lt;/p&gt;

&lt;p&gt;That included values near zero, values close to the &lt;code&gt;u64&lt;/code&gt; limit and combinations we would be unlikely to choose by hand.&lt;/p&gt;

&lt;p&gt;When a property failed, the tool reduced the input to a smaller counterexample.&lt;/p&gt;

&lt;p&gt;A huge random value might reveal the bug, but a minimal failing case usually makes it easier to understand.&lt;/p&gt;

&lt;p&gt;The focus moves away from listing examples and towards stating what must always remain true.&lt;/p&gt;

&lt;p&gt;For a vault, useful properties might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A deposit never reduces the balance.&lt;/li&gt;
&lt;li&gt;A withdrawal never increases it.&lt;/li&gt;
&lt;li&gt;A failed instruction leaves state unchanged.&lt;/li&gt;
&lt;li&gt;A user cannot withdraw more than the vault contains.&lt;/li&gt;
&lt;li&gt;Only the recorded authority can withdraw.&lt;/li&gt;
&lt;li&gt;No valid sequence of operations creates or loses funds unexpectedly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are invariants: rules that should survive every valid state transition.&lt;/p&gt;

&lt;p&gt;That makes them especially useful when the bug appears only after a sequence of instructions rather than in one isolated call.&lt;/p&gt;

&lt;p&gt;Trident extended the same idea into the Anchor program itself.&lt;/p&gt;

&lt;p&gt;Instead of testing only pure Rust arithmetic, it generated instructions and values, exercised the program, and checked the properties we supplied.&lt;/p&gt;

&lt;p&gt;That allowed it to explore unusual sequences of deposits and withdrawals, generated account combinations, failed transactions and boundary values.&lt;/p&gt;

&lt;p&gt;The developer still decides which rules matter.&lt;/p&gt;

&lt;p&gt;The fuzzer searches for a sequence that breaks them.&lt;/p&gt;

&lt;p&gt;When it finds one, the counterexample should become a named regression test.&lt;/p&gt;

&lt;p&gt;That gives us a useful workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State an invariant.&lt;/li&gt;
&lt;li&gt;Let the fuzzer search for a counterexample.&lt;/li&gt;
&lt;li&gt;Understand the failure.&lt;/li&gt;
&lt;li&gt;Fix the program.&lt;/li&gt;
&lt;li&gt;Add the failing case to the permanent test suite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuzzing finds the case.&lt;/p&gt;

&lt;p&gt;The regression test makes sure we do not forget it.&lt;/p&gt;

&lt;h2&gt;
  
  
  We reproduced an account-substitution exploit
&lt;/h2&gt;

&lt;p&gt;Day 82 turned the arc’s central warning into a working exploit.&lt;/p&gt;

&lt;p&gt;We built a deliberately insecure program that read configuration data from an unchecked account.&lt;/p&gt;

&lt;p&gt;The account looked valid.&lt;/p&gt;

&lt;p&gt;Its data had the expected shape, and the values were where the program expected them to be.&lt;/p&gt;

&lt;p&gt;But it was not the account the program should have trusted.&lt;/p&gt;

&lt;p&gt;Because the instruction checked the data without properly establishing the account’s identity and ownership, it accepted a forged configuration and allowed an unauthorised result.&lt;/p&gt;

&lt;p&gt;This reproduced the broad class of mistake seen in incidents such as Cashio and Wormhole: trusting a supplied account without proving that it was the account the program expected.&lt;/p&gt;

&lt;p&gt;The details differed.&lt;/p&gt;

&lt;p&gt;Cashio involved a broken chain of trust around supplied accounts.&lt;/p&gt;

&lt;p&gt;Wormhole accepted a substituted account in place of the real Instructions sysvar, allowing the attacker to bypass signature verification.&lt;/p&gt;

&lt;p&gt;The shared lesson is that convincing data is not enough.&lt;/p&gt;

&lt;p&gt;The program must establish which account supplied it.&lt;/p&gt;

&lt;p&gt;In our exercise, the fix was small.&lt;/p&gt;

&lt;p&gt;We replaced the unchecked configuration account with &lt;code&gt;Account&amp;lt;'info, Config&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Anchor then checked that the expected program owned the account and that its data matched the expected Anchor type before the handler read it.&lt;/p&gt;

&lt;p&gt;A different account could still contain similar data.&lt;/p&gt;

&lt;p&gt;It could no longer pass validation.&lt;/p&gt;

&lt;p&gt;This is why ownership and identity are part of the meaning of account data on Solana.&lt;/p&gt;

&lt;p&gt;A configuration account owned by our program is program state.&lt;/p&gt;

&lt;p&gt;Similar data in an account controlled elsewhere is untrusted input.&lt;/p&gt;

&lt;p&gt;Without the right checks, vulnerable code may not see the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The strongest fixes were often structural
&lt;/h2&gt;

&lt;p&gt;A recurring theme throughout the arc was that security fixes do not always require more handler code.&lt;/p&gt;

&lt;p&gt;Often, the cleaner answer is to choose account types and constraints that reject the bad request before the handler runs.&lt;/p&gt;

&lt;p&gt;Use a checked account type instead of manually reading an unchecked account.&lt;/p&gt;

&lt;p&gt;Declare PDA seeds rather than comparing addresses later.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;has_one&lt;/code&gt; to enforce an account relationship.&lt;/p&gt;

&lt;p&gt;Use checked arithmetic rather than assuming values will remain within the expected range.&lt;/p&gt;

&lt;p&gt;There will always be business rules that constraints cannot express, but structural protections have an important advantage: they apply regardless of which client sends the transaction.&lt;/p&gt;

&lt;p&gt;A well-behaved frontend does not make a program secure.&lt;/p&gt;

&lt;p&gt;Someone can always construct the transaction directly.&lt;/p&gt;

&lt;p&gt;The program has to defend itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  We turned the arc into a security checklist
&lt;/h2&gt;

&lt;p&gt;By Day 83, we had audited an existing program, hardened its constraints, written malicious transactions, generated edge cases and reproduced an account-substitution exploit.&lt;/p&gt;

&lt;p&gt;The next challenge was to turn that work into something we could use again.&lt;/p&gt;

&lt;p&gt;We published a practical Solana security checklist on DEV.&lt;/p&gt;

&lt;p&gt;It covered checks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify the owner and type of every account whose data is read.&lt;/li&gt;
&lt;li&gt;Require signatures for authority actions.&lt;/li&gt;
&lt;li&gt;Validate PDAs with the intended seeds and canonical bump.&lt;/li&gt;
&lt;li&gt;Bind related accounts with constraints such as &lt;code&gt;has_one&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Validate token-account mint and authority relationships.&lt;/li&gt;
&lt;li&gt;Use checked arithmetic.&lt;/li&gt;
&lt;li&gt;Test overflow, underflow and rounding.&lt;/li&gt;
&lt;li&gt;Treat unchecked accounts as high-risk inputs.&lt;/li&gt;
&lt;li&gt;Review the accounts and programs supplied to every CPI.&lt;/li&gt;
&lt;li&gt;Write adversarial tests for instructions that move assets or change authority.&lt;/li&gt;
&lt;li&gt;Check that attacks fail for the expected reason.&lt;/li&gt;
&lt;li&gt;Add property tests for important invariants.&lt;/li&gt;
&lt;li&gt;Fuzz high-value instruction sequences.&lt;/li&gt;
&lt;li&gt;Preserve discovered failures as regression tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact checklist will vary from one program to another.&lt;/p&gt;

&lt;p&gt;The useful change was replacing a vague final review with a repeatable process.&lt;/p&gt;

&lt;p&gt;Instead of asking, “Does this look secure?”, we could ask:&lt;/p&gt;

&lt;p&gt;Where is account ownership checked?&lt;/p&gt;

&lt;p&gt;Which authority must sign?&lt;/p&gt;

&lt;p&gt;How is the PDA verified?&lt;/p&gt;

&lt;p&gt;What prevents two unrelated accounts from being used together?&lt;/p&gt;

&lt;p&gt;What happens at the arithmetic boundary?&lt;/p&gt;

&lt;p&gt;Which test attempts an unauthorised withdrawal?&lt;/p&gt;

&lt;p&gt;Which properties must always remain true?&lt;/p&gt;

&lt;p&gt;Those questions are easier to answer, review and revisit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explaining the checks made us understand them properly
&lt;/h2&gt;

&lt;p&gt;The final two days focused on communicating what we had learned.&lt;/p&gt;

&lt;p&gt;That matters in security work because it is possible to copy a constraint without understanding the threat it addresses.&lt;/p&gt;

&lt;p&gt;Writing the checklist forced us to connect each protection to a concrete failure.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; matters because including an account in a transaction does not mean its owner approved the action.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; matters because valid-looking data does not prove that the right program owns the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; matter because the client can supply another account unless the program verifies the expected PDA.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_one&lt;/code&gt; matters because two accounts can each be valid while having no valid relationship to one another.&lt;/p&gt;

&lt;p&gt;Checked arithmetic matters because an attacker can choose values normal users never would.&lt;/p&gt;

&lt;p&gt;Adversarial tests matter because successful transactions do not prove that malicious ones are rejected.&lt;/p&gt;

&lt;p&gt;Property tests and fuzzing matter because we cannot write down every possible input and sequence.&lt;/p&gt;

&lt;p&gt;A useful security explanation should say more than which Anchor attribute to add.&lt;/p&gt;

&lt;p&gt;It should explain the assumption, how an attacker could violate it, and how the program prevents that from happening.&lt;/p&gt;

&lt;p&gt;Day 84 asked us to reduce the longer checklist to a short X thread or LinkedIn post.&lt;/p&gt;

&lt;p&gt;That meant choosing the checks that mattered most and backing them up with evidence from the week: a failing attack test, the forged configuration accepted by the insecure program, the same account rejected after the fix, or a counterexample found through generated testing.&lt;/p&gt;

&lt;p&gt;The result was more useful than a general list of security advice.&lt;/p&gt;

&lt;p&gt;It showed that we could identify an attack, reproduce it, fix it and prove that the fix worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 12 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 12 moved us from checking whether our programs worked to testing whether they could be abused.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit instructions for missing owner, signer and relationship checks.&lt;/li&gt;
&lt;li&gt;Use Anchor types and constraints to enforce account validation.&lt;/li&gt;
&lt;li&gt;Write adversarial tests and check that they fail for the intended reason.&lt;/li&gt;
&lt;li&gt;Describe important rules as properties and invariants.&lt;/li&gt;
&lt;li&gt;Use fuzzing to find cases we would not write by hand.&lt;/li&gt;
&lt;li&gt;Turn discovered counterexamples into permanent regression tests.&lt;/li&gt;
&lt;li&gt;Reproduce and fix an account-substitution exploit.&lt;/li&gt;
&lt;li&gt;Build and share a reusable security checklist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main lesson was that the accounts supplied to an instruction are claims made by the transaction.&lt;/p&gt;

&lt;p&gt;This is the authority.&lt;/p&gt;

&lt;p&gt;This is the vault.&lt;/p&gt;

&lt;p&gt;These accounts belong together.&lt;/p&gt;

&lt;p&gt;This data is genuine.&lt;/p&gt;

&lt;p&gt;This amount is safe to process.&lt;/p&gt;

&lt;p&gt;The program has to verify each claim before acting on it.&lt;/p&gt;

&lt;p&gt;Account ownership, signer checks, PDA derivation, relationship constraints and checked arithmetic provide that verification.&lt;/p&gt;

&lt;p&gt;The tests show whether it holds up.&lt;/p&gt;

&lt;p&gt;Adversarial tests cover the attacks we already understand.&lt;/p&gt;

&lt;p&gt;Property testing and fuzzing search for the cases we missed.&lt;/p&gt;

&lt;p&gt;Reproducing an exploit shows what one unchecked assumption can allow.&lt;/p&gt;

&lt;p&gt;A secure program does not depend on the client doing the right thing.&lt;/p&gt;

&lt;p&gt;It assumes someone will eventually try the wrong thing and is ready to reject it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 12 challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 78:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f3844-b0ad-0853-b56e-7811acca66cd" rel="noopener noreferrer"&gt;Audit your Anchor program for missing owner and signer checks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 79:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f3d22-6ed6-b18d-d0d6-78baed6a531e" rel="noopener noreferrer"&gt;Harden an insecure withdrawal instruction with Anchor constraints&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 80:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f3d51-f52b-1308-d4fd-a33958bbb56f" rel="noopener noreferrer"&gt;Write adversarial LiteSVM tests that try to break your program&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 81:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f46ef-a073-bb5c-f1cd-cdb958a034ef" rel="noopener noreferrer"&gt;Use property-based testing and fuzzing to find unknown edge cases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 82:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f4ce4-117f-20f1-a1dd-af3543253ed9" rel="noopener noreferrer"&gt;Reproduce and fix an account-substitution exploit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 83:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f5a93-a430-0918-92b4-b513a51dcaf2" rel="noopener noreferrer"&gt;Publish a practical Solana security checklist on DEV&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 84:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f5aa0-92b3-993e-2159-466faaccdf3e" rel="noopener noreferrer"&gt;Turn the checklist into a social post or thread&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>learning</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Arc 11 Catch-Up: Composing Solana Programs with CPIs</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Thu, 16 Jul 2026 12:46:25 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-11-catch-up-composing-solana-programs-with-cpis-406</link>
      <guid>https://dev.to/100daysofsolana/arc-11-catch-up-composing-solana-programs-with-cpis-406</guid>
      <description>&lt;p&gt;Arc 11 covered Days 71–77 of Epoch 3, and it was all about Cross-Program Invocations.&lt;/p&gt;

&lt;p&gt;In Arc 9, we wrote our first Solana program.&lt;/p&gt;

&lt;p&gt;In Arc 10, we gave that program a more useful state model with Program Derived Addresses.&lt;/p&gt;

&lt;p&gt;But our programs were still mostly working alone.&lt;/p&gt;

&lt;p&gt;They could read and update their own accounts, enforce their own constraints, and respond to instructions sent by a client. They could not directly change state owned by another program or bypass the rules that program enforced.&lt;/p&gt;

&lt;p&gt;That is an important part of Solana’s security model.&lt;/p&gt;

&lt;p&gt;The System Program owns the rules for creating accounts and transferring lamports.&lt;/p&gt;

&lt;p&gt;Token-2022 owns the rules for mints, token accounts, supply, and mint authorities.&lt;/p&gt;

&lt;p&gt;If our program needs one of those capabilities, it calls the program that owns the operation.&lt;/p&gt;

&lt;p&gt;That call is a Cross-Program Invocation, or CPI.&lt;/p&gt;

&lt;p&gt;The Web2 comparison is a service-to-service API call. One service sends a request through another service’s public interface, and the receiving service applies its own rules.&lt;/p&gt;

&lt;p&gt;A CPI works in a similar way, with one important difference: the outer and inner instructions execute as part of the same Solana transaction. If the inner call fails, the state changes made by the outer instruction are rolled back too.&lt;/p&gt;

&lt;p&gt;That combination of clear program boundaries and atomic execution is what makes Solana programs composable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our first CPI called the System Program
&lt;/h2&gt;

&lt;p&gt;The arc began with the smallest useful CPI we could build.&lt;/p&gt;

&lt;p&gt;Our Anchor program accepted a sender, a recipient, and an amount of SOL to transfer.&lt;/p&gt;

&lt;p&gt;But the program did not edit the sender’s balance directly.&lt;/p&gt;

&lt;p&gt;Instead, it called the System Program’s transfer instruction.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Accounts on Solana are owned by programs, and the owner program controls how their data may be changed. Our program could not simply reproduce the effect of a System Program transfer by adjusting balances itself.&lt;/p&gt;

&lt;p&gt;It had to ask the System Program to perform the operation.&lt;/p&gt;

&lt;p&gt;Every CPI needs three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program being called&lt;/li&gt;
&lt;li&gt;the accounts that program expects&lt;/li&gt;
&lt;li&gt;the authority required to approve the action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the SOL transfer, the called program was the System Program.&lt;/p&gt;

&lt;p&gt;The required accounts were the sender and recipient.&lt;/p&gt;

&lt;p&gt;The authority came from the sender, who had signed the original transaction.&lt;/p&gt;

&lt;p&gt;Our program assembled those pieces and made the inner call. The System Program then applied its own rules and either accepted or rejected the transfer.&lt;/p&gt;

&lt;p&gt;That gave us the basic CPI model we used throughout the arc.&lt;/p&gt;

&lt;p&gt;The caller requests an operation.&lt;/p&gt;

&lt;p&gt;The callee validates the request.&lt;/p&gt;

&lt;p&gt;The callee remains responsible for the state it owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signer authority can flow into an inner call
&lt;/h2&gt;

&lt;p&gt;The sender in that first exercise was a normal wallet signer.&lt;/p&gt;

&lt;p&gt;The wallet signed the outer transaction sent by the client. When our program called the System Program, that signer privilege was also available to the inner instruction.&lt;/p&gt;

&lt;p&gt;The CPI did not invent new authority.&lt;/p&gt;

&lt;p&gt;It passed along authority that already existed in the transaction.&lt;/p&gt;

&lt;p&gt;The same applies to writable accounts. A program cannot make an account writable during a CPI if the original transaction supplied it as read-only.&lt;/p&gt;

&lt;p&gt;That gives us an important boundary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A CPI cannot arbitrarily escalate account privileges.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writable access must already have been granted by the transaction. Signer authority must come either from an existing signer or from a PDA controlled by the calling program.&lt;/p&gt;

&lt;p&gt;That second form of authority became the major step forward later in the arc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token-2022 followed the same pattern
&lt;/h2&gt;

&lt;p&gt;The next exercise moved from transferring SOL to minting tokens.&lt;/p&gt;

&lt;p&gt;Our program called Token-2022’s &lt;code&gt;mint_to&lt;/code&gt; instruction to increase a mint’s supply and deposit the new tokens into a destination token account.&lt;/p&gt;

&lt;p&gt;The accounts were different, but the CPI model stayed the same.&lt;/p&gt;

&lt;p&gt;The call needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Token-2022 program&lt;/li&gt;
&lt;li&gt;the mint&lt;/li&gt;
&lt;li&gt;the destination token account&lt;/li&gt;
&lt;li&gt;the mint authority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mint authority was still a regular wallet signer, so its signature flowed from the outer transaction into the Token-2022 instruction.&lt;/p&gt;

&lt;p&gt;Token-2022 then applied its own rules.&lt;/p&gt;

&lt;p&gt;It checked that the mint and destination accounts were valid, that the supplied authority matched the mint’s recorded authority, and that the authority had signed.&lt;/p&gt;

&lt;p&gt;Our program did not need to recreate any token logic.&lt;/p&gt;

&lt;p&gt;It only needed to assemble a valid request.&lt;/p&gt;

&lt;p&gt;That is the larger value of learning the CPI pattern.&lt;/p&gt;

&lt;p&gt;Once we understand the combination of program, accounts, and authority, existing Solana programs become building blocks we can call from our own code.&lt;/p&gt;

&lt;p&gt;The System Program already knows how to transfer SOL.&lt;/p&gt;

&lt;p&gt;Token-2022 already knows how to manage token supply.&lt;/p&gt;

&lt;p&gt;Our program can use those capabilities while each called program keeps control of its own rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CPI still crosses a real program boundary
&lt;/h2&gt;

&lt;p&gt;Anchor’s CPI helpers can make the interaction look similar to calling a local function.&lt;/p&gt;

&lt;p&gt;But the called program does not simply trust the caller.&lt;/p&gt;

&lt;p&gt;It receives an instruction, examines the supplied accounts, checks their privileges and relationships, and runs its own logic.&lt;/p&gt;

&lt;p&gt;The caller decides what it wants to request.&lt;/p&gt;

&lt;p&gt;The callee decides whether that request is valid.&lt;/p&gt;

&lt;p&gt;For the System Program, that means enforcing the rules for lamport transfers.&lt;/p&gt;

&lt;p&gt;For Token-2022, it means enforcing the rules for mints and token accounts.&lt;/p&gt;

&lt;p&gt;For another Anchor program, it means applying that program’s account constraints and instruction logic.&lt;/p&gt;

&lt;p&gt;The inner program is therefore more than a shared code library.&lt;/p&gt;

&lt;p&gt;It is a separate on-chain authority with its own state and security boundary.&lt;/p&gt;

&lt;p&gt;That is what makes composition useful. Programs can depend on one another without surrendering control of the accounts they own.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDA signing gave the program its own authority
&lt;/h2&gt;

&lt;p&gt;The first two CPIs used wallet signers.&lt;/p&gt;

&lt;p&gt;That gave us a useful baseline: the user signed the outer transaction, and the inner program recognised the same authority.&lt;/p&gt;

&lt;p&gt;The next challenge introduced a different situation.&lt;/p&gt;

&lt;p&gt;We built a per-user SOL vault at a PDA.&lt;/p&gt;

&lt;p&gt;Each user had a predictable vault address derived from the program’s seeds.&lt;/p&gt;

&lt;p&gt;Depositing into the vault was straightforward. The user controlled the source account and signed the transaction, so that authority could flow into the inner transfer.&lt;/p&gt;

&lt;p&gt;Withdrawing from the vault raised a harder question.&lt;/p&gt;

&lt;p&gt;The vault was a PDA.&lt;/p&gt;

&lt;p&gt;It had no private key and could not sign a transaction in the way a wallet could.&lt;/p&gt;

&lt;p&gt;The answer was a PDA-signed CPI.&lt;/p&gt;

&lt;p&gt;When the program made the inner call, it supplied the original seeds and canonical bump used to derive the vault.&lt;/p&gt;

&lt;p&gt;The Solana runtime combined those values with the calling program’s ID and derived the address again.&lt;/p&gt;

&lt;p&gt;If the result matched the vault account, the runtime treated that PDA as a signer for the inner invocation.&lt;/p&gt;

&lt;p&gt;The program never obtained or stored a private key for the PDA.&lt;/p&gt;

&lt;p&gt;It proved authority by reproducing the derivation that created the address.&lt;/p&gt;

&lt;p&gt;That is the core of PDA signing.&lt;/p&gt;

&lt;p&gt;A program can control an on-chain account or asset through a deterministic address, then authorise actions for it only when its own instruction logic allows those actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signer seeds are not secrets
&lt;/h2&gt;

&lt;p&gt;The phrase “signer seeds” can make the seeds sound like passwords.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;Anyone can inspect the program and see the seed scheme. The bump may also be visible in account data or derived again by a client.&lt;/p&gt;

&lt;p&gt;That does not let another user or program impersonate the PDA.&lt;/p&gt;

&lt;p&gt;The program ID is part of the derivation.&lt;/p&gt;

&lt;p&gt;Another program could use the same seed values, but its own program ID would produce a different address.&lt;/p&gt;

&lt;p&gt;The runtime only grants signer privilege when the seeds, bump, calling program ID, and account address all match.&lt;/p&gt;

&lt;p&gt;The security comes from that relationship, along with the program logic that controls when the signer seeds are used.&lt;/p&gt;

&lt;p&gt;This gives a program its own deterministic on-chain identity without introducing another private key that someone has to create, protect, and rotate.&lt;/p&gt;

&lt;p&gt;A PDA can hold SOL, control a token mint, own a token account, or act as an authority for another program.&lt;/p&gt;

&lt;p&gt;The program decides when that authority may be exercised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programs can call other custom programs
&lt;/h2&gt;

&lt;p&gt;The first exercises called established Solana programs.&lt;/p&gt;

&lt;p&gt;Day 74 took the next step: one custom Anchor program called another.&lt;/p&gt;

&lt;p&gt;The first program owned a tally account and exposed an instruction that incremented it.&lt;/p&gt;

&lt;p&gt;The second program invoked that instruction through a CPI.&lt;/p&gt;

&lt;p&gt;The client sent a transaction to the caller program. The caller then invoked the counter program, which updated the account it owned.&lt;/p&gt;

&lt;p&gt;The client never called the counter instruction directly, but the tally still increased.&lt;/p&gt;

&lt;p&gt;That demonstrated an important boundary.&lt;/p&gt;

&lt;p&gt;The caller program could not edit the counter program’s account itself.&lt;/p&gt;

&lt;p&gt;It had to request the update through the counter program’s public instruction.&lt;/p&gt;

&lt;p&gt;The counter program remained responsible for validating the accounts and deciding whether the update was allowed.&lt;/p&gt;

&lt;p&gt;That is how larger on-chain systems can be assembled from smaller programs.&lt;/p&gt;

&lt;p&gt;One program can provide a focused capability.&lt;/p&gt;

&lt;p&gt;Another can use that capability as part of a wider workflow.&lt;/p&gt;

&lt;p&gt;The callee remains in control of its state, while the caller gains access to its public behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IDL becomes a contract between programs
&lt;/h2&gt;

&lt;p&gt;To call another Anchor program, the caller needs to understand its public interface.&lt;/p&gt;

&lt;p&gt;It needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program ID&lt;/li&gt;
&lt;li&gt;the available instructions&lt;/li&gt;
&lt;li&gt;the arguments those instructions accept&lt;/li&gt;
&lt;li&gt;the accounts they require&lt;/li&gt;
&lt;li&gt;the account and data types they expose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anchor describes that interface in an IDL.&lt;/p&gt;

&lt;p&gt;The caller can use the callee’s IDL to generate typed CPI bindings. It does not need to copy or compile the callee’s source code into its own program.&lt;/p&gt;

&lt;p&gt;That is similar to using an API specification to call another service.&lt;/p&gt;

&lt;p&gt;The caller depends on the published contract rather than the internal implementation.&lt;/p&gt;

&lt;p&gt;The callee can refactor its code while keeping callers compatible, provided that its external instruction and account interface remains stable.&lt;/p&gt;

&lt;p&gt;But changes to that interface can break integrations.&lt;/p&gt;

&lt;p&gt;Renaming an instruction, changing its arguments, adding required accounts, or altering an account layout may affect programs and clients that already depend on it.&lt;/p&gt;

&lt;p&gt;Once other programs compose with ours, the IDL is no longer only a development convenience.&lt;/p&gt;

&lt;p&gt;It becomes part of the interface other software expects us to preserve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole call stack remains atomic
&lt;/h2&gt;

&lt;p&gt;A CPI does not create a separate transaction.&lt;/p&gt;

&lt;p&gt;The inner instruction executes as part of the transaction that called the outer program.&lt;/p&gt;

&lt;p&gt;Consider an instruction that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates one of its own accounts.&lt;/li&gt;
&lt;li&gt;Calls Token-2022 to mint tokens.&lt;/li&gt;
&lt;li&gt;Updates another account after the mint succeeds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Token-2022 rejects the mint, the entire transaction fails.&lt;/p&gt;

&lt;p&gt;The state update performed before the CPI does not remain on-chain.&lt;/p&gt;

&lt;p&gt;There is no partially completed result where the caller records that the operation succeeded but the token mint did not change.&lt;/p&gt;

&lt;p&gt;Everything succeeds together or everything is rolled back.&lt;/p&gt;

&lt;p&gt;Anyone who has written compensation logic or reconciliation jobs for distributed workflows will recognise why that matters.&lt;/p&gt;

&lt;p&gt;It allows a Solana program to combine operations across several programs without having to repair state when one step in the middle fails.&lt;/p&gt;

&lt;p&gt;It also means that an error deep in a chain of CPIs can cause every preceding change in the transaction to be reverted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking CPIs showed where integrations fail
&lt;/h2&gt;

&lt;p&gt;Once the working paths were in place, we deliberately broke them.&lt;/p&gt;

&lt;p&gt;We tested three common categories of failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;incorrect PDA signer seeds or bump&lt;/li&gt;
&lt;li&gt;missing or incorrect accounts&lt;/li&gt;
&lt;li&gt;the wrong program ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each failure exposed a different part of the contract between caller and callee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong signer seeds
&lt;/h3&gt;

&lt;p&gt;For a PDA-signed CPI, the runtime derives the PDA again using the supplied seeds, bump, and calling program ID.&lt;/p&gt;

&lt;p&gt;If any of those inputs are wrong, the result does not match the account expected to sign.&lt;/p&gt;

&lt;p&gt;The inner instruction then sees that its required authority is missing.&lt;/p&gt;

&lt;p&gt;A one-byte difference is enough to break the derivation.&lt;/p&gt;

&lt;p&gt;When debugging, the key question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do these exact seeds and this bump derive the exact PDA being supplied as the authority?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If they do not, the runtime cannot grant signer privilege.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong accounts
&lt;/h3&gt;

&lt;p&gt;Every instruction expects a particular set of accounts.&lt;/p&gt;

&lt;p&gt;Those accounts may need to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writable&lt;/li&gt;
&lt;li&gt;signers&lt;/li&gt;
&lt;li&gt;owned by a particular program&lt;/li&gt;
&lt;li&gt;derived from particular seeds&lt;/li&gt;
&lt;li&gt;linked through a stored relationship&lt;/li&gt;
&lt;li&gt;associated with a particular mint or authority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Passing the wrong account often produces an Anchor constraint error.&lt;/p&gt;

&lt;p&gt;Those errors can identify both the account and the validation that failed.&lt;/p&gt;

&lt;p&gt;The callee’s accounts struct is therefore more than setup code.&lt;/p&gt;

&lt;p&gt;It is the contract the caller must satisfy.&lt;/p&gt;

&lt;p&gt;When a CPI fails, comparing the supplied accounts against that contract is often the fastest route to the cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong program ID
&lt;/h3&gt;

&lt;p&gt;The caller must also invoke the correct program.&lt;/p&gt;

&lt;p&gt;Typed program accounts help protect this boundary. Anchor can validate the supplied program account before attempting the CPI and reject an unexpected ID during account validation.&lt;/p&gt;

&lt;p&gt;With a loosely typed program account, that protection may not exist.&lt;/p&gt;

&lt;p&gt;The runtime can invoke the program it was given, even when the instruction data was intended for someone else.&lt;/p&gt;

&lt;p&gt;The error then comes from the unintended program trying to interpret an instruction it does not recognise.&lt;/p&gt;

&lt;p&gt;This is one reason typed accounts are valuable.&lt;/p&gt;

&lt;p&gt;They turn a confusing downstream failure into a clearer validation failure at the program boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transaction logs reveal the call stack
&lt;/h2&gt;

&lt;p&gt;CPI debugging becomes easier when we stop looking only at the final error code.&lt;/p&gt;

&lt;p&gt;The transaction logs show the sequence of execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the outer program begins&lt;/li&gt;
&lt;li&gt;it reaches the CPI&lt;/li&gt;
&lt;li&gt;the inner program is invoked&lt;/li&gt;
&lt;li&gt;the inner program validates its accounts&lt;/li&gt;
&lt;li&gt;an instruction succeeds or fails&lt;/li&gt;
&lt;li&gt;the result propagates back through the call stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sequence tells us which program was running when the failure occurred.&lt;/p&gt;

&lt;p&gt;Most CPI bugs come from one of two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The caller assembled the request incorrectly.&lt;/li&gt;
&lt;li&gt;The callee enforced a rule that the supplied request did not satisfy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which program produced the error?&lt;/li&gt;
&lt;li&gt;Which inner instruction was being called?&lt;/li&gt;
&lt;li&gt;Did the caller provide every required account?&lt;/li&gt;
&lt;li&gt;Were the signer and writable privileges correct?&lt;/li&gt;
&lt;li&gt;Did the signer seeds derive the expected PDA?&lt;/li&gt;
&lt;li&gt;Did Anchor identify a failed constraint?&lt;/li&gt;
&lt;li&gt;Was the intended program actually invoked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A CPI failure is usually the on-chain equivalent of an API request that did not match the receiving service’s contract.&lt;/p&gt;

&lt;p&gt;The logs help us find the point where that mismatch occurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the explanation forced us to choose the important part
&lt;/h2&gt;

&lt;p&gt;The final two days moved from implementation to communication.&lt;/p&gt;

&lt;p&gt;CPIs include enough moving parts that it is tempting to explain all of them at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;program ownership&lt;/li&gt;
&lt;li&gt;nested instructions&lt;/li&gt;
&lt;li&gt;account privileges&lt;/li&gt;
&lt;li&gt;wallet signers&lt;/li&gt;
&lt;li&gt;PDA signing&lt;/li&gt;
&lt;li&gt;IDLs&lt;/li&gt;
&lt;li&gt;atomicity&lt;/li&gt;
&lt;li&gt;transaction logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can produce an explanation that is technically complete but hard to follow.&lt;/p&gt;

&lt;p&gt;The challenge instead asked us to choose one clear thesis.&lt;/p&gt;

&lt;p&gt;One possible framing was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A CPI is a function call with a guest list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The called instruction needs the right program, the right accounts, and the right authority.&lt;/p&gt;

&lt;p&gt;Another focused on the major conceptual jump:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Signer seeds are how a program proves control of its PDA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The seeds are not secret credentials. They let the runtime reconstruct the PDA under the calling program’s ID and grant it signer privilege for one inner invocation.&lt;/p&gt;

&lt;p&gt;The public explanation needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one clear mental model&lt;/li&gt;
&lt;li&gt;one small example&lt;/li&gt;
&lt;li&gt;the three ingredients of a CPI&lt;/li&gt;
&lt;li&gt;one real error encountered during the week&lt;/li&gt;
&lt;li&gt;evidence from the working program&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That evidence might be passing test output, a GitHub repository, a concise implementation excerpt, or a devnet transaction.&lt;/p&gt;

&lt;p&gt;As in the previous arcs, publishing the work was part of the learning process.&lt;/p&gt;

&lt;p&gt;Explaining where authority comes from, which program owns the operation, and why a broken CPI fails reveals gaps that successful tests alone may not expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 11 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 11 moved us from isolated programs to composed systems.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had seen how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call the System Program from an Anchor instruction&lt;/li&gt;
&lt;li&gt;transfer SOL through a CPI&lt;/li&gt;
&lt;li&gt;call Token-2022 to mint tokens&lt;/li&gt;
&lt;li&gt;supply the program, accounts, and authority required by a callee&lt;/li&gt;
&lt;li&gt;pass wallet signer authority into an inner instruction&lt;/li&gt;
&lt;li&gt;understand the limits on signer and writable privileges&lt;/li&gt;
&lt;li&gt;authorise an inner call with a PDA&lt;/li&gt;
&lt;li&gt;control on-chain assets without storing a private key&lt;/li&gt;
&lt;li&gt;call one custom Anchor program from another&lt;/li&gt;
&lt;li&gt;use an IDL as the contract between programs&lt;/li&gt;
&lt;li&gt;treat instruction and account changes as compatibility decisions&lt;/li&gt;
&lt;li&gt;break CPIs with incorrect seeds, accounts, and program IDs&lt;/li&gt;
&lt;li&gt;read transaction logs as a record of the program call stack&lt;/li&gt;
&lt;li&gt;explain program composition through a clear, concrete example&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The enduring lesson is that Solana programs are interoperable pieces of infrastructure.&lt;/p&gt;

&lt;p&gt;A program that needs to transfer SOL can call the System Program.&lt;/p&gt;

&lt;p&gt;A program that needs to mint tokens can call Token-2022.&lt;/p&gt;

&lt;p&gt;A program that needs behaviour exposed by another custom program can call that program through its public instruction interface.&lt;/p&gt;

&lt;p&gt;Each callee keeps control of its accounts and applies its own authorisation rules.&lt;/p&gt;

&lt;p&gt;The caller gains the capability without copying the implementation or bypassing the program that owns the state.&lt;/p&gt;

&lt;p&gt;That is what CPIs make possible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solana programs can combine trusted on-chain capabilities into one atomic transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 11 challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 71:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f137c-fad9-d4c3-1645-e60101686d4b" rel="noopener noreferrer"&gt;Call the System Program from your own program to transfer SOL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 72:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1411-f45c-f3ea-c369-7d3fccce3e9e" rel="noopener noreferrer"&gt;Use a CPI to mint tokens through Token-2022&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 73:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1439-7cef-b445-1226-94ae3bfd1656" rel="noopener noreferrer"&gt;Build a PDA-controlled SOL vault and sign a CPI with seeds&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 74:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f17eb-4a8d-1431-746a-0b2e037b6c31" rel="noopener noreferrer"&gt;Call one custom Anchor program from another through its IDL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 75:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1806-4c0d-27a9-66f4-599a3c4c4f79" rel="noopener noreferrer"&gt;Break working CPIs and debug signer, account, and program errors&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 76:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1826-6ce3-acfb-f786-2f34571083f4" rel="noopener noreferrer"&gt;Write a clear public explanation of how Cross-Program Invocations work&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 77:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f1835-8e6d-27d6-4264-d40d141c8257" rel="noopener noreferrer"&gt;Share your composed program with code, tests, and on-chain evidence&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>learning</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Arc 10 Catch-Up: Designing Solana State with PDAs</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Thu, 16 Jul 2026 11:18:42 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/arc-10-catch-up-designing-solana-state-with-pdas-44if</link>
      <guid>https://dev.to/100daysofsolana/arc-10-catch-up-designing-solana-state-with-pdas-44if</guid>
      <description>&lt;p&gt;Arc 10 covered Days 64–70 of Epoch 3, and it was all about Program Derived Addresses.&lt;/p&gt;

&lt;p&gt;Arc 9 gave us our first Solana program.&lt;/p&gt;

&lt;p&gt;We built a counter, stored its state in an account, restricted updates to the correct authority, and used LiteSVM to prove those rules worked.&lt;/p&gt;

&lt;p&gt;But the counter account had a limitation.&lt;/p&gt;

&lt;p&gt;It used a randomly generated keypair for its address.&lt;/p&gt;

&lt;p&gt;That is manageable in a small exercise. The client creates the account, keeps hold of its public key, and passes that address back whenever it wants to increment the counter.&lt;/p&gt;

&lt;p&gt;It becomes awkward once we want one counter per user.&lt;/p&gt;

&lt;p&gt;Where do we store all those addresses? How does a user find their counter from another device? How does the program know that the account it received is the correct counter for that wallet?&lt;/p&gt;

&lt;p&gt;Arc 10 answered those questions with Program Derived Addresses, or PDAs.&lt;/p&gt;

&lt;p&gt;A PDA gives a Solana program a predictable way to create and locate state. Its address comes from the program ID and a set of seeds chosen by the program. For our counter, that meant deriving an address from the word &lt;code&gt;counter&lt;/code&gt; and the user's public key.&lt;/p&gt;

&lt;p&gt;The same inputs always produce the same address. Different users produce different addresses.&lt;/p&gt;

&lt;p&gt;That makes PDA design part of both the program's state model and its security model.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDA gives program state a predictable address
&lt;/h2&gt;

&lt;p&gt;Before Arc 10, our client created the counter account using a fresh keypair.&lt;/p&gt;

&lt;p&gt;That gave the account a unique address, but there was no relationship between the address and what the account represented.&lt;/p&gt;

&lt;p&gt;The address did not tell us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which program used the account&lt;/li&gt;
&lt;li&gt;which user owned the counter&lt;/li&gt;
&lt;li&gt;whether it was the correct counter for a particular instruction&lt;/li&gt;
&lt;li&gt;how to find it again without saving the address somewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PDAs solve this by deriving an address from known inputs.&lt;/p&gt;

&lt;p&gt;In JavaScript, we can derive one using &lt;code&gt;findProgramAddressSync&lt;/code&gt;:&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;counterPda&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;The inputs here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the program ID&lt;/li&gt;
&lt;li&gt;the seed &lt;code&gt;counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a bump value found during derivation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given those same inputs, we get the same PDA every time.&lt;/p&gt;

&lt;p&gt;That is the first important property: determinism.&lt;/p&gt;

&lt;p&gt;We can close the script, run it again tomorrow, and derive the same address without storing it in a database or configuration file.&lt;/p&gt;

&lt;p&gt;The second important property is that every byte matters.&lt;/p&gt;

&lt;p&gt;Changing the seed from &lt;code&gt;counter&lt;/code&gt; to &lt;code&gt;alice&lt;/code&gt; produces a completely different address:&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;alicePda&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;alice&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;Changing it again to &lt;code&gt;bob&lt;/code&gt; produces another:&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;bobPda&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;bob&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;Restoring the original seed gives us the original PDA again.&lt;/p&gt;

&lt;p&gt;That small experiment made the model tangible.&lt;/p&gt;

&lt;p&gt;A PDA is a deterministic address calculated from program-specific inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDA has no private key
&lt;/h2&gt;

&lt;p&gt;There is another important difference between a PDA and an ordinary wallet address.&lt;/p&gt;

&lt;p&gt;A normal Solana keypair contains a public key and a private key. The private key can sign transactions on behalf of the public key.&lt;/p&gt;

&lt;p&gt;A PDA is deliberately derived so that it falls outside the ed25519 curve used for Solana keypairs.&lt;/p&gt;

&lt;p&gt;That means there is no corresponding private key.&lt;/p&gt;

&lt;p&gt;Nobody can generate a secret key for the PDA and sign as it directly.&lt;/p&gt;

&lt;p&gt;Instead, the program associated with the PDA can authorise actions for it by supplying the same seeds and bump used to derive it.&lt;/p&gt;

&lt;p&gt;This is especially useful when a program needs to control assets or call another program through a Cross-Program Invocation. The PDA can act as an authority without requiring someone to store and protect another private key.&lt;/p&gt;

&lt;p&gt;We did not need all of that capability for our counter, but the underlying rule matters:&lt;/p&gt;

&lt;p&gt;The program controls the derivation logic, and the derivation logic determines which address the program recognises.&lt;/p&gt;

&lt;h2&gt;
  
  
  A PDA is an address before it is an account
&lt;/h2&gt;

&lt;p&gt;One easy mistake is to talk about deriving a PDA as though we have created an account.&lt;/p&gt;

&lt;p&gt;We have not.&lt;/p&gt;

&lt;p&gt;Derivation only calculates an address.&lt;/p&gt;

&lt;p&gt;We can derive a PDA that has never held any data, lamports, tokens, or executable code. Until an instruction creates an account at that address, there is nothing there to fetch.&lt;/p&gt;

&lt;p&gt;That gives us two separate steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Derive the address.&lt;/li&gt;
&lt;li&gt;Initialise an account at that address.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anchor helps us combine those steps when the account is first created.&lt;/p&gt;

&lt;p&gt;But the distinction is useful when debugging.&lt;/p&gt;

&lt;p&gt;If we derive the correct PDA and &lt;code&gt;getAccountInfo&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, that does not necessarily mean the derivation failed. It may simply mean the account has not been initialised yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the user creates one counter per wallet
&lt;/h2&gt;

&lt;p&gt;The fixed seed &lt;code&gt;[b"counter"]&lt;/code&gt; gives us one address for the entire program.&lt;/p&gt;

&lt;p&gt;That could be useful for a global account, but it cannot give every user their own counter.&lt;/p&gt;

&lt;p&gt;To do that, we added the user's public key to the seed list:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Now the PDA depends on both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the fixed namespace &lt;code&gt;counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the public key of the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wallet A gets an address derived from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"counter" + wallet A public key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wallet B gets an address derived from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"counter" + wallet B public key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the public keys differ, the resulting PDAs differ.&lt;/p&gt;

&lt;p&gt;That gives us a predictable one-counter-per-wallet model.&lt;/p&gt;

&lt;p&gt;The client does not need to create a random keypair for the counter.&lt;/p&gt;

&lt;p&gt;It does not need to store the counter address in local storage.&lt;/p&gt;

&lt;p&gt;It does not need to query a registry to discover which account belongs to the user.&lt;/p&gt;

&lt;p&gt;It can derive the address again whenever it needs it.&lt;/p&gt;

&lt;p&gt;In Web2 terms, it is similar to locating a row using a known composite key. The program defines the namespace, the wallet identifies the user, and the combination identifies the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anchor can create the account at the derived address
&lt;/h2&gt;

&lt;p&gt;We expressed that seed scheme inside our Anchor accounts struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Accounts)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;InitializeCounter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[account(&lt;/span&gt;
        &lt;span class="nd"&gt;init,&lt;/span&gt;
        &lt;span class="nd"&gt;payer&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;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;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="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;Several constraints work together here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init&lt;/code&gt; tells Anchor to create the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;payer = user&lt;/code&gt; says the user will fund the account's rent-exempt lamport deposit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;space = 8 + Counter::INIT_SPACE&lt;/code&gt; allocates enough storage for the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;seeds&lt;/code&gt; declares how the PDA should be derived.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bump&lt;/code&gt; tells Anchor to find and use the canonical bump for that derivation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;space&lt;/code&gt; value deserves a moment of attention. With &lt;code&gt;InitSpace&lt;/code&gt;, Anchor calculates the size of the account's fields for us, but the allocation still needs an extra eight bytes for Anchor's account discriminator. The discriminator identifies which Anchor account type the data belongs to, and it helps prevent one account type from being deserialised as another simply because its bytes happen to have a compatible shape. Getting this wrong can stop the account from initialising or leave too little space for its data.&lt;/p&gt;

&lt;p&gt;The account itself stores the user, the count, and the bump:&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;user&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="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;bump&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initialisation handler can then populate those fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;initialize_counter&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;InitializeCounter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.user&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="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&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.counter&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;Anchor has already checked the account address and created the account before this handler runs.&lt;/p&gt;

&lt;p&gt;The handler only needs to write the initial state.&lt;/p&gt;

&lt;p&gt;That follows the same pattern we saw in Arc 9:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the accounts struct describes the required accounts and validates them&lt;/li&gt;
&lt;li&gt;the handler performs the instruction's state change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bump completes the derivation
&lt;/h2&gt;

&lt;p&gt;The bump can feel mysterious when we first encounter it.&lt;/p&gt;

&lt;p&gt;Solana needs PDAs to be off the ed25519 curve so that no private key exists for them.&lt;/p&gt;

&lt;p&gt;The derivation process starts with the program ID and the seeds, then tries bump values until it finds an address that satisfies that requirement.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;findProgramAddressSync&lt;/code&gt; returns both results:&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;counterPda&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;user&lt;/span&gt;&lt;span class="p"&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;toBuffer&lt;/span&gt;&lt;span class="p"&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;Anchor normally handles this search for us.&lt;/p&gt;

&lt;p&gt;When we write a bare &lt;code&gt;bump&lt;/code&gt; during initialisation, Anchor runs the search, finds the canonical bump, and makes it available through &lt;code&gt;ctx.bumps.counter&lt;/code&gt; so we can store it in the account.&lt;/p&gt;

&lt;p&gt;That stored value is why later instructions use a different form:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Anchor: do not run the search again. Take the stored bump, perform a single derivation with it, and check the result against the supplied account.&lt;/p&gt;

&lt;p&gt;The search loop costs compute units, and it would otherwise run on every instruction that touches the account. Storing the bump once at initialisation and reusing it turns that repeated search into a single calculation.&lt;/p&gt;

&lt;p&gt;So the bump is part of how Solana finds a valid off-curve address, and storing it is a small optimisation the program pays for once and benefits from on every subsequent instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeds are validation rules
&lt;/h2&gt;

&lt;p&gt;At first, PDAs can look like an address-generation convenience.&lt;/p&gt;

&lt;p&gt;They save the client from keeping track of random account addresses.&lt;/p&gt;

&lt;p&gt;That is useful, but it is only half of the story.&lt;/p&gt;

&lt;p&gt;The same seeds also let Anchor verify that an instruction received the correct account.&lt;/p&gt;

&lt;p&gt;Our increment instruction used the counter's PDA constraints again:&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(&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="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="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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the handler runs, Anchor can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the user's public key.&lt;/li&gt;
&lt;li&gt;Combine it with the &lt;code&gt;counter&lt;/code&gt; seed.&lt;/li&gt;
&lt;li&gt;Re-derive the expected PDA.&lt;/li&gt;
&lt;li&gt;Compare that address with the supplied counter account.&lt;/li&gt;
&lt;li&gt;Check that the account's stored &lt;code&gt;user&lt;/code&gt; field matches the signer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of those checks fail, the transaction is rejected before the instruction changes state.&lt;/p&gt;

&lt;p&gt;Strictly speaking, the seed constraint alone already binds this counter to the signer, because the signer's public key is part of the derivation. The &lt;code&gt;has_one = user&lt;/code&gt; check overlaps with it here, acting as defence in depth. Where &lt;code&gt;has_one&lt;/code&gt; really earns its keep is when the seeds do not include the related key, and the stored relationship is the only thing tying the account to a signer. We will see that shortly with the config account.&lt;/p&gt;

&lt;p&gt;Either way, the program does not need to repeat address checks inside every handler.&lt;/p&gt;

&lt;p&gt;The account constraints declare the invariant once:&lt;/p&gt;

&lt;p&gt;This instruction accepts the counter derived for this user, and that counter must record the same user internally.&lt;/p&gt;

&lt;p&gt;The handler can then remain focused on the state change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Increment&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="py"&gt;.count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;
        &lt;span class="py"&gt;.count&lt;/span&gt;
        &lt;span class="nf"&gt;.checked_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ProgramError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArithmeticOverflow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The seed constraint proves we received the expected address.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;has_one&lt;/code&gt; constraint proves the account data records the expected relationship.&lt;/p&gt;

&lt;p&gt;Together, they protect the counter from substitution.&lt;/p&gt;

&lt;h2&gt;
  
  
  A spoofing attempt showed the security boundary
&lt;/h2&gt;

&lt;p&gt;We tested those constraints by deliberately passing the wrong account.&lt;/p&gt;

&lt;p&gt;Wallet A had its own counter PDA.&lt;/p&gt;

&lt;p&gt;Wallet B had a different counter PDA.&lt;/p&gt;

&lt;p&gt;Then Wallet A called &lt;code&gt;increment&lt;/code&gt; while supplying Wallet B's counter account.&lt;/p&gt;

&lt;p&gt;Without PDA validation, the program might have accepted any account with the right data shape.&lt;/p&gt;

&lt;p&gt;With the seed constraint in place, Anchor derived the counter address expected for Wallet A and compared it with the supplied address.&lt;/p&gt;

&lt;p&gt;They did not match.&lt;/p&gt;

&lt;p&gt;The instruction failed before the handler executed.&lt;/p&gt;

&lt;p&gt;That test made the security role of PDAs much clearer.&lt;/p&gt;

&lt;p&gt;The PDA is not secure merely because its address looks unusual. The protection comes from the program declaring the correct derivation and validating that derivation whenever the account is used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global state needs a different seed scheme
&lt;/h2&gt;

&lt;p&gt;The per-user counter gave each wallet its own state.&lt;/p&gt;

&lt;p&gt;Real applications often need another category of state: information that applies to the whole program.&lt;/p&gt;

&lt;p&gt;For that, we added a configuration PDA:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this derivation does not include a user public key, every caller derives the same address.&lt;/p&gt;

&lt;p&gt;That makes it suitable for singleton state.&lt;/p&gt;

&lt;p&gt;Our config account stored fields such as:&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;Config&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;admin&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;paused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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;total_counters&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;pub&lt;/span&gt; &lt;span class="n"&gt;bump&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the program had two kinds of state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one global configuration account&lt;/li&gt;
&lt;li&gt;one counter account per user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The config account could hold program-wide policy.&lt;/p&gt;

&lt;p&gt;The counter account could hold user-specific data.&lt;/p&gt;

&lt;p&gt;The two can also meet in a single instruction. Our &lt;code&gt;initialize_counter&lt;/code&gt; handler incremented &lt;code&gt;config.total_counters&lt;/code&gt; alongside creating the user's counter, so one transaction touched both the global account and the per-user account. That pattern, one instruction updating global and scoped state together, appears constantly in real programs.&lt;/p&gt;

&lt;p&gt;A program might have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one global marketplace configuration&lt;/li&gt;
&lt;li&gt;one position per trader&lt;/li&gt;
&lt;li&gt;one vault per token mint&lt;/li&gt;
&lt;li&gt;one profile per wallet&lt;/li&gt;
&lt;li&gt;one pool account per asset pair&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The seed scheme tells us which state is shared and which state is scoped to a particular entity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints let us express relationships and business rules
&lt;/h2&gt;

&lt;p&gt;As the program gained more state, Anchor constraints became its main validation language.&lt;/p&gt;

&lt;p&gt;For example:&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;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"config"&lt;/span&gt;&lt;span class="nd"&gt;]&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;config&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;admin&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;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;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;has_one = admin&lt;/code&gt; checks that the public key stored in &lt;code&gt;config.admin&lt;/code&gt; matches the signer passed as &lt;code&gt;admin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice that the config seeds contain no user key, so the derivation alone cannot tell us who is allowed to administer the account. The stored &lt;code&gt;admin&lt;/code&gt; field is the only link, and &lt;code&gt;has_one&lt;/code&gt; is what enforces it. This is the case where &lt;code&gt;has_one&lt;/code&gt; is doing essential work rather than doubling up on the seed check.&lt;/p&gt;

&lt;p&gt;If the counter accounts are rows keyed by user ID, the config account is the settings table: one row, program-wide, with a recorded owner. And &lt;code&gt;has_one&lt;/code&gt; behaves much like a foreign-key check, verifying that a stored reference actually points at the account the instruction received. The difference from a Web2 database is that these checks execute on-chain, as part of every transaction.&lt;/p&gt;

&lt;p&gt;We could also enforce the pause rule directly on the account. To do that, the increment instruction's accounts struct gains the config account alongside the counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[account(&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"config"&lt;/span&gt;&lt;span class="nd"&gt;]&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;config&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;constraint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="py"&gt;.paused&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;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;p&gt;Because the config account is now part of the instruction's required accounts, its constraints run during account validation, and an increment fails while the program is paused.&lt;/p&gt;

&lt;p&gt;The handler does not need to contain a separate conditional:&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;if&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="py"&gt;.paused&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nd"&gt;err!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CounterError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ProgramPaused&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not mean every business rule belongs in an account constraint. Some rules are clearer inside the handler.&lt;/p&gt;

&lt;p&gt;But Arc 10 showed how much of the program's structure can be expressed declaratively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seeds&lt;/code&gt; identifies the expected PDA&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bump&lt;/code&gt; completes its derivation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has_one&lt;/code&gt; checks a stored relationship&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constraint&lt;/code&gt; applies a custom condition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mut&lt;/code&gt; marks accounts whose state may change&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Signer&lt;/code&gt; proves that the required wallet authorised the transaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These constraints run before the handler.&lt;/p&gt;

&lt;p&gt;The instruction only reaches its state-changing logic after the required accounts and relationships have been validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Singleton initialisation needs an explicit governance decision
&lt;/h2&gt;

&lt;p&gt;Our exercise allowed the first caller of &lt;code&gt;init_config&lt;/code&gt; to become the administrator.&lt;/p&gt;

&lt;p&gt;That kept the initialisation code approachable and let us focus on PDAs and constraints.&lt;/p&gt;

&lt;p&gt;It should not be copied into a production program without considering the consequences.&lt;/p&gt;

&lt;p&gt;If anyone can initialise the singleton config account, then whoever reaches it first may become the program administrator.&lt;/p&gt;

&lt;p&gt;A production deployment should define who is allowed to perform that initial setup.&lt;/p&gt;

&lt;p&gt;Depending on the program, that authority might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a known deployment wallet&lt;/li&gt;
&lt;li&gt;the program's upgrade authority&lt;/li&gt;
&lt;li&gt;a multisig&lt;/li&gt;
&lt;li&gt;an existing governance account&lt;/li&gt;
&lt;li&gt;another PDA with its own initialisation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The correct design depends on how the program will be governed.&lt;/p&gt;

&lt;p&gt;The important lesson is that a singleton PDA gives us one canonical address. It does not decide who should be allowed to create or control the account at that address.&lt;/p&gt;

&lt;p&gt;That decision still belongs to the program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing an account completes the state lifecycle
&lt;/h2&gt;

&lt;p&gt;Creating an account costs lamports.&lt;/p&gt;

&lt;p&gt;The payer deposits enough lamports to make the account rent-exempt, and those lamports remain attached to the account while it exists.&lt;/p&gt;

&lt;p&gt;When the state is no longer needed, the program can close the account and return those lamports.&lt;/p&gt;

&lt;p&gt;We added a &lt;code&gt;close_counter&lt;/code&gt; instruction using Anchor's &lt;code&gt;close&lt;/code&gt; constraint:&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;CloseCounter&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"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="n"&gt;close&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="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nd"&gt;#[account(mut)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The familiar constraints still protect the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; verify that this is the user's canonical counter PDA.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_one = user&lt;/code&gt; checks the ownership relationship stored inside the account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;close = user&lt;/code&gt; tells Anchor where to send the account's remaining lamports.&lt;/p&gt;

&lt;p&gt;The handler itself can be empty:&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;close_counter&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;CloseCounter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;Once the instruction completes, Anchor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfers the account's lamports to the user&lt;/li&gt;
&lt;li&gt;invalidates the account data&lt;/li&gt;
&lt;li&gt;leaves no live account at that PDA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can still derive the same address later because the derivation inputs have not changed.&lt;/p&gt;

&lt;p&gt;But fetching the account will show that it no longer exists.&lt;/p&gt;

&lt;p&gt;That makes closing a PDA-backed account feel different from deleting a row in a normal database. We are releasing on-chain storage and returning its refundable deposit.&lt;/p&gt;

&lt;p&gt;It also shows the full lifecycle of program state:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Derive the address.&lt;/li&gt;
&lt;li&gt;Initialise the account.&lt;/li&gt;
&lt;li&gt;Read and mutate its data.&lt;/li&gt;
&lt;li&gt;Enforce who may use it.&lt;/li&gt;
&lt;li&gt;Close it when it is no longer needed.&lt;/li&gt;
&lt;li&gt;Recover the lamports that funded it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Seed design defines the state namespace
&lt;/h2&gt;

&lt;p&gt;The experimentation day made us change the seeds and observe the consequences.&lt;/p&gt;

&lt;p&gt;First, we derived counters for two users using:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The two wallets produced two different addresses.&lt;/p&gt;

&lt;p&gt;That is the behaviour we wanted.&lt;/p&gt;

&lt;p&gt;Then we removed the user public key and derived both counters from:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Both wallets now produced the same address.&lt;/p&gt;

&lt;p&gt;There was no cryptographic collision. The derivation worked exactly as designed.&lt;/p&gt;

&lt;p&gt;The problem was our design.&lt;/p&gt;

&lt;p&gt;We had asked the program for one global &lt;code&gt;counter&lt;/code&gt; address, then tried to use it as though every user had their own.&lt;/p&gt;

&lt;p&gt;Only the first account could be initialised there. The second user would attempt to initialise an address that was already occupied.&lt;/p&gt;

&lt;p&gt;This is one of the most important lessons from the arc.&lt;/p&gt;

&lt;p&gt;Seed design determines the namespace of the program's state.&lt;/p&gt;

&lt;p&gt;Including a user public key creates user-scoped state.&lt;/p&gt;

&lt;p&gt;Including a mint creates mint-scoped state.&lt;/p&gt;

&lt;p&gt;Including two asset addresses could create one account per pair.&lt;/p&gt;

&lt;p&gt;Using only a fixed seed creates a singleton.&lt;/p&gt;

&lt;p&gt;The program will follow the scheme exactly, even when the scheme does not match the application we intended to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Almost identical seeds are still different seeds
&lt;/h2&gt;

&lt;p&gt;We also tried small changes to the seed bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;counter
counters
Counter
counter\0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each produced a different PDA.&lt;/p&gt;

&lt;p&gt;Those values may look related to a person, but the derivation process does not interpret their meaning.&lt;/p&gt;

&lt;p&gt;It only sees bytes.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uppercase and lowercase values differ&lt;/li&gt;
&lt;li&gt;singular and plural values differ&lt;/li&gt;
&lt;li&gt;invisible terminators or extra bytes matter&lt;/li&gt;
&lt;li&gt;changing seed order changes the result&lt;/li&gt;
&lt;li&gt;changing the program ID changes the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes seed conventions worth planning.&lt;/p&gt;

&lt;p&gt;Once a program is deployed and accounts have been created, changing a seed can make the new version derive different addresses from the accounts that already exist.&lt;/p&gt;

&lt;p&gt;The old state is still on-chain, but the updated program may no longer look for it at the same address.&lt;/p&gt;

&lt;p&gt;So seeds should be treated as part of the program's persistent interface, rather than temporary labels we can rename without consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the explanation exposed the gaps
&lt;/h2&gt;

&lt;p&gt;The last two days moved away from program code.&lt;/p&gt;

&lt;p&gt;First, we wrote a long-form explanation of PDAs.&lt;/p&gt;

&lt;p&gt;That meant describing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why random account keypairs become inconvenient&lt;/li&gt;
&lt;li&gt;how seeds and program IDs produce deterministic addresses&lt;/li&gt;
&lt;li&gt;what the bump does&lt;/li&gt;
&lt;li&gt;why PDAs have no private keys&lt;/li&gt;
&lt;li&gt;why deriving an address does not create an account&lt;/li&gt;
&lt;li&gt;how Anchor validates PDA constraints&lt;/li&gt;
&lt;li&gt;how seed design affects ownership and account scope&lt;/li&gt;
&lt;li&gt;how PDA-backed accounts can be closed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we turned that explanation into a shorter social post or thread.&lt;/p&gt;

&lt;p&gt;The public version needed a useful example and some evidence from the work: code, test output, terminal output, or an Explorer screenshot.&lt;/p&gt;

&lt;p&gt;That final step matters because PDAs can feel clear while we are following an exercise and become much harder to explain without the scaffolding.&lt;/p&gt;

&lt;p&gt;Writing the model in plain language forces us to answer the questions that code alone can hide.&lt;/p&gt;

&lt;p&gt;Why is the address predictable?&lt;/p&gt;

&lt;p&gt;Why can nobody hold its private key?&lt;/p&gt;

&lt;p&gt;Why does adding a wallet public key produce one account per user?&lt;/p&gt;

&lt;p&gt;Why does a seed constraint prevent account substitution?&lt;/p&gt;

&lt;p&gt;Why does the address remain derivable after the account is closed?&lt;/p&gt;

&lt;p&gt;If we cannot answer those questions clearly, we probably need another pass through the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arc 10 taught us
&lt;/h2&gt;

&lt;p&gt;Arc 10 turned account addresses into part of the program design.&lt;/p&gt;

&lt;p&gt;By the end of the arc, we had seen how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;derive a PDA from a program ID, seeds, and a bump&lt;/li&gt;
&lt;li&gt;verify that the same inputs always produce the same address&lt;/li&gt;
&lt;li&gt;create one state account per user&lt;/li&gt;
&lt;li&gt;initialise an Anchor account at a PDA&lt;/li&gt;
&lt;li&gt;distinguish a derived address from an initialised account&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;seeds&lt;/code&gt; and &lt;code&gt;bump&lt;/code&gt; to validate an account&lt;/li&gt;
&lt;li&gt;store the bump once and reuse it to save compute&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;has_one&lt;/code&gt; to bind stored state to a signer&lt;/li&gt;
&lt;li&gt;combine global configuration with per-user state&lt;/li&gt;
&lt;li&gt;enforce a pause rule before the handler runs&lt;/li&gt;
&lt;li&gt;close an account and reclaim its lamports&lt;/li&gt;
&lt;li&gt;test account-substitution attempts&lt;/li&gt;
&lt;li&gt;observe how incomplete seed schemes create logical address conflicts&lt;/li&gt;
&lt;li&gt;explain PDA design in public&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The counter still performed the same basic job as it did in Arc 9.&lt;/p&gt;

&lt;p&gt;It initialised a number, stored an owner, and allowed that number to be incremented.&lt;/p&gt;

&lt;p&gt;But the surrounding design became much more useful.&lt;/p&gt;

&lt;p&gt;Each wallet could find its counter without saving a random address.&lt;/p&gt;

&lt;p&gt;The program could prove that the supplied counter belonged in that user's namespace.&lt;/p&gt;

&lt;p&gt;Global policy lived at one predictable config address.&lt;/p&gt;

&lt;p&gt;User state lived at a different predictable address for every wallet.&lt;/p&gt;

&lt;p&gt;And when the state was no longer needed, the user could close it and recover the lamports used to create it.&lt;/p&gt;

&lt;p&gt;That is the lasting lesson from Arc 10.&lt;/p&gt;

&lt;p&gt;PDA design is state-model design.&lt;/p&gt;

&lt;p&gt;The seeds decide what state exists, how many instances can exist, what each instance belongs to, and how the program can locate and validate it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisit the Arc 10 challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 64:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019eefab-9ea1-8eb3-2534-15e1ae96435d" rel="noopener noreferrer"&gt;Derive your first Program Derived Address and see how seeds change the result&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 65:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019eefe7-365e-7146-2928-4ce4f6270721" rel="noopener noreferrer"&gt;Replace random counter keypairs with one predictable PDA per user&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 66:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ef011-f8d2-14bc-753f-f67456e0039c" rel="noopener noreferrer"&gt;Add a global configuration PDA and enforce admin and pause rules&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 67:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ef02b-7b80-7dc2-e9b7-8a0875c844b1" rel="noopener noreferrer"&gt;Close a user's counter and return its rent-exempt lamports&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 68:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019ef03d-6aed-7a5e-bc15-fb93f3992732" rel="noopener noreferrer"&gt;Break the seed scheme and test whether account spoofing succeeds&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 69:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f0424-79a6-431c-f492-9aa6ced5f3ac" rel="noopener noreferrer"&gt;Write a long-form explanation of how PDAs shape program state&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Day 70:&lt;/strong&gt; &lt;a href="https://www.mlh.com/events/100-days-of-solana/challenges/019f04a8-8ebc-44f2-ec7d-d3e642ec1d97" rel="noopener noreferrer"&gt;Turn your PDA explanation into a public post with evidence from your work&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>learning</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Welcome to Epoch 4: Shipping and Exploring</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:34:40 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/welcome-to-epoch-4-shipping-and-exploring-1080</link>
      <guid>https://dev.to/100daysofsolana/welcome-to-epoch-4-shipping-and-exploring-1080</guid>
      <description>&lt;p&gt;Arc 13 of 100 Days of Solana starts today and it opens Epoch 4: Ship and Explore.&lt;/p&gt;

&lt;p&gt;Every tutorial has the same ending. The program builds, the tests pass, it runs on devnet, and the article stops. Which is a strange place to stop, because that's exactly where a Web2 developer would say the interesting part begins: the production deploy.&lt;/p&gt;

&lt;p&gt;Arc 13 is seven days on that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mainnet is not just another cluster
&lt;/h2&gt;

&lt;p&gt;On devnet, everything is free and nothing is permanent. Airdropped SOL, throwaway deploys, program addresses nobody will ever call. It's staging, and it behaves like staging.&lt;/p&gt;

&lt;p&gt;Mainnet-beta changes four things at once. The deploy costs real SOL — program accounts are rent-exempt, and for a non-trivial Anchor program that's a real (if modest) amount of money. The program address becomes public infrastructure: anyone can find it, read it, and call it. Whoever holds the upgrade authority can replace the program's code entirely, which makes that keypair equivalent to root credentials on a production box. And there's no access control at the network level — strangers can send transactions to your program the moment it lands.&lt;/p&gt;

&lt;p&gt;None of this is scary if you've run production systems before. It's the same discipline — deploy intentionally, control who holds the keys, publish a contract for integrators, handle failure states in front of users — wearing different names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rosetta Stone
&lt;/h2&gt;

&lt;p&gt;If you know the Web2 column, Arc 13 teaches you the Solana column:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Web2&lt;/th&gt;
&lt;th&gt;Solana&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Staging → production&lt;/td&gt;
&lt;td&gt;Devnet → mainnet-beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release artifact&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anchor build&lt;/code&gt; output (the &lt;code&gt;.so&lt;/code&gt; file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production deploy rights&lt;/td&gt;
&lt;td&gt;Upgrade authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch protection / required reviewers&lt;/td&gt;
&lt;td&gt;Multisig upgrade authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAPI / GraphQL schema&lt;/td&gt;
&lt;td&gt;IDL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated typed SDK&lt;/td&gt;
&lt;td&gt;Codama client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Sign in with Google"&lt;/td&gt;
&lt;td&gt;Wallet Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public changelog / release page&lt;/td&gt;
&lt;td&gt;Explorer link&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one that surprises most people is the IDL. Solana programs don't self-describe the way a REST API with an OpenAPI spec does — unless you publish the IDL, at which point other developers can generate typed clients, inspect your instructions, and integrate without reading your source. Publishing it is the difference between a deployed program and a usable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven days
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 85&lt;/strong&gt; — Deploy an Anchor program to mainnet-beta. The staging-to-production promotion, done deliberately rather than by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 86&lt;/strong&gt; — Decide who is allowed to upgrade the program. Keep the authority, transfer it to a multisig, or burn it entirely — each is a real product decision with real trade-offs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 87&lt;/strong&gt; — Publish the IDL and generate a typed client, so other developers can call the program without guessing at byte layouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 88&lt;/strong&gt; — Connect a wallet from the frontend and send a transaction. This is where a human finally meets the program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 89&lt;/strong&gt; — Write useful messages for failed transactions. Wallet errors are production UX, not an edge case to be polished later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 90&lt;/strong&gt; — Write a production launch checklist. The artifact future-you actually needs at 11pm before the next deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 91&lt;/strong&gt; — Launch publicly, with an explorer link. A deploy isn't done until someone else can verify it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you'll have at the end
&lt;/h2&gt;

&lt;p&gt;A program on mainnet-beta, a frontend that talks to it, and a public launch post backed by a verifiable on-chain receipt. Not "I completed a course" — "here's my program ID, go look."&lt;/p&gt;

&lt;p&gt;That receipt matters more than it might seem. In Web2, a side project claim is just a claim unless it's deployed somewhere. On Solana, the explorer link is proof: the deploy transaction, the upgrade authority, the program's activity, all publicly inspectable. It's the strongest possible artifact for a portfolio or a launch post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Joining
&lt;/h2&gt;

&lt;p&gt;Interested but not started yet? No problem. You can learn the fundamentals at your own pace with our previous 84 challenges!&lt;/p&gt;

&lt;p&gt;Start here: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;https://mlh.link/solana-100&lt;/a&gt;&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>learning</category>
    </item>
    <item>
      <title>From Devnet to Mainnet: What Changes When Your Solana Program Goes Live</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 10 Jul 2026 21:46:13 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/from-devnet-to-mainnet-what-changes-when-your-solana-program-goes-live-hg2</link>
      <guid>https://dev.to/100daysofsolana/from-devnet-to-mainnet-what-changes-when-your-solana-program-goes-live-hg2</guid>
      <description>&lt;p&gt;There's a moment in every Solana project where the work stops being about whether the program &lt;em&gt;works&lt;/em&gt; and starts being about whether it's &lt;em&gt;ready&lt;/em&gt;. You've tested it, the logic holds, the constraints are tight. Then you point it at mainnet, and a different set of questions shows up: questions about money, permanence, and strangers.&lt;/p&gt;

&lt;p&gt;This post is about that transition. Not the commands, which are short and well documented, but the shift in what you're responsible for once real users can touch your code. If you've been building on devnet and you're starting to think about a live launch, this is the mental model to carry in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Devnet was a sandbox. Mainnet is not.
&lt;/h2&gt;

&lt;p&gt;Devnet is a practice field. The SOL is free, you airdrop more whenever you run low, and if you deploy something broken, the only casualty is your afternoon. That safety is the whole point of devnet: it lets you fail cheaply and often, which is exactly how you should be learning.&lt;/p&gt;

&lt;p&gt;Mainnet removes the safety net, and three things change the moment you cross over.&lt;/p&gt;

&lt;p&gt;The SOL is real. Deploying a program allocates an on-chain account sized to your compiled binary, and you pay rent for that space in actual SOL. Larger programs cost more. This isn't a huge sum for a typical program, but it's real money leaving a real wallet, and that alone tends to sharpen how carefully you check things before you hit deploy.&lt;/p&gt;

&lt;p&gt;The audience is real. On devnet the only person calling your program is you. On mainnet, anyone can find your program and send it any transaction they like, the moment it's live. Everything from the security arc stops being theoretical: the accounts strangers pass in, the inputs you didn't expect, the edge cases you hoped no one would hit. Mainnet is where "every account is attacker-controlled until proven otherwise" becomes a live condition rather than a lesson.&lt;/p&gt;

&lt;p&gt;The mistakes are visible. A bad devnet deploy disappears into the noise. A bad mainnet deploy is a public event, on a permanent ledger, in front of the users you were hoping to attract. This is a feature as much as a risk: the same permanence that raises the stakes is what lets users trust the program without trusting you personally.&lt;/p&gt;

&lt;p&gt;None of this should scare you off. It's just the difference between a rehearsal and opening night, and the way you prepare for opening night is by knowing which decisions are reversible and which are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-way door: upgrade authority
&lt;/h2&gt;

&lt;p&gt;Here's the single most important concept to understand before you launch, because it's the one decision on Solana that you genuinely cannot take back.&lt;/p&gt;

&lt;p&gt;When you deploy a program, your wallet becomes its &lt;strong&gt;upgrade authority&lt;/strong&gt;. That's the key that lets you push new versions of the program later, fixing bugs, adding features, changing logic. As long as you hold the upgrade authority, your deployed program is mutable: you can replace its code.&lt;/p&gt;

&lt;p&gt;That power cuts both ways, and it's the central tension of shipping on Solana:&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;keep&lt;/em&gt; the upgrade authority, you can fix bugs after launch, which is a real safety valve. But your users have to trust that you won't push a malicious update that drains them. An upgradeable program is only as trustworthy as whoever holds its authority.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;revoke&lt;/em&gt; the upgrade authority, the program becomes immutable and can never be changed again. Users get maximum trust, the code is the code, forever, but you've also given up your ability to patch a bug you discover next week. Revoking is permanent: there is no undo, no recovery, no support ticket that brings it back.&lt;/p&gt;

&lt;p&gt;This is why teams rarely treat it as a binary. Many keep the authority early, when bugs are most likely, then move it to a multisig so no single person can push an update alone, and only consider full immutability once the program has been battle-tested and audited. The point isn't which choice is correct, it's that you make it deliberately, understanding that revocation is a door that only opens one way.&lt;/p&gt;

&lt;p&gt;You can always check who holds a program's authority, which matters because it's also how &lt;em&gt;you&lt;/em&gt; evaluate anyone else's program before trusting it with your funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your program is now a public API
&lt;/h2&gt;

&lt;p&gt;On devnet your program was a thing you called from your own test file. On mainnet it becomes infrastructure that other people's code talks to, and that reframes a few things worth previewing.&lt;/p&gt;

&lt;p&gt;Other developers need to know how to call your program. The typed interface that describes your instructions and accounts, the IDL, becomes a public contract. Publishing it lets others generate clients and build on top of you, the same way a REST API publishes its schema. Your program stops being private code and becomes something composable.&lt;/p&gt;

&lt;p&gt;Real users connect through wallets, not keypairs in a file. On devnet you signed with a local keypair. Real users have browser wallets, and connecting to them means speaking a shared standard so any wallet works, not just the one you happen to use. The interface between your app and a stranger's wallet is now part of your product.&lt;/p&gt;

&lt;p&gt;Users see errors you didn't write. When a transaction fails on mainnet, the user doesn't see your clean &lt;code&gt;require!&lt;/code&gt; message, they might see a raw RPC error, a rejected signature, an insufficient-funds failure. Part of shipping is catching those and turning them into something a human can understand, so a failed transaction is a clear message rather than a scary wall of text. The difference between a program and a product is often just this layer of care.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mindset to carry in
&lt;/h2&gt;

&lt;p&gt;The whole shift can be summed up in one sentence: on devnet you were proving the program works, and on mainnet you're taking responsibility for it in the world.&lt;/p&gt;

&lt;p&gt;That responsibility is mostly about knowing which decisions are reversible. Deploying is reversible, you can upgrade, redeploy, or close and reclaim your SOL while you hold the authority. Revoking the upgrade authority is not reversible. Neither is the ledger's memory of a launch. So the craft of shipping is sequencing: do the reversible things freely, approach the irreversible ones slowly, and never let a permanent decision be the one you made in a hurry.&lt;/p&gt;

&lt;p&gt;A short version of what "ready" looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your program does what it should, and refuses what it shouldn't, because mainnet is where the second half gets tested by strangers.&lt;/li&gt;
&lt;li&gt;You've decided, on purpose, what happens to your upgrade authority, and when.&lt;/li&gt;
&lt;li&gt;The people who'll build on or use your program can actually reach it: a published interface, a wallet connection, and errors that make sense.&lt;/li&gt;
&lt;li&gt;You know which of your launch steps you can undo and which you can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deployment feels like the finish line, but it's better to think of it as the handoff, the moment your program stops being yours alone and starts being something the world can use. Everything up to now was making it correct. Shipping is making it ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/programs/deploying" rel="noopener noreferrer"&gt;Solana program deployment docs&lt;/a&gt; cover the actual commands for deploying, transferring the upgrade authority, and making a program immutable, including how to check who currently holds authority over any program. The &lt;a href="https://solana.com/docs/core/programs/program-deployment" rel="noopener noreferrer"&gt;program deployment overview&lt;/a&gt; explains the upgrade mechanism itself: buffer accounts, the program data account, and what setting the authority to &lt;code&gt;None&lt;/code&gt; does. For the client side, &lt;a href="https://solana.com/docs/programs/codama/clients" rel="noopener noreferrer"&gt;Codama's client generation docs&lt;/a&gt; walk through turning a program's IDL into a typed client others can build with.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc walks this exact path end to end: promoting a program to mainnet, handling the upgrade-authority decision, publishing a client, wiring up a wallet, and launching with on-chain proof. Reading this first means the stakes will already make sense when you get there. Not joined yet? It's not too late to build alongside everyone: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>learning</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Account Security on Solana, Made Simple</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 03 Jul 2026 20:21:08 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/account-security-on-solana-made-simple-154d</link>
      <guid>https://dev.to/100daysofsolana/account-security-on-solana-made-simple-154d</guid>
      <description>&lt;p&gt;When you write a normal backend, you mostly trust your own database. A row you wrote is a row you can read back, and the data is what you put there. Solana works differently, and the difference is the single most important thing to understand about writing safe programs.&lt;/p&gt;

&lt;p&gt;On Solana, your program is handed a list of accounts with every instruction, and any of those accounts can be anything the caller wants. The caller picks them. An account is just an address, a balance, some bytes, and a field saying which program owns it, and a caller is free to hand your program an account they built themselves, filled with whatever bytes serve them. Your program has no trusted edge. Every account is attacker-controlled until your code proves otherwise.&lt;/p&gt;

&lt;p&gt;That sounds alarming, but it collapses into something manageable. Most account security on Solana comes down to two questions you ask about every account in every instruction. Learn to ask them and a whole class of expensive bugs stops being mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift: from "what did I mean" to "what did I forget to forbid"
&lt;/h2&gt;

&lt;p&gt;When you're building a feature, you think about the happy path: a user calls this, the program does that, everyone's content. Security asks a different question. Not "what did I intend this to do," but "what did I forget to forbid." Those are not the same question, and the gap between them is where exploits live.&lt;/p&gt;

&lt;p&gt;A useful way to hold it: when you write a program, you imagine the user you designed for. An attacker is every user you didn't. They will pass the account you didn't expect, sign with a key you didn't intend, and send the number you assumed no one would. Your job is to make the program say no to all of them, out loud, before it touches anything.&lt;/p&gt;

&lt;p&gt;The good news is you don't need to imagine every attack. Two questions cover most of the ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question one: who owns this account?
&lt;/h2&gt;

&lt;p&gt;If your program reads data off an account and trusts it, the program needs to know which program owns that account.&lt;/p&gt;

&lt;p&gt;Here's why it matters. Say your program has a &lt;code&gt;Config&lt;/code&gt; account that stores an admin's public key, and your withdraw logic reads &lt;code&gt;config.admin&lt;/code&gt; to decide who's allowed. If you read those bytes without checking who owns the account, an attacker can create their own account, write a &lt;code&gt;Config&lt;/code&gt; into it that names &lt;em&gt;themselves&lt;/em&gt; as admin, and hand it to your program. The bytes deserialize perfectly. The struct says what they want it to say. Your check passes, and they walk out with the funds.&lt;/p&gt;

&lt;p&gt;The defense is an owner check: confirm the account is owned by the program that's supposed to own it. A real &lt;code&gt;Config&lt;/code&gt; your program created is owned by your program. The attacker's forgery is owned by the System Program (or whatever they chose), so an owner check rejects it on sight.&lt;/p&gt;

&lt;p&gt;This exact gap, an account trusted without verifying what it was, is behind some of the largest losses in Solana's history. It isn't exotic cryptography. It's a missing question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question two: did the authority actually sign?
&lt;/h2&gt;

&lt;p&gt;The second question is about permission. If an account decides whether an action is allowed, an account named &lt;code&gt;authority&lt;/code&gt; or &lt;code&gt;admin&lt;/code&gt; or &lt;code&gt;owner&lt;/code&gt;, the program has to confirm that account actually signed the transaction.&lt;/p&gt;

&lt;p&gt;The trap here is subtle and worth slowing down for. It's tempting to check authority by comparing public keys: "does the admin field on this account equal the admin pubkey I expect?" But a public key is public. Anyone can put anyone's pubkey in a transaction. Comparing pubkeys only proves that someone &lt;em&gt;knew&lt;/em&gt; a public key, which is no proof at all, since they're visible on chain to everyone.&lt;/p&gt;

&lt;p&gt;What you actually need to know is that the holder of the matching &lt;em&gt;private&lt;/em&gt; key approved this transaction. That's what a signature proves and a pubkey comparison doesn't. So the rule is: an authority must sign, not just match. Checking the pubkey without checking the signature is the precise mistake behind one of the most famous bridge exploits, where the program compared an account but never confirmed it had signed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Anchor answers both questions for you
&lt;/h2&gt;

&lt;p&gt;If you're using Anchor, the reassuring part is that the account &lt;em&gt;types&lt;/em&gt; answer these questions automatically. You often get the checks for free, as long as you reach for the right type.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; answers the owner question. When you type an account as &lt;code&gt;Account&amp;lt;'info, Config&amp;gt;&lt;/code&gt;, Anchor verifies the account is owned by your program and that its first eight bytes match the &lt;code&gt;Config&lt;/code&gt; type's discriminator, before your handler runs. The forged account from question one never makes it through.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; answers the signer question. Typing an account as &lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; makes Anchor confirm it actually signed the transaction. No pubkey-comparison trap, because you're checking the signature itself.&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="c1"&gt;// owner + discriminator checked automatically&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;span class="c1"&gt;// signature checked automatically&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Signer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'info&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The danger lives in the escape hatch. When you reach for &lt;code&gt;UncheckedAccount&amp;lt;'info&amp;gt;&lt;/code&gt; or &lt;code&gt;AccountInfo&amp;lt;'info&amp;gt;&lt;/code&gt;, Anchor checks &lt;em&gt;nothing&lt;/em&gt;: no owner, no signer, no type. Sometimes you need that, but every time you use it you've quietly promised to do the validation by hand. Anchor even makes you write a &lt;code&gt;/// CHECK:&lt;/code&gt; comment above it to acknowledge the promise. The bugs tend to be the moments someone reached for an unchecked account to make something compile, then forgot they'd taken on that promise.&lt;/p&gt;

&lt;p&gt;So a lot of Anchor security is simply: use the typed account instead of the raw one, and let the framework ask the two questions for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binding accounts together: the constraints
&lt;/h2&gt;

&lt;p&gt;The types answer "is this a real account my program owns" and "did this account sign." One question they can't answer on their own is whether two accounts &lt;em&gt;belong together&lt;/em&gt;. Does this &lt;code&gt;config&lt;/code&gt; belong to this &lt;code&gt;authority&lt;/code&gt;? For that, Anchor gives you constraints that live right on 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;#[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="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="nd"&gt;#[account(&lt;/span&gt;
        &lt;span class="nd"&gt;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="nd"&gt;)]&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;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; checks that the &lt;code&gt;authority&lt;/code&gt; field stored inside &lt;code&gt;config&lt;/code&gt; equals the &lt;code&gt;authority&lt;/code&gt; account that signed. It binds the on-chain record to the live signer, so a valid signer can't operate on a config that isn't theirs. There are more of these (&lt;code&gt;seeds&lt;/code&gt;/&lt;code&gt;bump&lt;/code&gt; to confirm a PDA, &lt;code&gt;address&lt;/code&gt; to pin an exact key, &lt;code&gt;constraint&lt;/code&gt; for any boolean you like), but they all share one idea: declare the rule next to the account, and the runtime enforces it before your handler runs. A rule on the struct can't be forgotten in a refactor the way a check buried in handler logic can.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model to carry in
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing in one frame. A Web2 API often validates once at the edge and trusts the request afterward. A Solana program has no edge. Every account on every instruction is untrusted input, every time. Security is the practice of declaring your assumptions in a place the runtime checks them, so the program refuses bad input by construction rather than by you remembering to write an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you read an instruction with that lens, the checklist is short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For every account whose data you trust, ask: is its owner verified? (&lt;code&gt;Account&amp;lt;'info, T&amp;gt;&lt;/code&gt; does this.)&lt;/li&gt;
&lt;li&gt;For every account that authorizes an action, ask: did it actually sign? (&lt;code&gt;Signer&amp;lt;'info&amp;gt;&lt;/code&gt; does this.)&lt;/li&gt;
&lt;li&gt;For accounts that must belong together, ask: are they bound? (&lt;code&gt;has_one&lt;/code&gt;, &lt;code&gt;seeds&lt;/code&gt;, &lt;code&gt;constraint&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;For every &lt;code&gt;UncheckedAccount&lt;/code&gt;, ask: did I really mean to skip all of that, and did I validate it by hand?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's most of account security, and none of it is clever. It's a posture: assume nothing the caller hands you is what it claims, and make the program prove each piece before it acts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.anchor-lang.com/docs/references/account-types" rel="noopener noreferrer"&gt;Anchor account types reference&lt;/a&gt; documents exactly what each type checks, and the &lt;a href="https://www.anchor-lang.com/docs/references/account-constraints" rel="noopener noreferrer"&gt;account constraints reference&lt;/a&gt; lists &lt;code&gt;has_one&lt;/code&gt;, &lt;code&gt;seeds&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;constraint&lt;/code&gt;, and the rest. For the attack patterns these defend against, &lt;a href="https://github.com/coral-xyz/sealevel-attacks" rel="noopener noreferrer"&gt;coral-xyz/sealevel-attacks&lt;/a&gt; pairs a deliberately vulnerable program with its Anchor fix for each common exploit class, owner checks, signer checks, account substitution, and more. It's the clearest way to see each mistake and its correction side by side.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc turns this posture into practice: you'll audit a program for these exact gaps, close them with constraints, and write tests that prove the locks hold. Reading this first means the two questions will already feel like second nature. Not joined yet? It's not too late to build alongside everyone: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>learning</category>
    </item>
    <item>
      <title>Cross-Program Invocations: How One Solana Program Calls Another</title>
      <dc:creator>Vincent Jande</dc:creator>
      <pubDate>Fri, 26 Jun 2026 16:00:33 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/cross-program-invocations-how-one-solana-program-calls-another-1o70</link>
      <guid>https://dev.to/100daysofsolana/cross-program-invocations-how-one-solana-program-calls-another-1o70</guid>
      <description>&lt;p&gt;Last time we ended on a promise. A Program Derived Address has no private key, so no human can sign for it, yet a program still needs to move tokens out of a vault it owns or release funds from an escrow. The mechanism that makes that work is the Cross-Program Invocation, and it's what this post is about.&lt;/p&gt;

&lt;p&gt;CPIs are also the feature that makes Solana composable. A program on its own is a closed box. A program that can call other programs can build on everything already deployed: the System Program to create accounts and move SOL, the Token Program to mint and transfer tokens, any other program someone has shipped. If you've ever wondered how a single Anchor instruction manages to transfer tokens when your program clearly doesn't contain transfer logic, the answer is a CPI into the Token Program.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: your program can't do everything itself
&lt;/h2&gt;

&lt;p&gt;Say your program runs a vault that holds SOL for a user, and later releases it under some condition. To move SOL between system-owned accounts, the transfer has to be performed by the System Program, since that's the program that owns those accounts and your program can only debit lamports from accounts it owns directly. So when the source is a system-owned account, your program needs a way to &lt;em&gt;call&lt;/em&gt; the System Program mid-instruction.&lt;/p&gt;

&lt;p&gt;That's one wall. There's a second one as soon as the account being moved from is a PDA your program controls: authorizing the action requires a signature for an address that has no private key. CPIs solve the first wall, letting your program invoke another program's instruction. PDA signing, layered on top, solves the second. The same pattern shows up constantly with tokens, where your program's PDA is the authority over a token account and has to authorize a transfer through the Token Program, which is the example we'll build below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: one instruction calls another program's instruction
&lt;/h2&gt;

&lt;p&gt;A Cross-Program Invocation is one program calling an instruction on another program during its own execution. The calling program (the caller) builds an instruction aimed at the callee program, hands it the accounts that instruction needs, and invokes it. The callee runs to completion, then control returns to the caller, which continues where it left off.&lt;/p&gt;

&lt;p&gt;The key thing to hold onto is that this is the same kind of instruction a normal client would send from outside. You are assembling a target program ID, an account list, and instruction data, exactly what a transaction contains, except your program is the one issuing it instead of a wallet. The runtime treats it as a nested call within the same transaction.&lt;/p&gt;

&lt;p&gt;There's a depth limit worth knowing: CPIs can only nest so far. Program A can call B, B can call C, and so on, but the runtime caps how deep the chain goes to keep execution bounded. The current limit is 5 levels (raised to 9 under a newer runtime change, SIMD-0268), and you'll rarely come close in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  invoke and invoke_signed: the two functions
&lt;/h2&gt;

&lt;p&gt;At the native level, Solana gives you two functions for making a CPI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;invoke&lt;/code&gt; is for calls where the signatures you need are already present on the transaction. The privileges of the accounts passed into your program extend to the program you call, so if a user signed the outer transaction, that signature carries through to the CPI. This is what you use to act on a normal user-owned account, like asking the System Program to transfer SOL the user already authorized.&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;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;instruction&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;Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;account_infos&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;AccountInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgramResult&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;invoke_signed&lt;/code&gt; is for when your program needs a PDA to sign. Since the PDA has no private key, you instead pass the seeds (plus the bump) used to derive it. The runtime re-derives the address from those seeds and your program's ID, and if it matches an account in the call, it treats that account as having signed.&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;invoke_signed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;instruction&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;Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;account_infos&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;AccountInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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="n"&gt;signers_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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgramResult&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detail that demystifies the whole thing: &lt;code&gt;invoke&lt;/code&gt; is just &lt;code&gt;invoke_signed&lt;/code&gt; with an empty seeds array. They run through the same path; the only difference is whether you supply signer seeds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instruction&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;Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_infos&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;AccountInfo&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgramResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;invoke_signed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_infos&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part that matters most: PDA signing is not cryptographic
&lt;/h2&gt;

&lt;p&gt;This is the conceptual center of the post, so it's worth slowing down.&lt;/p&gt;

&lt;p&gt;When a normal account signs a transaction, a private key produces a cryptographic signature. A PDA has no private key, so nothing like that can happen. Instead, PDA signing is a runtime construct. You hand &lt;code&gt;invoke_signed&lt;/code&gt; the seeds, the runtime re-derives the PDA from those seeds and the calling program's ID, and if the derived address matches an account in the call, the runtime marks that account as signed for the duration of the CPI.&lt;/p&gt;

&lt;p&gt;No signature is ever computed. The "signature" is the runtime agreeing that because your program supplied the correct seeds, and those seeds plus your program ID derive to this exact address, your program is allowed to authorize actions on it. This is the cash-out of everything from the PDA post: the address is derived from your program, so only your program can present the seeds that unlock it. The absence of a private key isn't a gap that PDA signing patches over. It's the whole reason the scheme is secure, because no external party can ever produce these credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it looks in Anchor
&lt;/h2&gt;

&lt;p&gt;Anchor wraps the raw functions in a &lt;code&gt;CpiContext&lt;/code&gt;, which bundles the program you're calling with the accounts that instruction needs. You build the context, then call the typed helper for the instruction.&lt;/p&gt;

&lt;p&gt;A plain CPI, transferring SOL via the System Program using the user's own signature. Here &lt;code&gt;Transfer&lt;/code&gt; is the System Program's transfer, which takes just &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;anchor_lang&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;system_program&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;transfer&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cpi_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.system_program&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&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.sender&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.recipient&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A signed CPI, where a PDA is the authority. This one transfers SPL tokens through the Token Program, so the &lt;code&gt;Transfer&lt;/code&gt; here is the token program's version, which also takes an &lt;code&gt;authority&lt;/code&gt;, the account that must sign. That authority is our PDA, and &lt;code&gt;new_with_signer&lt;/code&gt; plus the seeds is how the PDA signs:&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;use&lt;/span&gt; &lt;span class="nn"&gt;anchor_spl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;token&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;Transfer&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;.accounts.vault.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;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;authority_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_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CpiContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_with_signer&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.token_program&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;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_token_account&lt;/span&gt;&lt;span class="nf"&gt;.to_account_info&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.recipient_token_account&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;authority&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="n"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpi_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the difference in plain terms. &lt;code&gt;CpiContext::new&lt;/code&gt; says "make this call using the signatures already on the transaction." &lt;code&gt;CpiContext::new_with_signer&lt;/code&gt; says "make this call, and also present these seeds so the runtime will let my PDA sign." The seeds you pass are the same ones you'd use to derive the PDA, with the bump as the final element.&lt;/p&gt;

&lt;p&gt;If you're calling another Anchor program rather than a built-in one, you add it as a dependency with the &lt;code&gt;cpi&lt;/code&gt; feature, which generates typed instruction builders for it. That's a setup detail the lessons will walk through; the mental model is the same either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things that trip people up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting an account the callee needs.&lt;/strong&gt; A CPI passes accounts to the callee, and the callee validates them just like any instruction. If the inner instruction needs the System Program or a token program account, it has to be in your account list and in your &lt;code&gt;#[derive(Accounts)]&lt;/code&gt; struct. A missing account shows up as a failure inside the CPI, not in your own logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong seeds in &lt;code&gt;invoke_signed&lt;/code&gt;.&lt;/strong&gt; If the seeds you pass don't re-derive to the PDA the callee expects as a signer, the runtime won't mark it signed and the call fails. The seeds, their order, and the bump all have to match the derivation exactly, the same discipline as the PDA constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching for &lt;code&gt;invoke_signed&lt;/code&gt; when &lt;code&gt;invoke&lt;/code&gt; would do.&lt;/strong&gt; If the authority is a normal user who already signed the transaction, you don't need signer seeds. Signing with a PDA is only for accounts your program controls that have no key of their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing "who signs" with "who owns."&lt;/strong&gt; A program can sign for a PDA derived from its own ID. It can't sign for arbitrary accounts just because they're passed in. The seeds prove the PDA belongs to the calling program, which is what authorizes the signature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CPI is one program calling another program's instruction mid-execution, which is what makes Solana composable.&lt;/li&gt;
&lt;li&gt;You build a target instruction, accounts, and data, much like a client would, and your program issues it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoke&lt;/code&gt; uses signatures already on the transaction; &lt;code&gt;invoke_signed&lt;/code&gt; lets a PDA sign by supplying its seeds.&lt;/li&gt;
&lt;li&gt;PDA signing isn't cryptographic: the runtime re-derives the PDA from the seeds and your program ID and marks it signed if it matches.&lt;/li&gt;
&lt;li&gt;In Anchor, &lt;code&gt;CpiContext::new&lt;/code&gt; is a plain CPI and &lt;code&gt;CpiContext::new_with_signer&lt;/code&gt; adds the PDA seeds.&lt;/li&gt;
&lt;li&gt;CPIs nest only so deep: the stack limit is 5 levels (9 under SIMD-0268).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://solana.com/docs/core/cpi" rel="noopener noreferrer"&gt;Solana CPI documentation&lt;/a&gt; covers both functions, privilege extension, and the execution flow in detail, and the &lt;a href="https://solana.com/docs/intro/quick-start/cross-program-invocation" rel="noopener noreferrer"&gt;quick-start CPI tutorial&lt;/a&gt; builds a vault that signs with a PDA end to end. For the native signature, the &lt;a href="https://docs.rs/solana-cpi/latest/solana_cpi/fn.invoke_signed.html" rel="noopener noreferrer"&gt;&lt;code&gt;invoke_signed&lt;/code&gt; reference on docs.rs&lt;/a&gt; spells out exactly how the runtime treats PDA signing.&lt;/p&gt;

&lt;p&gt;If you're doing 100 Days of Solana, the next arc puts this to work, and since you already understand PDAs, the signing half should click rather than mystify. Not joined yet? It's not too late to build alongside everyone: &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;mlh.link/solana-100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>100daysofsolana</category>
      <category>web3</category>
      <category>learning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Epoch 2 content creator challenge</title>
      <dc:creator>Matthew Revell</dc:creator>
      <pubDate>Fri, 26 Jun 2026 11:22:47 +0000</pubDate>
      <link>https://dev.to/100daysofsolana/epoch-2-content-creator-challenge-5e88</link>
      <guid>https://dev.to/100daysofsolana/epoch-2-content-creator-challenge-5e88</guid>
      <description>&lt;p&gt;In Epoch 2 of &lt;a href="https://mlh.link/solana-100" rel="noopener noreferrer"&gt;100 Days of Solana&lt;/a&gt;, we moved from getting to grips with the fundamentals to creating our own on-chain assets.&lt;/p&gt;

&lt;p&gt;That gave us a lot to work with: tokens, mints, metadata, authorities, extensions, and the choices developers make when they create an asset on Solana.&lt;/p&gt;

&lt;p&gt;And this challenge is all about seeing those ideas explained clearly for developers coming from Web2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prize categories
&lt;/h2&gt;

&lt;p&gt;For the Epoch 2 Content Challenge, we’re awarding prizes in two categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best DEV post&lt;/li&gt;
&lt;li&gt;Best YouTube video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To enter, you’ll need to have completed at least five Epoch 2 challenges. Missed a few? No problem. You can catch up now and still take part.&lt;/p&gt;

&lt;p&gt;Your post or video should use something from Epoch 2 to help Web2 developers understand Solana. That might mean explaining tokens through a Web2 comparison, showing how NFT metadata works, walking through a Token-2022 extension, or using something you built during the challenges to make an unfamiliar idea easier to grasp.&lt;/p&gt;

&lt;p&gt;This shouldn’t just be a diary of what you completed. The goal is to create something useful for developers who are Solana-curious but mostly coming from a Web2 background.&lt;/p&gt;

&lt;p&gt;There are a couple of other rules, too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your entry will also need to reach at least 50 views.&lt;/li&gt;
&lt;li&gt;Entries must be submitted by the end of Sunday, July 5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your post or video is live, submit it &lt;a href="https://forms.gle/TtHBb5KhrcUeQKc3A" rel="noopener noreferrer"&gt;using this form&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We'll announce the winners on DEV the week after the challenge closes!&lt;/p&gt;

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