<?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>Forem: Meriç Cintosun</title>
    <description>The latest articles on Forem by Meriç Cintosun (@mericcintosun).</description>
    <link>https://forem.com/mericcintosun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3854042%2F4711d921-320e-44bc-a2a8-8aec225d1770.jpeg</url>
      <title>Forem: Meriç Cintosun</title>
      <link>https://forem.com/mericcintosun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mericcintosun"/>
    <language>en</language>
    <item>
      <title>Agentic AI: A New Era in Web Applications</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Tue, 12 May 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/agentic-ai-a-new-era-in-web-applications-5gam</link>
      <guid>https://forem.com/mericcintosun/agentic-ai-a-new-era-in-web-applications-5gam</guid>
      <description>&lt;p&gt;Autonomous AI systems are reshaping how we think about application logic. Where traditional chatbots execute predefined rules and hand off problems to users, agentic AI systems decompose complex goals into sequences of actions, make decisions independently, and iterate toward outcomes without human intervention. The distinction matters for builders. A chatbot might tell you how to fix a bug; an agentic system can inspect your codebase, write test cases, run them, refactor based on failures, and commit the result. This shift from instruction-following to goal-directed behavior creates new architectural challenges and opportunities, especially when agentic capabilities must integrate seamlessly into web applications where users expect transparent, controllable interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Agentic AI vs. Traditional LLM Workflows
&lt;/h2&gt;

&lt;p&gt;The taxonomy of AI behavior in production systems centers on how much autonomy the system holds. Traditional LLM applications function as stateless text transformers: a user provides input, the model generates output, and the application presents that output. If the output is incorrect, the burden falls on the user to rerun the query with different framing. Agentic systems invert this dynamic by embedding decision-making into the application layer itself.&lt;/p&gt;

&lt;p&gt;An agentic system receives a high-level goal (for example, "reduce database query latency by 20%") and then recursively executes a loop: it perceives the current state, selects actions from a toolkit, executes them, observes results, and updates its world model. This loop continues until the system either achieves the goal or exhausts its action budget. The system might call database profiling tools, analyze query plans, test index strategies, measure performance, and propose schema changes without requiring confirmation at each step.&lt;/p&gt;

&lt;p&gt;The distinction surfaces in practical ways. Traditional LLM pipelines are easier to debug because every interaction is a transaction: input in, output out. Agentic systems are harder to reason about because they maintain implicit state across multiple tool calls, and failures may occur deep in a chain of reasoning where the original goal and current action have become semantically distant. However, agentic systems can tackle problems that require exploration, where the solution path is not known in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;Agentic AI systems in production rely on a small set of architectural patterns that determine how goals flow through the system and how actions get executed. Understanding these patterns is prerequisite to building reliable systems.&lt;/p&gt;

&lt;p&gt;The most basic pattern is the &lt;strong&gt;ReAct loop&lt;/strong&gt;: Reasoning, Acting, and observing. The system explicitly generates a thought (reasoning step), selects and invokes a tool or action (acting), and then incorporates the tool's output into its reasoning for the next step. This pattern is simple and transparent. A developer can inspect the chain of reasoning and tool calls to understand why the system made a decision. ReAct is deterministic given the same initial state and action space, which aids debugging.&lt;/p&gt;

&lt;p&gt;A more sophisticated pattern is &lt;strong&gt;hierarchical planning&lt;/strong&gt;, where the agent decomposes a goal into subgoals, solves each subgoal by spinning up a sub-agent or executing a routine, and then synthesizes results. This pattern handles problems with compositional structure. For instance, an agent tasked with "optimize API performance" might spawn parallel sub-agents for database optimization, caching strategy, and endpoint profiling, then synthesize the results into a cohesive recommendation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval-augmented action&lt;/strong&gt; is the pattern where the agent's decision-making depends on looking up external context. Rather than relying solely on its training knowledge, the agent queries a database, vector store, or API to retrieve relevant facts, then incorporates that context into its reasoning. This pattern is essential for web applications where the agent must make decisions based on current data (user profile, inventory levels, market prices) rather than stale information.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;agentic loop with human-in-the-loop breaks&lt;/strong&gt; is a critical production pattern. The system executes a chain of reasoning and actions, but before committing to high-impact decisions (payment processing, data deletion, security changes), it pauses and requests human confirmation. This pattern balances autonomy with accountability. The human can review the proposed action, modify it, or reject it entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an Agent from First Principles
&lt;/h2&gt;

&lt;p&gt;Constructing an agentic system starts with defining the goal space, the action space, and the feedback mechanism. These three components determine what the agent can accomplish and how it will behave under uncertainty.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;goal space&lt;/strong&gt; is the set of problems the agent is designed to solve. A well-defined goal should be specific and measurable. Instead of "improve the codebase," a better goal is "reduce the average response time of the &lt;code&gt;/api/users&lt;/code&gt; endpoint from 500ms to under 100ms." The specificity allows the agent to recognize when the goal is achieved and to adjust its strategy when it is not.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;action space&lt;/strong&gt; is the set of tools, APIs, and functions the agent can invoke. In a web application context, this might include database queries, API calls to external services, code analysis tools, testing frameworks, or even the ability to write and execute scripts. The richer the action space, the more flexible the agent becomes, but also the more likely it is to attempt invalid or harmful actions. Careful API design is essential. Each tool should have clear preconditions, well-documented side effects, and built-in guards against dangerous operations.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;feedback mechanism&lt;/strong&gt; is how the agent observes whether its actions moved it closer to the goal. This might be a metric (query latency), a test result (unit tests pass), or a classification (error resolved). Without clear feedback, the agent cannot learn from its actions and will repeat ineffective strategies. Feedback should be immediate and actionable. A message saying "that didn't work" is less useful than "your query now completes in 120ms, down from 500ms, but the target is under 100ms."&lt;/p&gt;

&lt;p&gt;Once these three components are defined, the agent implementation becomes tractable. The agent maintains a state representation of the current problem (what has been tried, what the system looks like now, what metrics show). It reasons about possible next actions given that state, executes an action, observes the result, updates its state representation, and loops. The termination condition is either goal achievement or exhaustion of the action budget (iterations, time, or token count).&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Agent Management Interfaces in Next.js
&lt;/h2&gt;

&lt;p&gt;Modern web applications must surface agentic behavior in ways that users can understand and control. This is where Next.js application architecture becomes essential. The interface must show what the agent is doing at each step, allow users to intervene or halt execution, display results clearly, and maintain a history of agent runs for auditing and debugging.&lt;/p&gt;

&lt;p&gt;A robust agent management interface separates the real-time action stream from the result presentation layer. The backend maintains an event log of every reasoning step, tool call, and observation. The frontend subscribes to this stream via WebSocket or Server-Sent Events and renders the log in real time. This design allows the user to watch the agent work without blocking the interaction on latency.&lt;/p&gt;

&lt;p&gt;Building this in Next.js requires careful separation of concerns. The API route handling agent execution should be separate from the route serving the frontend. The agent execution logic runs asynchronously on the backend, persisting its state and event log to a database. A separate WebSocket endpoint or SSE endpoint streams events to connected clients. The frontend React component subscribes to this stream, renders events as they arrive, and allows the user to send signals back to the agent (pause, stop, modify goal).&lt;/p&gt;

&lt;p&gt;Here is a minimal example of an API route that initiates an agent run and stores execution metadata:&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="c1"&gt;// app/api/agent/run/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolset&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Create run record in database&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentRuns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;toolset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Start agent execution asynchronously&lt;/span&gt;
  &lt;span class="nf"&gt;executeAgentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Agent run &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeAgentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;toolset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentRuns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Persist event to database&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'reasoning', 'action', 'observation', 'error'&lt;/span&gt;
        &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Broadcast event to connected clients&lt;/span&gt;
      &lt;span class="nf"&gt;broadcastEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentRuns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;completedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentRuns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;completedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend component subscribes to the event stream and renders the agent's progress:&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="c1"&gt;// app/components/AgentViewer.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AgentEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AgentViewerProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AgentViewer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AgentViewerProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEvents&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AgentEvent&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/agent/stream?runId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setEvents&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newStatus&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&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;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleStop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/agent/stop`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;space-y-4 p-6 bg-slate-50 rounded-lg border border-slate-200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex justify-between items-center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-lg font-semibold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="nx"&gt;Execution&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;
          &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`px-3 py-1 rounded text-sm font-medium &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
            &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-blue-100 text-blue-800&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-green-100 text-green-800&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;space-y-3 max-h-96 overflow-y-auto&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EventCard&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="p"&gt;))}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
          &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleStop&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;Stop&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EventCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentEvent&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-slate-100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-blue-50&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-green-50&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-red-50&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-slate-100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`p-3 rounded border border-slate-300 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-xs font-semibold text-slate-600 mb-2&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-sm text-slate-800 font-mono&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern provides real-time visibility into agent execution without blocking on asynchronous work. The user sees what the agent is thinking, doing, and observing, which builds trust. If the agent goes off track, the user can halt it before it performs irreversible actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Complexity: State, Memory, and Tool Reliability
&lt;/h2&gt;

&lt;p&gt;As agents grow more sophisticated, managing their internal state becomes critical. An agent that has executed fifty tool calls across multiple goals needs a way to track what it has tried, what worked, and what the current state of the system is. Without this, the agent will either repeat failed actions or forget critical context.&lt;/p&gt;

&lt;p&gt;State management in agentic systems differs from traditional application state because it must persist across multiple reasoning steps and be queryable by the reasoning loop itself. Many implementations use a combination of a working memory (transient state active during the current reasoning episode) and a persistent history (log of all past actions and observations). The working memory contains the current goal, the list of available tools, recent observations, and a summary of progress so far. The persistent history is queryable (via embedding or keyword search) so the agent can reference past runs when the current approach is not working.&lt;/p&gt;

&lt;p&gt;Tool reliability is another critical concern. In a traditional application, a failed API call returns an error that the application handles. In an agentic system, a failed API call becomes an observation that the agent incorporates into its reasoning. If the tool is flaky, the agent might misinterpret failures as meaningful information and adapt its strategy incorrectly. Production agentic systems require robust tool implementations with retry logic, timeout handling, and circuit breakers. Each tool should be designed to fail gracefully and return informative error messages rather than exceptions.&lt;/p&gt;

&lt;p&gt;Consider the implementation of a tool that executes database queries. The tool must handle connection failures, timeout failures, query syntax errors, and permission errors. Each failure mode should have a distinct error message that guides the agent toward a corrective action. A message like "Connection timeout after 30 seconds; ensure the database is reachable" is more actionable than "Failed."&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="c1"&gt;// lib/tools/database.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryResultRow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;QueryToolOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseQueryTool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryToolOptions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryToolOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;QueryResultRow&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&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="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connection timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
              &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Query timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lastError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Exponential backoff&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Construct informative error message&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Database query failed after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; attempts: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Ensure the query is syntactically correct and you have permission to access the tables.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool composition is another design consideration. Complex goals often require multiple tools in sequence. The agent must understand the dependencies: it cannot analyze query performance without first collecting queries, and it cannot optimize indexes without understanding the workload. Some implementations use a tool registry with metadata about tool dependencies and preconditions. The agent consults this registry when planning its action sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Uncertainty and Failure Modes
&lt;/h2&gt;

&lt;p&gt;Agentic systems operate under uncertainty in ways that traditional applications do not. An LLM's reasoning may be incorrect, a tool may behave unexpectedly, or the goal itself may be impossible. The system must handle these cases gracefully without cascading failures.&lt;/p&gt;

&lt;p&gt;One critical pattern is &lt;strong&gt;goal refinement&lt;/strong&gt;. If the agent fails to achieve its goal after several iterations, it should backtrack and redefine the goal rather than continuing to bang its head against the same wall. For instance, if the goal is "reduce query latency by 50%," but the agent quickly discovers that the bottleneck is not in the database layer but in the application, it should signal this finding and propose a revised goal: "reduce application-level processing latency for user list queries."&lt;/p&gt;

&lt;p&gt;Another pattern is &lt;strong&gt;rollback and recovery&lt;/strong&gt;. If an agent's action causes harm (corrupts data, breaks production), the system should have a way to reverse it. This might involve database transactions (the agent's database operations are wrapped in a transaction that rolls back on failure), version control (code changes are committed to a temporary branch rather than main), or infrastructure snapshots (infrastructure changes are applied to a staging environment first).&lt;/p&gt;

&lt;p&gt;A third pattern is &lt;strong&gt;bounded execution&lt;/strong&gt;. The agent should never run indefinitely. It should have limits on the number of iterations, total wall-clock time, and API calls it can make. When the agent exceeds a limit, it should gracefully terminate and report what it accomplished and what it could not.&lt;/p&gt;

&lt;p&gt;Here is a simplified example of an agent loop with these safeguards:&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="c1"&gt;// lib/agent.ts&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AgentConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tool&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;private&lt;/span&gt; &lt;span class="nx"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LLMReasoner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LLMReasoner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasoning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;iterationCount&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalTokens&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Check termination conditions&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iterationCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Reached iteration limit (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). Goal not achieved.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iterationCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Exceeded token budget. Goal not achieved after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalTokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tokens.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iterationCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Execution timeout exceeded. Goal not achieved.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iterationCount&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="c1"&gt;// Reasoning step&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;totalTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Iteration &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;iterationCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Check if agent declares goal achieved&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Goal achieved.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;iterationCount&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="c1"&gt;// Execute action&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error: Tool "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" not found.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;iterationCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Tool execution failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;iterationCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Goal: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nHistory:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\nObservations:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure gives the agent autonomy while ensuring it cannot exceed resource constraints or run indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability and Debugging Agentic Systems
&lt;/h2&gt;

&lt;p&gt;Traditional applications are debugged by inspecting logs and state snapshots at failure points. Agentic systems require a different debugging strategy because the failure might be deep in a chain of reasoning steps, far removed from where the observable symptom occurs. An agent might make a poor decision in iteration three that doesn't cause a failure until iteration fifteen, by which point the context has changed.&lt;/p&gt;

&lt;p&gt;Comprehensive logging is the foundation. Every reasoning step, tool call, and observation must be logged with timestamps and context. The logging must capture not just what happened, but why the agent made the decision it did. This includes the full prompt sent to the LLM, the LLM's reasoning, and the action it selected.&lt;/p&gt;

&lt;p&gt;Replay and simulation are powerful debugging techniques. If an agent's behavior is incorrect, developers should be able to replay the exact sequence of reasoning and observations that led to the bad behavior. This might involve re-running the LLM with the same prompt and checking if it produces the same reasoning, or simulating the tool environment to ensure the observations are what the agent actually received.&lt;/p&gt;

&lt;p&gt;Tracing is essential for understanding where time and resources are spent. An agent might make decisions that are correct locally but inefficient globally. For instance, it might call an API that succeeds but takes 5 seconds, when a cached query would have returned the answer in 10 milliseconds. Tracing tools should measure the latency of each tool call and flag unexpectedly slow operations.&lt;/p&gt;

&lt;p&gt;Here is a minimal tracing infrastructure:&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="c1"&gt;// lib/tracing.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;perf_hooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tracer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;rootSpan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;currentSpan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rootSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prevSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSpan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prevSpan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getTrace&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rootSpan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;printTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rootSpan&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(running)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;Used in the context of an agent execution:&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="nx"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Tracer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeAgentWithTracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mainSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent-execution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;goal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iterationSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`iteration-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reasoningSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reasoning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;reasoningSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool-execution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;toolSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;iterationSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;mainSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printTrace&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 trace output reveals which operations consume the most time and which tool calls are unexpectedly slow. Developers can then investigate whether the slowness is inherent to the tool or whether it indicates a problem with the agent's strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration with Production Systems
&lt;/h2&gt;

&lt;p&gt;Deploying agentic systems into production requires careful consideration of reliability, cost, and compliance. Agents consume API calls and LLM tokens at a higher rate than traditional applications because they reason and act iteratively. This increases operational costs and introduces new failure modes.&lt;/p&gt;

&lt;p&gt;Cost management is critical. Agents should operate within a defined token budget and iteration limit. If a goal is expensive to achieve (requires many iterations or large prompts), the system should either reject the goal upfront or allocate a smaller agent to investigate the problem first and report back to a human. Some production systems use a two-tiered approach: an inexpensive small model makes the initial attempt, and if it fails, a more capable (and more expensive) model takes over.&lt;/p&gt;

&lt;p&gt;Compliance and auditability are essential for regulated industries. Every agent execution should be logged with enough detail to reconstruct exactly what the agent did and why. This is especially important if the agent makes decisions that affect users. Financial services, healthcare, and legal technology require that agent decisions be explainable and subject to human review.&lt;/p&gt;

&lt;p&gt;Integration with existing observability stacks is important. The agent execution traces should feed into your existing metrics and logging infrastructure. If your organization uses Datadog, New Relic, or similar platforms, ensure that agent spans and events are exported to those systems so that agent behavior is visible alongside application metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead: Agentic Architectures Beyond 2026
&lt;/h2&gt;

&lt;p&gt;Agentic AI is not yet a mature discipline. Production systems today are largely bespoke, built by teams with deep expertise in both LLMs and distributed systems. As the field matures, several trends are likely to emerge. First, frameworks and libraries will abstract away the boilerplate of reasoning loops, tool management, and state persistence. Today's engineers write these from scratch; tomorrow's engineers will configure them. Second, standards for agent communication and composition will emerge, allowing agents to delegate subtasks to other agents and coordinate results. Third, more sophisticated reasoning models will reduce the number of iterations required to solve a given problem, lowering costs and improving reliability.&lt;/p&gt;

&lt;p&gt;For developers building with modern tooling today, the foundation is understanding how agentic loops differ fundamentally from request-response workflows. An agent is not a chatbot that happens to know more. It is a system that maintains state, reasons about goals, selects actions, observes outcomes, and adjusts its strategy. Building these systems reliably requires attention to state management, tool design, observability, and failure handling. The reward is applications that can handle open-ended problems and adapt to circumstances that the original engineers did not anticipate.&lt;/p&gt;

&lt;p&gt;The author is available for professional Web3 documentation or full-stack Next.js development work; you can find details at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Real-World Asset Tokenization: Technical Standards and Compliance for Bonds to Real Estate</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Sun, 10 May 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/real-world-asset-tokenization-technical-standards-and-compliance-for-bonds-to-real-estate-29je</link>
      <guid>https://forem.com/mericcintosun/real-world-asset-tokenization-technical-standards-and-compliance-for-bonds-to-real-estate-29je</guid>
      <description>&lt;h2&gt;
  
  
  The Bridge Between Physical Assets and Blockchain
&lt;/h2&gt;

&lt;p&gt;Real-world asset (RWA) tokenization represents a fundamental shift in how traditional financial instruments move from regulated ledgers into decentralized systems. The process is straightforward in concept: a treasury bond, a commercial property, or a commodity holding becomes a digital token on a blockchain. In execution, however, the gap between a fungible token and a compliant financial instrument creates layers of technical and regulatory complexity that developers must navigate.&lt;/p&gt;

&lt;p&gt;The challenge arises because blockchain networks operate on immutability, transparency, and code-as-law principles. Financial regulation, by contrast, requires auditability, jurisdictional control, and the ability to reverse or restrict transactions in specific contexts. Tokenizing real-world assets demands that both systems coexist within the same architecture. This article examines the technical standards, compliance frameworks, and architectural patterns that make this possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Traditional Token Standards Fall Short
&lt;/h2&gt;

&lt;p&gt;Ethereum's &lt;strong&gt;ERC-20&lt;/strong&gt; standard powers most fungible tokens. It specifies seven core functions: &lt;code&gt;transfer&lt;/code&gt;, &lt;code&gt;transferFrom&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;, &lt;code&gt;allowance&lt;/code&gt;, &lt;code&gt;balanceOf&lt;/code&gt;, &lt;code&gt;totalSupply&lt;/code&gt;, and &lt;code&gt;decimals&lt;/code&gt;. These functions model currency-like behavior with no restrictions beyond cryptographic signatures. A holder of tokens can move them to any address without friction. This design is deliberate for applications where permissionless exchange is the goal.&lt;/p&gt;

&lt;p&gt;Financial assets, however, operate under rules that &lt;strong&gt;ERC-20&lt;/strong&gt; cannot express. A treasury bond cannot be transferred to a sanctioned entity. A real estate token cannot be split into smaller units once minted. A dividend-bearing security requires the ability to pause, freeze, or selectively restrict transactions based on identity, jurisdiction, or regulatory status. These constraints are not edge cases in traditional finance; they are the norm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERC-20&lt;/strong&gt; also provides no way for token contracts to differentiate between holders. Every transfer function accepts any address as a destination. In regulated markets, this blindness to identity is a liability. Compliance officers need to know who holds the tokens, verify that they meet accreditation requirements, and block transactions that violate securities laws or sanctions.&lt;/p&gt;

&lt;p&gt;The limitations of &lt;strong&gt;ERC-20&lt;/strong&gt; pushed the Ethereum and broader blockchain community toward designing tokens that carry compliance logic at the protocol level rather than enforcing it externally through monitoring and intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  ERC-3643: The Standard for Permissioned Assets
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;ERC-3643&lt;/strong&gt; standard emerged from this need. Formally titled "T-REX (Token for Real Estate eXchange)," it was developed by Tokeny, a compliance-focused blockchain infrastructure firm, and adopted through the Ethereum Improvement Proposal process. Unlike &lt;strong&gt;ERC-20&lt;/strong&gt;, which assumes permissionless transfer, &lt;strong&gt;ERC-3643&lt;/strong&gt; assumes that transfers are restricted until proven compliant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Architecture of ERC-3643
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ERC-3643&lt;/strong&gt; introduces three new layers on top of the basic token model: identity management, rule enforcement, and transfer restrictions.&lt;/p&gt;

&lt;p&gt;The identity layer is handled by a protocol called &lt;strong&gt;ONCHAINID&lt;/strong&gt;. Instead of operating directly on wallet addresses, &lt;strong&gt;ERC-3643&lt;/strong&gt; tokens refer to identity documents anchored on-chain. Each investor holds an identity contract at a unique address. That identity contract stores claims about the investor: jurisdiction, accreditation status, investor type (individual, fund, institution), and beneficial ownership information. These claims are cryptographically signed by trusted claim issuers (regulated identity providers, KYC platforms, or compliance vendors).&lt;/p&gt;

&lt;p&gt;Transfer restrictions are encoded through a compliance rules engine. Before any token transfer executes, the contract evaluates a series of rules: Does the sender hold an identity document? Has the receiver been verified? Are they in a jurisdiction where the asset can be held? Does the transaction exceed the holder's purchase limit? Is the receiver an accredited investor? Only when all applicable rules return true does the transfer proceed.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;ERC-3643&lt;/strong&gt; transfer function signature reflects this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function transfer(address _to, uint256 _value) external returns (bool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks identical to &lt;strong&gt;ERC-20&lt;/strong&gt;. The difference lies in what happens inside the contract. A compliant implementation performs these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the sender's identity document and request the compliance rules engine to validate whether this transfer is allowed.&lt;/li&gt;
&lt;li&gt;Fetch the receiver's identity document and repeat the validation.&lt;/li&gt;
&lt;li&gt;If any rule fails, revert the transaction and emit an event describing the restriction.&lt;/li&gt;
&lt;li&gt;If all rules pass, update balances and emit a standard transfer event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The issuer of the token can update rules at any time, allowing the contract to adapt to changing regulations without redeploying the token itself. Rules are stored in a separate contract, often referred to as the Compliance Contract or Transfer Manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identity and Claims in ERC-3643
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;ONCHAINID&lt;/strong&gt; protocol is central to making this work. An investor creates an identity document, which is itself a smart contract. That contract does not store private information; instead, it stores a registry of claims. A claim is a signed statement from a claim issuer asserting something about the identity.&lt;/p&gt;

&lt;p&gt;A typical claim for an &lt;strong&gt;ERC-3643&lt;/strong&gt; token might be issued by a regulated KYC provider and include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claim key: &lt;code&gt;investorType&lt;/code&gt; (e.g., "accredited")&lt;/li&gt;
&lt;li&gt;Claim value: &lt;code&gt;1&lt;/code&gt; (true)&lt;/li&gt;
&lt;li&gt;Issuer: the address of the KYC provider&lt;/li&gt;
&lt;li&gt;Expiration: a Unix timestamp indicating when the claim expires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;ERC-3643&lt;/strong&gt; transfer manager queries the receiver's identity contract for claims matching specific keys. If the required claims exist, are signed by trusted issuers, and have not expired, the transfer is approved.&lt;/p&gt;

&lt;p&gt;The cryptographic model here is important. The claim issuer signs the claim, and the signature is stored on-chain. The identity contract exposes a function to verify that the signature is valid and from a trusted issuer. This allows the blockchain to validate claims without storing private information on-chain, and without requiring the claim issuer to execute every transfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document and Event Logging in ERC-3643
&lt;/h3&gt;

&lt;p&gt;Compliance officers and regulators require audit trails. &lt;strong&gt;ERC-3643&lt;/strong&gt; includes an optional document registry that allows issuers to attach on-chain documents to the token contract. These documents are hashed and referenced, not stored fully on-chain (due to gas costs and data availability).&lt;/p&gt;

&lt;p&gt;For example, the token issuer might register a document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function addDocument(bytes32 _docHash, string memory _uri, bytes32 _docType) 
    external onlyIssuer returns (bool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The document hash is the keccak256 hash of the document. The URI points to where the full document can be retrieved (typically IPFS or a centralized server). The document type classifies it: prospectus, KYC terms, compliance rules, or other metadata relevant to investors.&lt;/p&gt;

&lt;p&gt;When a compliance officer needs to audit the token's history, they can retrieve transfer events from the blockchain and cross-reference them with documents stored via this registry. Every significant event (transfer, minting, rule change, identity claim updates) emits an event log that includes references to the applicable compliance documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Emerging Standards and Their Roles
&lt;/h2&gt;

&lt;p&gt;While &lt;strong&gt;ERC-3643&lt;/strong&gt; is the most mature standard for securities-like tokens, other standards address specific asset classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERC-1400&lt;/strong&gt; predates &lt;strong&gt;ERC-3643&lt;/strong&gt; and was proposed by Polymath for securities tokens. It introduces similar concepts of partition (restricting tokens to certain states, such as "locked" or "restricted") and operator-based control. It is less widely adopted than &lt;strong&gt;ERC-3643&lt;/strong&gt;, partly because its design assumes centralized operators manage compliance rather than embedding rules in smart contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERC-1777&lt;/strong&gt; addresses real estate tokens specifically. It introduces the concept of fractional ownership with fractional transfers, allowing a building to be tokenized into units that can be sold as fractions of whole shares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERC-1404&lt;/strong&gt; provides a simple restriction mechanism with three levels: allowed, restricted, and disallowed. It requires less infrastructure than &lt;strong&gt;ERC-3643&lt;/strong&gt; and is suitable for simpler use cases where compliance rules are static.&lt;/p&gt;

&lt;p&gt;For stablecoins backed by real-world collateral (typically fiat reserves), &lt;strong&gt;ERC-2612&lt;/strong&gt; extends &lt;strong&gt;ERC-20&lt;/strong&gt; with permit functionality, allowing token approvals to be signed off-chain and executed on-chain atomically. This reduces transaction friction.&lt;/p&gt;

&lt;p&gt;Each standard makes different trade-offs between simplicity and regulatory completeness. &lt;strong&gt;ERC-3643&lt;/strong&gt; remains the most comprehensive for complex assets because it assumes that compliance is not optional and must be checked before every state change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance Layers and Their Implementation
&lt;/h2&gt;

&lt;p&gt;Real-world asset tokenization requires compliance layers that operate at multiple levels: the smart contract, the oracle network, and the legal framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1: Smart Contract Compliance
&lt;/h3&gt;

&lt;p&gt;The transfer manager contract in &lt;strong&gt;ERC-3643&lt;/strong&gt; implementations enforces rules written in code. These rules are functions that take the sender and receiver as input and return true or false:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function canTransfer(
    address _from, 
    address _to, 
    uint256 _amount
) external view returns (bool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules can check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity requirements&lt;/strong&gt;: Does the receiver have a valid identity document?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accreditation&lt;/strong&gt;: Is the receiver accredited in the relevant jurisdiction?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holder limits&lt;/strong&gt;: Does the receiver's portfolio contain tokens of this type beyond regulatory limits?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jurisdiction restrictions&lt;/strong&gt;: Is the receiver domiciled in a jurisdiction where the asset can be held?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanctions screening&lt;/strong&gt;: Is either party on a sanctions list?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beneficial ownership&lt;/strong&gt;: Does the receiver's ultimate beneficial owner violate concentration limits?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical implementation combines multiple rules using AND or OR logic. For example, a bond token might enforce this rule set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function canTransfer(address _from, address _to, uint256 _amount) 
    external view returns (bool) 
{
    // Rule 1: Receiver must have identity
    require(identityRegistry.contains(_to), "No identity");

    // Rule 2: Receiver must be accredited
    require(
        identityRegistry.contains(_to) &amp;amp;&amp;amp; 
        identityRegistry.identity(_to).hasValidClaim("accredited"), 
        "Not accredited"
    );

    // Rule 3: Sender must not be under transfer lockup
    require(!transferLockup[_from], "Sender locked");

    // Rule 4: Total holdings by receiver cannot exceed cap
    uint256 receiverHoldings = balanceOf(_to) + _amount;
    require(receiverHoldings &amp;lt;= maxHoldingsPerInvestor, "Exceeds cap");

    return true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating these rules does not require redeploying the token. The issuer calls a function to update the transfer manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function setTransferManager(address _newTransferManager) 
    external onlyIssuer
{
    transferManager = _newTransferManager;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design allows regulators to impose new restrictions (or relax existing ones) in response to market conditions or new rules without affecting the token's identity or holder balances.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 2: Oracle-Based Compliance Checks
&lt;/h3&gt;

&lt;p&gt;Some compliance checks require data that exists off-chain. Sanctions screening, regulatory updates, and accreditation status are maintained by centralized services. Smart contracts access this data through oracles.&lt;/p&gt;

&lt;p&gt;A sanctions screening oracle, for example, accepts a wallet address and returns whether that address is on a OFAC (Office of Foreign Assets Control) watchlist. The oracle's response is cryptographically signed and submitted to the blockchain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function verifySanctionsStatus(address _account, bytes calldata _proof) 
    external returns (bool)
{
    (bytes32 _hash, uint8 v, bytes32 r, bytes32 s) = decodeProof(_proof);
    address signer = ecrecover(_hash, v, r, s);
    require(signer == trustedSanctionsOracle, "Invalid signature");

    (address account, bool isSanctioned) = decodeSanctionsResult(_hash);
    require(account == _account, "Mismatch");

    sanctionedAccounts[_account] = isSanctioned;
    return !isSanctioned;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signed proof is provided by a trusted oracle service. The contract verifies the signature and stores the result. Sanctions screens are typically refreshed monthly or quarterly, depending on regulatory requirements.&lt;/p&gt;

&lt;p&gt;Chainlink provides a sanctions oracle service that integrates with &lt;strong&gt;ERC-3643&lt;/strong&gt; tokens. The token issuer subscribes to the oracle and receives periodic updates. If an address becomes sanctioned, the oracle marks it, and subsequent transfer attempts by that address revert.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 3: Legal and Operational Compliance
&lt;/h3&gt;

&lt;p&gt;Smart contracts enforce rules, but those rules derive from legal obligations. A bond issuer must ensure that the rules encoded in the contract reflect the prospectus, relevant securities laws, and regulations governing the asset.&lt;/p&gt;

&lt;p&gt;This creates a critical documentation burden. The issuer must maintain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A prospectus&lt;/strong&gt; that describes the asset, its rights, restrictions, and the rules governing transfers. The prospectus must be accessible to investors and regulators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KYC and AML policies&lt;/strong&gt; that define which identity claims are required, which claim issuers are trusted, and how often claims must be refreshed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transfer restriction policies&lt;/strong&gt; that explain the business logic for each rule in the transfer manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit reports&lt;/strong&gt; from independent third parties verifying that the smart contract implementation matches the prospectus and policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance documentation&lt;/strong&gt; describing how rules can be updated, who has authority to update them, and what review process is required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;strong&gt;ERC-3643&lt;/strong&gt;, these documents are registered on-chain. The issuer calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function addDocument(
    bytes32 _docHash, 
    string memory _uri, 
    bytes32 _docType
) external onlyIssuer returns (bool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The document hash is the keccak256 hash of the document (stored off-chain, typically on IPFS). Investors can verify the hash matches the document they received, confirming the issuer has not altered the terms.&lt;/p&gt;

&lt;p&gt;When a transfer restriction triggers, the revert message should reference a document hash so that the restricted investor can understand the rule and verify that the rule was disclosed at the time of purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Technical Challenges and Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Identity Without Privacy
&lt;/h3&gt;

&lt;p&gt;Real-world assets require knowing who holds them. Blockchain was designed to enable pseudonymous transactions. These goals conflict.&lt;/p&gt;

&lt;p&gt;The solution is to use zero-knowledge proofs or cryptographic commitments. Rather than storing an investor's name and address on-chain, the identity contract stores a cryptographic commitment (a hash) and claims issued by trusted parties. The actual identity information remains off-chain, managed by regulated identity providers.&lt;/p&gt;

&lt;p&gt;When a transfer requires verification, the receiver (or a claim issuer on their behalf) provides a cryptographic proof that they satisfy the required claims without revealing the underlying identity. For example, proving "I am accredited" without revealing name, address, or income.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERC-3643&lt;/strong&gt; doesn't mandate zero-knowledge proofs; most current implementations use semi-trusted claim issuers and on-chain identity contracts. However, as regulatory sophistication grows, zero-knowledge verification is becoming standard for cross-border transfers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Regulatory Jurisdiction and Patchwork Rules
&lt;/h3&gt;

&lt;p&gt;An RWA token may be offered in multiple jurisdictions, each with different rules. A US-accredited investor faces different restrictions than an EU-qualified investor, who faces different restrictions than an investor in a jurisdiction with no specific rules.&lt;/p&gt;

&lt;p&gt;Tokens typically implement jurisdiction-aware rule engines. The transfer manager queries the receiver's identity for a jurisdiction claim and applies the appropriate rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function canTransfer(address _from, address _to, uint256 _amount) 
    external view returns (bool)
{
    bytes32 receiverJurisdiction = 
        identityRegistry.identity(_to).getClaimValue("jurisdiction");

    if (receiverJurisdiction == keccak256("US")) {
        // Apply US rules: accreditation required
        require(
            identityRegistry.identity(_to).hasValidClaim("accredited-us"),
            "Not accredited"
        );
    } else if (receiverJurisdiction == keccak256("EU")) {
        // Apply EU rules: qualified investor status required
        require(
            identityRegistry.identity(_to).hasValidClaim("qualified-investor-eu"),
            "Not qualified"
        );
    } else {
        // Apply default rules
        require(
            identityRegistry.identity(_to).hasValidClaim("kyc-approved"),
            "Not verified"
        );
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows a single token contract to serve multiple jurisdictions while enforcing locally appropriate restrictions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: Oracle Latency and Finality
&lt;/h3&gt;

&lt;p&gt;Compliance checks that depend on off-chain data introduce oracle latency. A sanctions update might be delayed by hours or days. During that window, a token holder might make transfers that later become non-compliant.&lt;/p&gt;

&lt;p&gt;Solutions include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch updates&lt;/strong&gt;: Sanctions lists are updated daily or weekly. The token issuer (or a trusted service) submits updates to the oracle, and transfers are checked against the most recent known list. Investors are informed that sanctions screening is not real-time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crank-based enforcement&lt;/strong&gt;: Rather than blocking transfers proactively, the contract allows transfers to proceed but flags them as pending compliance confirmation. A designated oracle "crank" (a bot or service) periodically checks compliance and executes or reverts pending transfers. This adds latency but provides a fallback if an oracle fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual signing&lt;/strong&gt;: Some implementations require signatures from both the issuer (confirming the rules are correct) and an oracle (confirming compliance checks passed). If either party refuses to sign, the transfer cannot proceed. This ensures both the contract logic and external compliance state are aligned.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Challenge 4: Gas Costs and Scalability
&lt;/h3&gt;

&lt;p&gt;Each transfer in &lt;strong&gt;ERC-3643&lt;/strong&gt; requires identity lookups, rule engine execution, and potentially oracle interactions. These operations are expensive in gas costs, especially on Ethereum mainnet. A single transfer might cost 150,000 to 300,000 gas, compared to 20,000 to 40,000 for a standard &lt;strong&gt;ERC-20&lt;/strong&gt; transfer.&lt;/p&gt;

&lt;p&gt;Solutions include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Layer 2 rollups&lt;/strong&gt;: Deploying &lt;strong&gt;ERC-3643&lt;/strong&gt; tokens on Arbitrum or Optimism reduces gas costs by 50 to 100x while maintaining Ethereum security. Most new RWA tokenization projects target Layer 2.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optimized rule engines&lt;/strong&gt;: Pre-computing common rule checks or using calldata instead of storage reduces gas per transfer. Some tokens batch identity updates or use Merkle trees to avoid redundant lookups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tiered identity verification&lt;/strong&gt;: Simple identity checks (does an identity exist?) are performed on every transfer. Complex checks (is the holder accredited in this specific jurisdiction?) are cached for a period and refreshed periodically rather than on every transfer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-World Implementation Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bond Tokenization
&lt;/h3&gt;

&lt;p&gt;Treasury bonds and corporate debt are among the first RWAs targeted for tokenization. A hypothetical US treasury bond tokenization might work as follows:&lt;/p&gt;

&lt;p&gt;The issuer creates an &lt;strong&gt;ERC-3643&lt;/strong&gt; token with one token per dollar of face value. Rules enforce that buyers are accredited investors or qualified institutional buyers. Transfer restrictions prohibit sales to sanctioned entities and enforce OFAC compliance through oracle integration.&lt;/p&gt;

&lt;p&gt;Claims are issued by a US-based KYC provider. Each investor's identity contract holds a claim proving they are accredited. The transfer manager checks for the accreditation claim on every transfer. If an investor becomes sanctioned, an oracle update marks their address, and further transfers fail.&lt;/p&gt;

&lt;p&gt;The prospectus is hashed and registered on-chain. Investors can verify they received the same prospectus as others and confirm the rules match the stated terms.&lt;/p&gt;

&lt;p&gt;Coupons (interest payments) are handled through a separate mechanism. The issuer maintains a snapshot of holders at a coupon date and sends payments to each holder's address. This requires no transfer, avoiding compliance overhead for distributions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real Estate Tokenization
&lt;/h3&gt;

&lt;p&gt;A commercial property tokenized as an &lt;strong&gt;ERC-3643&lt;/strong&gt; token faces additional complexity. The token represents fractional ownership of a building. Transfers must comply with securities laws (restricting to accredited investors) and may also require approval from the property manager or underwriter.&lt;/p&gt;

&lt;p&gt;Additional rules might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum holding period&lt;/strong&gt;: A transferred token cannot be re-transferred for 6 months (a lock-up period common in real estate funds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accreditation requirement&lt;/strong&gt;: Only accredited investors can hold the token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum concentration&lt;/strong&gt;: No single investor can own more than 10% of the property token (anti-concentration rule).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Distributions (rental income or appreciation) are handled separately. The issuer holds cash in a custody account and periodically distributes it to token holders based on their balance at a snapshot date.&lt;/p&gt;

&lt;p&gt;The challenge here is legal recognition. A token represents an ownership stake, but the underlying property deed is registered with a land authority. The issuer must maintain a bridge: the smart contract controls who can own the token, while the property deed and operating agreement define the legal rights associated with ownership. This bridge is typically maintained through corporate governance documents and legal opinions, not on-chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commodity and Precious Metals
&lt;/h3&gt;

&lt;p&gt;Gold and other commodities can be tokenized as &lt;strong&gt;ERC-20&lt;/strong&gt; or &lt;strong&gt;ERC-3643&lt;/strong&gt; tokens backed by physical holdings. The issuer holds gold in a vault and mints tokens representing ownership. Compliance requirements depend on whether the token is a security (regulated differently from the underlying commodity).&lt;/p&gt;

&lt;p&gt;For physical commodities not classified as securities, simpler standards like &lt;strong&gt;ERC-20&lt;/strong&gt; with a custody multi-signature wallet are common. The token is backed by regular audits proving the issuer holds sufficient physical commodity. Transfers are not restricted by accreditation or jurisdiction claims; instead, the issuer accepts any buyer.&lt;/p&gt;

&lt;p&gt;However, if the commodity token itself becomes a financial instrument (traded on secondary markets, with speculative buyers), regulation may impose restrictions. Some commodity tokens now use &lt;strong&gt;ERC-3643&lt;/strong&gt; to support both use cases: unrestricted transfers in primary issuance, and restricted secondary markets through compliance rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation Requirements for Compliance Tokens
&lt;/h2&gt;

&lt;p&gt;The gap between what a smart contract does and what a regulator requires is filled by documentation. A token developer must produce:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architecture document&lt;/strong&gt;: A technical description of how the token works, which standards it follows, and how compliance rules are enforced. This should be written for engineers and compliance officers unfamiliar with blockchain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compliance mapping&lt;/strong&gt;: A table mapping each smart contract function and rule to the relevant regulation or policy it implements. This demonstrates that the code reflects the legal structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit report&lt;/strong&gt;: An independent audit by a firm experienced in blockchain security and compliance, verifying that the smart contract implementation is secure and matches the architecture document.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KYC and AML policy&lt;/strong&gt;: A document specifying which identity claims are required for token holders, how those claims are verified, and how often they must be refreshed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Risk assessment&lt;/strong&gt;: An analysis of potential failures (oracle failure, rule engine bug, identity provider breach) and how the issuer mitigates each risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Governance documentation&lt;/strong&gt;: A description of how rules can be updated, who has authority, and what approval process is required.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For &lt;strong&gt;ERC-3643&lt;/strong&gt; tokens, these documents should be hashed and registered in the token contract. Investors can verify they hold a token whose rules are publicly documented and unchangeable without governance approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Directions and Emerging Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Interoperability Between Standards
&lt;/h3&gt;

&lt;p&gt;Most institutions building RWA tokens use &lt;strong&gt;ERC-3643&lt;/strong&gt;, but some use older standards or proprietary solutions. Bridges between standards are emerging. A token built on &lt;strong&gt;ERC-1400&lt;/strong&gt; can be wrapped as an &lt;strong&gt;ERC-3643&lt;/strong&gt; token, allowing it to work with identity registries and claim issuers built for the newer standard.&lt;/p&gt;

&lt;p&gt;These bridges are typically implemented through adapter contracts that translate function calls and claim checks between standards. The adapter itself becomes a compliance point, requiring auditing and legal review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regulatory-Grade Custody and Settlement
&lt;/h3&gt;

&lt;p&gt;Most RWA token implementations today handle only the issuance layer. Custody and settlement (the infrastructure for holding and transferring tokens securely) remain centralized. Banks and specialized custody providers manage the actual token accounts on behalf of institutional investors.&lt;/p&gt;

&lt;p&gt;As the market matures, custody infrastructure built natively on blockchain (using multi-signature wallets, time-locked vaults, and programmable access controls) is becoming more common. This reduces counterparty risk and allows tokens to be truly self-custodied by sophisticated investors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Chain Compliance
&lt;/h3&gt;

&lt;p&gt;Tokens are increasingly deployed across multiple blockchains (Ethereum, Polygon, Arbitrum). A single RWA token might have instances on different chains, backed by the same underlying asset. Compliance rules must be synchronized across chains; a transfer on one chain must reflect in identity records used on another chain.&lt;/p&gt;

&lt;p&gt;This requires either centralized oracles that maintain state across chains or interoperability protocols (like bridges) that relay compliance events between chains. Both approaches add latency and introduce new failure points.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privacy-Preserving Compliance
&lt;/h3&gt;

&lt;p&gt;As jurisdictions like the EU impose stronger data protection requirements (GDPR), storing identity information on-chain becomes risky. Privacy-preserving techniques such as zero-knowledge proofs allow tokens to verify compliance without revealing the underlying personal data.&lt;/p&gt;

&lt;p&gt;A receiver can prove "I am accredited and not sanctioned" without revealing their name, address, or income. The verifier (the transfer manager contract) checks the proof cryptographically without accessing private information. This requires more complex smart contracts and interactions but preserves privacy while enforcing compliance.&lt;/p&gt;

&lt;p&gt;Real-world implementations of this are still early. Tokens using privacy-preserving techniques are primarily research projects or early-stage institutional offerings. Adoption in retail markets is likely years away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and Implementation Roadmap
&lt;/h2&gt;

&lt;p&gt;Building a compliant RWA token requires decisions across multiple dimensions. Standards like &lt;strong&gt;ERC-3643&lt;/strong&gt; provide a framework, but implementation details depend on the asset class, target market, and regulatory requirements.&lt;/p&gt;

&lt;p&gt;A typical implementation roadmap looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define the asset and its regulations&lt;/strong&gt;: Understand the securities laws or commodity regulations governing the asset and the jurisdictions where it will be offered.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose a standard&lt;/strong&gt;: &lt;strong&gt;ERC-3643&lt;/strong&gt; is the most mature for securities-like assets. Simpler assets may use &lt;strong&gt;ERC-20&lt;/strong&gt; with centralized compliance oversight.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design the compliance rule set&lt;/strong&gt;: Identify which investor categories are allowed, which restrictions apply, and which oracles are needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Implement the identity layer&lt;/strong&gt;: Either build a custom identity contract or integrate with an existing &lt;strong&gt;ONCHAINID&lt;/strong&gt; provider.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deploy to a test network&lt;/strong&gt;: Build the token on a testnet, test the rule engine against various transfer scenarios, and collect feedback from compliance officers and investors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit and document&lt;/strong&gt;: Hire a blockchain security firm to audit the smart contracts. Document the architecture, rules, and compliance mapping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deploy to production&lt;/strong&gt;: Launch on a mainnet or Layer 2 with limited initial supply, expanding as confidence grows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor and upgrade&lt;/strong&gt;: Respond to regulatory changes by updating rules, refreshing claim issuers, or migrating to new standards as they emerge.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The regulatory landscape for RWAs is still evolving. Tokens built today must be designed with flexibility: the ability to update rules, change claim issuers, and upgrade to new standards without breaking existing holder positions. This flexibility is the difference between a prototype and a system trusted with billions in real-world assets.&lt;/p&gt;

&lt;p&gt;For professional Web3 documentation or full-stack Next.js development work, reach out through my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>cryptocurrency</category>
      <category>web3</category>
    </item>
    <item>
      <title>Zero-Knowledge Proofs: The Intersection of Scalability and Privacy</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Fri, 08 May 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/zero-knowledge-proofs-the-intersection-of-scalability-and-privacy-51ng</link>
      <guid>https://forem.com/mericcintosun/zero-knowledge-proofs-the-intersection-of-scalability-and-privacy-51ng</guid>
      <description>&lt;p&gt;Zero-knowledge proofs represent one of the most powerful primitives in modern cryptography, enabling a prover to convince a verifier that a statement is true without revealing any information beyond the validity of that statement itself. This capability has shifted from theoretical interest to production infrastructure, particularly in blockchain systems where both scalability and privacy constraints demand solutions that scale without exposing sensitive data.&lt;/p&gt;

&lt;p&gt;The intersection of these two properties—the ability to process thousands of transactions while keeping user information private—defines the frontier of practical cryptography in distributed systems. Understanding how zero-knowledge proofs achieve this requires examining both the mathematical foundations and the engineering patterns that make these systems work at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cryptographic Foundations of Zero-Knowledge Proofs
&lt;/h2&gt;

&lt;p&gt;A zero-knowledge proof satisfies three formal properties: completeness, soundness, and zero-knowledge itself. Completeness means that if a statement is true, an honest prover can always convince an honest verifier. Soundness guarantees that if a statement is false, a dishonest prover cannot convince an honest verifier except with negligible probability. Zero-knowledge ensures that the verifier learns nothing beyond the truth of the statement.&lt;/p&gt;

&lt;p&gt;These properties rest on the assumption that certain computational problems remain difficult to solve. The security of many zero-knowledge systems relies on the hardness of discrete logarithm problems or the factorization of large integers. A prover must demonstrate knowledge of a solution to a hard problem without actually revealing the solution itself. This is accomplished through an interactive protocol where the prover commits to a value, the verifier issues a challenge, and the prover responds in a way that is consistent with the commitment.&lt;/p&gt;

&lt;p&gt;The simplest conceptual example involves graph isomorphism. Suppose we have two graphs that the prover claims are isomorphic. The prover cannot simply reveal the mapping between vertices without giving away the solution. Instead, the prover randomly permutes one of the graphs and commits to the result. The verifier then challenges the prover to either prove that the permuted graph is isomorphic to the original, or to the other graph. The prover responds correctly only if the original claim was true. By repeating this protocol multiple times, the verifier becomes confident in the truth of the statement while learning no information about the actual isomorphism.&lt;/p&gt;

&lt;p&gt;Interactive proofs work well for understanding the concept, but they require multiple rounds of communication and the verifier's presence during execution. The Fiat-Shamir transformation eliminates this requirement by replacing the verifier's random challenge with a cryptographic hash function. The prover applies a hash function to its commitment, treating the output as the challenge, and then computes a response based on that hash value. Because the hash output appears random and the prover cannot predict it in advance, the security properties remain intact, but now a single non-interactive proof can be verified at any time without the prover's involvement.&lt;/p&gt;

&lt;p&gt;Non-interactive zero-knowledge proofs unlock the possibility of publishing proofs on blockchain systems, where verifiers are smart contracts that execute deterministically and independently. A prover generates a proof, submits it to the chain, and the contract verifies it without requiring communication with the prover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arithmetic Constraints and Circuit Languages
&lt;/h2&gt;

&lt;p&gt;Practical zero-knowledge systems work by converting a computational statement into arithmetic constraints over a finite field. Rather than proving knowledge of a specific secret directly, systems prove that the prover knows a witness that satisfies a set of polynomial equations.&lt;/p&gt;

&lt;p&gt;Consider a simple example: proving knowledge of two numbers whose product is a specific public value. Let us say the prover knows a and b, and wants to prove that a × b = c, where c is public. The prover commits to a and b through some commitment scheme. The proof then demonstrates that there exist values satisfying the constraint equation without revealing what a and b are.&lt;/p&gt;

&lt;p&gt;In practice, developers express computational problems in a domain-specific language that compiles down to arithmetic circuits. A circuit is a system of polynomial equations where each equation corresponds to a logical or arithmetic gate. For instance, if a program multiplies two numbers, that multiplication becomes a polynomial constraint. If a program hashes a value, the circuit must contain all the arithmetic constraints that express the hash function's computation in terms of field operations.&lt;/p&gt;

&lt;p&gt;Languages like Circom let developers write circuits in a syntax resembling JavaScript. A Circom program takes inputs, applies constraints, and produces outputs. The developer specifies which inputs are public (known to the verifier) and which are private (known only to the prover). The compiler then transforms the program into a constraint system, typically expressed as rank-1 constraint systems (R1CS), a standardized format that many proof systems consume.&lt;/p&gt;

&lt;p&gt;A rank-1 constraint system is a list of equations of the form (a · w) * (b · w) = (c · w), where w is a vector containing all witness values (including both public and private inputs), and a, b, c are coefficient vectors. The dot product notation represents a linear combination of witness values. Every arithmetic operation in the original program translates into one or more R1CS constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof Systems: Understanding the Trade-offs
&lt;/h2&gt;

&lt;p&gt;Different zero-knowledge proof systems make different choices about proof size, verification time, prover time, and required cryptographic assumptions. No single proof system dominates across all dimensions, and the choice depends on the specific application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SNARKs (Succinct Non-interactive Arguments of Knowledge)&lt;/strong&gt; produce very small proofs, typically a few hundred bytes. A SNARK proof can be verified in milliseconds, making these proofs highly efficient for blockchain verification where gas costs scale with computation. The trade-off is that SNARK proofs require a trusted setup: a ceremony where cryptographic parameters are generated and destroyed, and anyone participating in the setup could theoretically forge proofs if they retained the setup randomness.&lt;/p&gt;

&lt;p&gt;SNARKs typically use pairing-based cryptography, which relies on elliptic curves with a bilinear pairing. The pairing operation allows combining two group elements from different curves to produce a result in a target group, enabling certain cryptographic constructions impossible with standard elliptic curve operations alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STARKs (Scalable Transparent Arguments of Knowledge)&lt;/strong&gt; eliminate the trusted setup requirement entirely, relying instead on hash functions that are believed to be collision-resistant. STARKs produce larger proofs than SNARKs (typically kilobytes rather than bytes) and require more verification time, but the removal of trusted setup reduces the surface area for attacks and simplifies deployment. STARKs use the FRI protocol (Fast Reed-Solomon Interactive Oracle Proofs), which decomposes a polynomial commitment into smaller sub-problems that can be solved recursively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulletproofs&lt;/strong&gt; offer a middle ground, providing relatively small proofs without a trusted setup, though with higher verification complexity than SNARKs. Bulletproofs use logarithmic-sized arguments and rely on discrete logarithm assumptions similar to SNARKs, but without requiring pairings.&lt;/p&gt;

&lt;p&gt;The choice between these systems in production depends on specific constraints. If proof size is critical (as it is for on-chain verification), SNARKs with trusted setup may be acceptable if the setup ceremony is sufficiently transparent. If eliminating trust assumptions is paramount, STARKs accept larger proofs. If neither extreme is suitable, systems like Bulletproofs provide a compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  ZK-Rollups: Scaling Through Aggregated Proofs
&lt;/h2&gt;

&lt;p&gt;A ZK-rollup is a layer-2 scaling solution that processes thousands of transactions off-chain, generates a single zero-knowledge proof attesting to their correctness, and submits both a compressed state update and the proof to the main blockchain. The main chain verifies the proof in a single transaction, ensuring that all rolled-up transactions are valid without re-executing them.&lt;/p&gt;

&lt;p&gt;The architecture consists of several key components. A rollup operator maintains a state tree representing all user balances and contract state off-chain. Users submit transactions to the operator's mempool. The operator collects transactions into batches, executes them sequentially, updates the state, and then generates a proof that attests to the validity of the entire batch and the correctness of the state transition.&lt;/p&gt;

&lt;p&gt;The proof demonstrates several properties simultaneously. First, it proves that each transaction in the batch was executed according to the rollup's rules: valid signatures, sufficient balances, and correct state transitions. Second, it proves that the state root before processing the batch combined with the batch of transactions produces the new state root. Third, it proves that all constraints of the rollup protocol were satisfied.&lt;/p&gt;

&lt;p&gt;Generating this proof requires encoding the entire batch execution as an arithmetic circuit. The circuit takes the previous state root, the batch of transactions, and a Merkle proof for each account's current balance as private inputs. It then verifies signatures, checks balance constraints, executes the transactions, updates the state tree, and outputs the new state root. The circuit is large—processing a typical batch of 2000 transactions might require 10-50 million constraints—but the circuit is generated once per batch, not per transaction.&lt;/p&gt;

&lt;p&gt;The gas cost of posting a ZK-rollup proof to Ethereum is dominated by the verification step itself, plus the cost of publishing the compressed batch data and the new state root. Verification typically costs 200,000-500,000 gas depending on the proof system and the specific circuit, which is amortized across all transactions in the batch. A batch containing 2000 transactions thus pays roughly 100-250 gas per transaction just for proof verification, compared to 21,000 gas for a minimal Ethereum transaction.&lt;/p&gt;

&lt;p&gt;The state tree structure itself requires careful design. ZK-rollups typically use Merkle trees or sparse Merkle trees to represent the state, allowing efficient proofs of account inclusion and balance transitions. A sparse Merkle tree maps account addresses to their balances, with empty branches represented implicitly rather than stored. This allows constant-size Merkle proofs regardless of the total number of accounts in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mathematical Models and Constraint Systems
&lt;/h2&gt;

&lt;p&gt;Designing a production ZK-rollup requires precisely modeling the mathematical guarantees that the system provides. The core model expresses the relationship between states and valid transitions.&lt;/p&gt;

&lt;p&gt;A rollup state can be represented as S = (root, nonce), where root is the Merkle root of the account tree and nonce is a counter that increments with each batch. A batch B is a sequence of transactions [tx1, tx2, ..., txn]. A valid state transition S → S' via batch B requires that executing B starting from state S produces state S'.&lt;/p&gt;

&lt;p&gt;More formally, we define a transition relation T(S, B, S') that holds true if and only if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each transaction txi in B, the transaction's sender exists in the state tree at S&lt;/li&gt;
&lt;li&gt;The sender's account in S has sufficient balance to cover the transaction's value plus fees&lt;/li&gt;
&lt;li&gt;The transaction's signature is valid under the sender's public key&lt;/li&gt;
&lt;li&gt;Executing the transaction updates the state tree correctly, producing intermediate state Si&lt;/li&gt;
&lt;li&gt;After all transactions execute in sequence, the final state equals S'&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The zero-knowledge proof demonstrates that this relation T holds without revealing any of the intermediate states, account balances, or transaction contents beyond what is publicly necessary.&lt;/p&gt;

&lt;p&gt;The circuit representation of this relation requires expressing each condition as polynomial constraints. For transaction signature verification, the circuit encodes elliptic curve operations as field arithmetic. For state tree updates, the circuit verifies Merkle proofs and computes new roots. For balance checks, the circuit reads account values from the state tree and verifies arithmetic constraints.&lt;/p&gt;

&lt;p&gt;A simplified version of the constraint logic for a single transaction might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Verify sender exists in the state tree
merkle_proof_valid(sender_merkle_proof, sender_address, old_root) == true

// Read sender's balance
sender_balance = merkle_path_value(sender_merkle_proof, balance_index)

// Verify sufficient balance
sender_balance &amp;gt;= transaction_amount + transaction_fee

// Compute new sender balance
new_sender_balance = sender_balance - (transaction_amount + transaction_fee)

// Verify recipient balance update (if recipient is a new account, new balance = transaction_amount)
recipient_balance_new = (recipient_exists ? recipient_balance_old : 0) + transaction_amount

// Update state tree with new balances
new_root = update_merkle_root(old_root, sender_address, new_sender_balance, recipient_address, recipient_balance_new)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each operation in this sequence becomes one or more polynomial constraints in the arithmetic circuit. Merkle proof verification is the most constraint-intensive operation, as hash functions like Keccak require thousands of constraints to express in polynomial form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving and Verification Performance
&lt;/h2&gt;

&lt;p&gt;The time required to generate a proof scales with the circuit size and grows faster than linearly. For a circuit with 10 million constraints using an SNARK, proof generation might require 10-30 seconds on a modern CPU, or a few seconds using specialized hardware like GPUs. Proof verification, by contrast, is nearly constant-time regardless of circuit size, typically completing in 50-200 milliseconds.&lt;/p&gt;

&lt;p&gt;This asymmetry makes ZK-rollups economically viable. A single operator or sequencer incurs the cost of generating proofs, but that cost is amortized across all users in a batch. Verification happens once on-chain for all transactions, paying a fixed gas cost rather than a per-transaction cost.&lt;/p&gt;

&lt;p&gt;Proof generation can be optimized through batching and parallelization. Multiple batches can be proven in parallel if sufficient hardware is available. Some proof systems support recursive composition, where a proof of proofs allows further aggregation. For instance, several rollup batches can each generate a proof, and then those proofs can be combined into a single proof attesting to all batches. This reduces the on-chain verification cost even further when multiple batches are accumulated.&lt;/p&gt;

&lt;p&gt;The memory requirements for proof generation depend on the proof system. SNARKs typically require holding the entire constraint system in memory, which can be gigabytes for large circuits. STARKs have different memory characteristics but generally scale less favorably than SNARKs in terms of runtime for typical circuit sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy Guarantees and Limitations
&lt;/h2&gt;

&lt;p&gt;A fundamental property of zero-knowledge proofs is that they provide privacy regarding the witness (the prover's secret), but the public inputs and outputs remain visible to the verifier. In a ZK-rollup, this means the batch of transactions and the before-and-after state roots are public on-chain, but the proof itself reveals nothing about individual account balances or transaction internals beyond what is necessarily public.&lt;/p&gt;

&lt;p&gt;However, privacy in a ZK-rollup is limited by the nature of the public data. If accounts are linked to real identities through on-chain interactions, an observer can analyze the transaction graph and make inferences about behavior even without seeing transaction contents directly. The zero-knowledge property prevents the proof itself from leaking information, but does not protect against inference attacks based on public transaction patterns.&lt;/p&gt;

&lt;p&gt;Some ZK-rollup designs include additional privacy features, such as encrypted transaction data. In these systems, transactions are encrypted under a rollup-specific key that only the operator knows. The proof attests to the correctness of decryption and execution without the verifier ever seeing the plaintext. This prevents external observers from analyzing the transaction graph, though the rollup operator remains able to see all transaction contents.&lt;/p&gt;

&lt;p&gt;The trade-off between privacy and verifiability is fundamental. A ZK-rollup that publishes all transaction data on-chain allows anyone to verify the operator's work independently, but sacrifices user privacy. A ZK-rollup that encrypts transactions improves privacy but requires trusting the operator to not censor transactions or steal funds, as full verification becomes impossible for external observers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Considerations
&lt;/h2&gt;

&lt;p&gt;Building a ZK-rollup system requires careful engineering across multiple layers. The circuit design must be correct, as any error in the constraint system could allow invalid transactions to be proven valid. Circuit audits by independent cryptographers are standard practice.&lt;/p&gt;

&lt;p&gt;The proof system selection determines acceptable trade-offs. If minimizing on-chain verification cost is primary, SNARKs with trusted setup are standard. If removing trust assumptions is critical, STARKs are preferred despite larger proof sizes. Some systems use multiple proof systems, with a faster SNARK for initial verification and a slower but trustless STARK for final settlement.&lt;/p&gt;

&lt;p&gt;The state tree implementation must support efficient proof generation and verification. Sparse Merkle trees are common, but alternative structures like Ethereum's Verkle trees are under exploration. The hash function used in the tree must be efficiently expressible as constraints; keccak-256 is common on Ethereum rollups but requires many constraints, while specialized hash functions like Poseidon are designed specifically for arithmetic circuit efficiency.&lt;/p&gt;

&lt;p&gt;The operator architecture must ensure liveness and prevent censorship. A centralized operator can refuse to include transactions, forcing users to wait or use alternative mechanisms. Some designs include a forced transaction exit mechanism, allowing users to withdraw funds without the operator's cooperation if the operator becomes unavailable.&lt;/p&gt;

&lt;p&gt;Data availability remains a separate concern from zero-knowledge proofs themselves. The batch data must be available on-chain or on a separate data availability layer so that users can compute their account state and generate proofs of withdrawal if needed. A ZK-rollup without adequate data availability can hide fraud from observers, defeating the purpose of on-chain verification.&lt;/p&gt;

&lt;p&gt;If you are building a Web3 application or need professional documentation for complex technical systems, my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; covers Web3 documentation, smart contract development, and full-stack Next.js applications.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>computerscience</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>From Monolithic to Modular Blockchain: 2026 Ecosystem Analysis</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Wed, 06 May 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/from-monolithic-to-modular-blockchain-2026-ecosystem-analysis-160g</link>
      <guid>https://forem.com/mericcintosun/from-monolithic-to-modular-blockchain-2026-ecosystem-analysis-160g</guid>
      <description>&lt;h2&gt;
  
  
  The Fundamental Shift in Blockchain Architecture
&lt;/h2&gt;

&lt;p&gt;The blockchain industry is undergoing a profound architectural transition. For years, systems like Ethereum packaged execution, consensus, and data availability into a single monolithic layer. This approach simplified design but created hard tradeoffs: throughput suffered because every validator had to execute every transaction and store every byte of data. The scalability ceiling became apparent as transaction costs climbed and block times remained bounded by consensus limitations.&lt;/p&gt;

&lt;p&gt;The modular approach inverts this assumption. Instead of forcing all operations into one canonical layer, modular chains separate concerns into distinct roles. One layer handles execution (computing state transitions), another reaches consensus on which transactions are valid, and a third provides persistent data availability guarantees. Each layer optimizes for its specific function rather than compromising to fit a unified design. This separation is not merely theoretical; it reshapes how blockchain systems scale, how developers build applications, and where real economic constraints move.&lt;/p&gt;

&lt;p&gt;By 2026, this transition has moved beyond blockchain research papers and experimental networks. Production systems in use today bear the scars of monolithic limitations, and the modular paradigm has proven itself in live networks handling real economic activity. Understanding this shift is no longer optional for developers shipping blockchain systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Monolithic Blockchains Constrain Scalability
&lt;/h2&gt;

&lt;p&gt;Monolithic blockchains like Ethereum operate under a unified security model. Every full node must validate every transaction, execute every smart contract, and store the entire state of the network. This design provides simplicity and ensures that any participant can audit the system end-to-end. However, it creates an inescapable bottleneck.&lt;/p&gt;

&lt;p&gt;When a monolithic chain increases its block size or decreases its block time to process more transactions, the bandwidth and computation requirements for running a full node increase proportionally. A user who might run a full node on consumer hardware today cannot do so if block size doubles. This forces centralization: fewer participants can afford the infrastructure to validate blocks, and the network's security guarantees degrade. The monolithic design locks scalability and decentralization into a zero-sum game.&lt;/p&gt;

&lt;p&gt;Ethereum addresses this constraint through Layer 2 rollups, which batch transactions off-chain and post compressed proofs or transaction data back to Ethereum. Rollups inherit Ethereum's security assumptions, but they do so by treating Ethereum as a rigid settlement layer. The throughput gains come at the cost of additional complexity, liquidity fragmentation across multiple rollups, and bridging risk. The fundamental monolithic architecture remains unchanged.&lt;/p&gt;

&lt;p&gt;Performance becomes measurable in hard numbers. Ethereum's network today processes approximately 15 transactions per second on Layer 1, with a 12-second block time and approximately 32 GB of annual state growth. Every full node must keep up with this rate. A monolithic system cannot increase these numbers without asking node operators to upgrade hardware continuously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Layers of Modular Architecture
&lt;/h2&gt;

&lt;p&gt;Modular blockchain design splits responsibilities across three distinct layers, each with its own constraints and optimization targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution Layer
&lt;/h3&gt;

&lt;p&gt;The execution layer is where state transitions occur. Smart contracts execute, balances update, and applications run. The execution layer does not participate in consensus. Instead, it takes transactions from another source, processes them in a deterministic order, and outputs a new state root. This separation means the execution layer can process transactions as fast as the hardware allows, without waiting for a blockchain's consensus mechanism to reach agreement.&lt;/p&gt;

&lt;p&gt;An execution environment can be deployed as a separate chain (called an execution chain), or it can exist as a rollup that settles to another chain. What matters is the split: execution is now independent of the consensus process that secures the network. Different execution layers can have different virtual machines, different programming languages, different fee models, or different design philosophies. Solana uses a different execution model than Ethereum; Arbitrum's execution layer has different characteristics than Optimism's. None of them are forced to conform to a single standard because execution is no longer bundled with consensus.&lt;/p&gt;

&lt;p&gt;The execution layer's performance ceiling is determined by hardware capability and the complexity of the transactions it processes. A sufficiently powerful machine can execute more transactions per second than any monolithic blockchain's consensus layer can validate and finalize. This decoupling is the core insight that makes modular scaling possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consensus Layer
&lt;/h3&gt;

&lt;p&gt;The consensus layer determines which transactions are valid and in what order they occur. It does not execute transactions itself. Instead, the consensus layer reaches agreement on the ordering of transactions that the execution layer will process.&lt;/p&gt;

&lt;p&gt;In Ethereum's current monolithic model, consensus and execution are tightly coupled. Validators run the same virtual machine, execute the same transactions, and verify that the execution is correct. In a modular system, the consensus layer only cares about the ordering and authenticity of transaction data. Consensus reaches agreement on a commitment to transaction ordering without executing transactions itself.&lt;/p&gt;

&lt;p&gt;This separation allows the consensus layer to be simpler and faster. It can use a leaner protocol because it no longer needs to validate the computational correctness of every transaction. Proof of validity can be delegated to the execution layer, which produces cryptographic proofs that the execution was correct. The consensus layer verifies the proof, not the computation.&lt;/p&gt;

&lt;p&gt;Consider the difference in burden: a monolithic validator must have enough resources to execute every contract call in the network. A consensus-layer validator only needs to reach agreement on transaction ordering and verify lightweight proofs from the execution layer. The consensus layer scales differently because it has fundamentally different work to perform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Availability Layer
&lt;/h3&gt;

&lt;p&gt;The data availability layer guarantees that transaction data exists and is accessible to anyone who needs it. This layer does not execute transactions or reach consensus. It stores transaction data and provides a commitment that the data can be retrieved.&lt;/p&gt;

&lt;p&gt;In a monolithic blockchain, every full node stores all transaction history. This requirement ensures data availability trivially: if every node has the data, it is certainly available. However, this is an expensive solution that grows with every transaction.&lt;/p&gt;

&lt;p&gt;A modular system can separate the data availability commitment from the requirement that every node store all data. Technologies like data availability sampling allow light clients to verify that data is available by sampling a small random subset of it. The full data exists somewhere in the network, but no individual node needs to store it all. This makes data availability a scalable property rather than a bottleneck.&lt;/p&gt;

&lt;p&gt;The Ethereum beacon chain provides consensus for Ethereum mainnet. A dedicated data availability layer could provide data availability for multiple execution chains that build on top of it. Each execution chain is independent, but they all inherit the same security assumption about data availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethereum's Current Position: L2-Centric Monoliths
&lt;/h2&gt;

&lt;p&gt;Ethereum itself remains fundamentally monolithic in its architecture. It executes transactions, reaches consensus on them, and stores the data that proves what happened. Layer 2 rollups build on top of Ethereum by batching transactions and posting them back to Ethereum's Layer 1.&lt;/p&gt;

&lt;p&gt;This design is not accidental. Ethereum's monolithic architecture provides security and censorship resistance that early blockchain systems required. Running a full node is difficult but possible on consumer hardware. Ethereum processes enough transactions per second to serve as a settlement layer for higher-level protocols. It is decentralized enough that no single entity can stop it.&lt;/p&gt;

&lt;p&gt;Rollups exploit a key fact: they can prove that a batch of transactions was executed correctly without Ethereum needing to execute those transactions itself. An optimistic rollup (Arbitrum, Optimism) posts transaction data and a state root to Ethereum. If no one disputes the state root within a challenge window, the rollup assumes it is correct. If someone disputes it, Ethereum can re-execute a subset of transactions to determine the truth. A ZK rollup (StarkNet, zkSync) posts a cryptographic proof of correct execution. Ethereum verifies the proof without executing anything.&lt;/p&gt;

&lt;p&gt;Both approaches turn Ethereum into a settlement and data availability layer. This is a powerful position, but it is not a clean modular separation. Ethereum's execution layer still processes transactions, still validates them, and still stores state. The rollups are an add-on, not a fundamental architectural redesign.&lt;/p&gt;

&lt;p&gt;By 2026, Ethereum's roadmap includes Proto-Danksharding and Danksharding, which optimize Ethereum for the role of data availability layer. The Ethereum Execution Layer (EL) and Beacon Chain continue to handle consensus. Rollups build on top of this unified stack. This is an evolution toward a more modular model, but Ethereum remains a monolithic base layer with modular scaling on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  True Modular Chains: Sovereign Rollups and Beyond
&lt;/h2&gt;

&lt;p&gt;Some blockchain systems have committed to a more radical modular separation. Sovereign rollups, which gained prominence through frameworks like Rollkit and projects built on &lt;strong&gt;Cosmos&lt;/strong&gt;, implement execution and consensus separately but do not rely on an external consensus layer for security.&lt;/p&gt;

&lt;p&gt;A sovereign rollup typically uses &lt;strong&gt;Celestia&lt;/strong&gt; or similar systems for data availability. The rollup reaches its own consensus independently, either through a validator set or through a different consensus mechanism. When the rollup creates a block, it posts the data to the data availability layer. As long as the data is available, the rollup can prove its history and state to external observers. The data availability layer does not validate the rollup's state transitions or consensus; it only ensures the data exists.&lt;/p&gt;

&lt;p&gt;This design decouples execution from consensus entirely. The rollup's validators execute transactions and reach consensus on the ordering. A separate, lighter-weight process proves that the data is available. The system is fully modular: execution, consensus, and data availability are three separate concerns.&lt;/p&gt;

&lt;p&gt;The security tradeoff is real. A sovereign rollup does not inherit the security of an external consensus layer. Instead, it must maintain its own security through its own validator set or consensus mechanism. A rollup with few validators is more vulnerable to attack than a system with many validators. However, this design also provides flexibility: a rollup can upgrade its consensus mechanism or change its validator set without requiring approval from an external chain. It is truly sovereign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cosmos&lt;/strong&gt; chains exemplify this approach. Each zone in the Cosmos ecosystem runs its own execution layer and reaches its own consensus. Many zones use &lt;strong&gt;Interchain Security&lt;/strong&gt; to borrow consensus security from the Cosmos Hub, but they are not required to. The Hub provides a data availability layer through Inter-blockchain Communication (IBC), but each zone remains sovereign.&lt;/p&gt;

&lt;p&gt;Rollkit enables developers to build these kinds of chains using familiar frameworks. Rollkit wraps rollup execution in a standard Cosmos SDK application and handles the communication with a data availability layer. Developers write smart contracts or applications using Solidity or the Cosmos SDK, deploy to a rollup, and let Rollkit handle the modular plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Availability Problem and Solutions
&lt;/h2&gt;

&lt;p&gt;Data availability is often the most misunderstood aspect of modular architecture. When execution and consensus are separated from data storage, someone must guarantee that the data continues to exist and is retrievable.&lt;/p&gt;

&lt;p&gt;In a monolithic blockchain, full nodes store all historical data, and all current state. This provides data availability as a side effect: the data must be available because the nodes store it. However, it does not scale. If millions of transactions occur daily, the state and history grow continuously. A new node joining the network must download terabytes of data.&lt;/p&gt;

&lt;p&gt;A modular system must explicitly address data availability. One solution is to designate a separate set of nodes whose job is to store and serve data. These nodes do not execute transactions or reach consensus. They receive data from the execution and consensus layers, store it, and serve it to anyone who requests it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Celestia&lt;/strong&gt; implements this with data availability sampling. Celestia validators download only a small random sample of each block instead of the entire block. Through the mathematics of erasure coding, sampling allows light clients to verify that the full block is available without downloading the whole thing. If the data is truly available, a client sampling 1% of the block can confidently verify availability. If the data is missing a significant portion, the sampling will detect it with high probability.&lt;/p&gt;

&lt;p&gt;This design allows Celestia to scale the amount of data it can commit to per block without forcing every validator to download terabytes. Validators instead download kilobytes of samples and verify cryptographic commitments.&lt;/p&gt;

&lt;p&gt;Other data availability solutions exist. &lt;strong&gt;EigenLayer&lt;/strong&gt; provides data availability through a network of operators that stake cryptocurrency. If an operator claims data is available but it actually is not, the operator loses its stake. This creates economic incentives for honest behavior without requiring every operator to store all data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avail&lt;/strong&gt;, created by Polygon and later transferred to a foundation, uses a similar sampling technique to Celestia but with different economics and validator incentives.&lt;/p&gt;

&lt;p&gt;The choice of data availability layer affects the cost, security model, and throughput of the entire system. A rollup on &lt;strong&gt;Celestia&lt;/strong&gt; pays Celestia's fees for data. A rollup using &lt;strong&gt;Ethereum&lt;/strong&gt; as a data availability layer pays Ethereum's fees. The modular architecture allows rollups to choose their data availability provider based on their specific cost and security requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Modular Architecture Changes Developer Experience
&lt;/h2&gt;

&lt;p&gt;For developers building applications, the modular shift reshapes how they deploy and scale systems.&lt;/p&gt;

&lt;p&gt;In a monolithic model, a developer writes a smart contract and deploys it to a single chain. If the chain reaches capacity and transaction fees rise, the developer must convince the chain's community to increase capacity, or migrate to a different monolithic chain (Polygon, Avalanche, etc.). Migration requires redeploying contracts, managing liquidity across chains, and dealing with bridging risk.&lt;/p&gt;

&lt;p&gt;In a modular system, developers have more choices. If Ethereum Layer 1 becomes expensive, a developer can deploy to an Optimistic Rollup like Arbitrum or Optimism, which settles to Ethereum. If the rollup becomes congested, the developer can deploy to multiple rollups simultaneously and use bridges to provide liquidity. If none of the Ethereum-based options meet their needs, they can deploy to a sovereign rollup like a Cosmos zone or a Rollkit-based chain.&lt;/p&gt;

&lt;p&gt;The modular architecture also enables experimentation. A developer can test a new execution environment or consensus mechanism without needing to coordinate with a global set of validators. A rollup can choose to use a ZK proof system instead of optimistic proofs if the tradeoffs suit their application. A Cosmos zone can experiment with a custom virtual machine without modifying the Cosmos SDK.&lt;/p&gt;

&lt;p&gt;However, this flexibility comes with fragmentation. Liquidity becomes split across multiple rollups. A user might hold tokens on Arbitrum, Optimism, and a standalone Cosmos zone simultaneously. Bridges enable liquidity flow, but bridges introduce counterparty risk. This is the tradeoff the modular paradigm introduces: scalability and flexibility at the cost of liquidity concentration and cross-chain complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Economic Incentives and Fee Markets
&lt;/h2&gt;

&lt;p&gt;Modular architecture changes how fees are distributed across layers and how economic incentives function.&lt;/p&gt;

&lt;p&gt;In a monolithic blockchain, the fee market is centralized. Users compete in a single fee pool. Miners or validators determine which transactions are included based on the fees offered. All fees are collected and distributed to the security providers at that layer.&lt;/p&gt;

&lt;p&gt;In a modular system, fees are distributed across layers. A user's transaction might generate fees for the execution layer (if it is a rollup), for the consensus layer (if it uses an external consensus), and for the data availability layer. Each layer has its own fee market and its own incentive structure.&lt;/p&gt;

&lt;p&gt;Consider a transaction on Arbitrum, which posts to Ethereum. The transaction pays fees to Arbitrum sequencers for inclusion and execution. The batch is posted to Ethereum, where it competes in Ethereum's fee market for block space. The batch also uses &lt;strong&gt;Ethereum&lt;/strong&gt; data availability, so the cost includes the cost of posting data to Ethereum. The total cost is the sum of these fees, and each component has its own market dynamics.&lt;/p&gt;

&lt;p&gt;This separation creates new economic properties. As Ethereum's Layer 1 becomes more expensive, the advantage of posting to a rollup increases. The data availability layer's fee market becomes decoupled from the execution layer's fee market. If the execution layer is congested but data is cheap, transactions can be batched efficiently and posted cheaply.&lt;/p&gt;

&lt;p&gt;However, the separation also introduces inefficiency. A single-layer system achieves better capital efficiency for its security providers because all fees go to one place. A modular system distributes fees, which means each layer must attract enough fees to incentivize security provision independently. In practice, this means the total fees paid across modular layers may exceed the fees of a monolithic system processing the same transaction volume, because each layer must maintain its own economic security.&lt;/p&gt;

&lt;p&gt;By 2026, most transaction volume on Ethereum occurs on Layer 2 rollups, and this fee distribution pattern is well-established. The economic reality is clear: execution layers (rollups) can charge low fees, pushing users toward them. Data availability and settlement on Ethereum remain the costlier components of the transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Implications of Modularity
&lt;/h2&gt;

&lt;p&gt;Modular architecture changes how security is maintained and where risks emerge.&lt;/p&gt;

&lt;p&gt;A monolithic blockchain's security comes from the difficulty of attacking the entire system: an attacker would need to compromise the execution layer, the consensus layer, and the data layer simultaneously. If one component is secure, the whole system is secure. The security model is unified.&lt;/p&gt;

&lt;p&gt;A modular system's security is only as strong as its weakest modular layer. If the data availability layer is compromised, even an execution layer with perfect security can be attacked. An attacker could prove false execution results if the data that would expose the lie is unavailable. The security is now multiplicative: every layer must be secure for the whole system to remain secure.&lt;/p&gt;

&lt;p&gt;This creates new attack surfaces. The data availability layer must be secured against attacks that specifically target availability rather than validity. An attacker does not need to prevent the correct data from existing; they only need to make it inaccessible. This is a different threat model than traditional Byzantine fault tolerance, which assumes an attacker can forge arbitrary data but cannot delete data from the majority of honest nodes.&lt;/p&gt;

&lt;p&gt;Solutions like Celestia address this with data availability sampling, which makes large-scale data deletion attacks expensive and detectable. However, the attack surface still exists and requires explicit defense.&lt;/p&gt;

&lt;p&gt;Another security concern emerges from the separation of execution and consensus. An execution layer that relies on an external consensus layer must trust that the consensus layer is operating correctly. If the consensus layer is under attack, an execution layer building on top of it inherits that vulnerability. Sovereign rollups face this differently: they must maintain their own consensus security, which is harder than borrowing security from an established layer.&lt;/p&gt;

&lt;p&gt;Bridges between modular layers introduce additional risk. A bridge is a smart contract that locks tokens on one chain and mints them on another. If the bridge is exploited, users lose funds. As of 2026, bridge security remains an unsolved category of risk. Each modular system must implement bridges carefully, and users must understand the risk they are accepting when moving value across a bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 Landscape: Dominance and Experiments
&lt;/h2&gt;

&lt;p&gt;By 2026, the modular paradigm is not theoretical. Several systems are live and handling real transaction volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arbitrum&lt;/strong&gt; and &lt;strong&gt;Optimism&lt;/strong&gt; are the largest Optimistic Rollups on Ethereum. Both run independent execution layers, post transaction data and state roots to Ethereum, and rely on Ethereum for data availability and settlement. Together, they process more daily transaction volume than Ethereum Layer 1. Their economics and operation are well-understood by developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polygon&lt;/strong&gt; operates as a separate monolithic chain with its own validators and consensus, but it also recently announced a shift toward an AggLayer that aggregates multiple Polygon Chains, moving toward a more modular model where individual chains can be more lightweight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solana&lt;/strong&gt; remains a monolithic chain, but its execution performance is comparable to or better than most rollups. Its validators handle execution and consensus in a tightly coupled design that prioritizes speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cosmos&lt;/strong&gt; is the most developed modular ecosystem. Multiple zones (Osmosis, Juno, Stargaze, Stride) run as sovereign chains with their own execution and consensus, often using Cosmos for data availability through Inter-blockchain Communication. Rollkit is emerging as a tool for developers to build these chains more easily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Celestia&lt;/strong&gt; has become the primary standalone data availability layer. It reached mainnet in 2024 and has processed gigabytes of data from rollups building on top of it. Its existence proves that data availability as a separate layer is feasible at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;StarkNet&lt;/strong&gt;, an Ethereum-based ZK rollup, uses &lt;strong&gt;Cairo&lt;/strong&gt; as its execution environment, which is fundamentally different from the EVM. This demonstrates that modular architecture allows execution environments to diverge without losing compatibility with the rest of the system.&lt;/p&gt;

&lt;p&gt;Experiments continue. &lt;strong&gt;Fuel&lt;/strong&gt; is building a modular execution layer and exploring new virtual machine designs. &lt;strong&gt;Espresso&lt;/strong&gt; is developing a data availability and consensus layer specifically designed for modular systems. &lt;strong&gt;Eigenlayer&lt;/strong&gt; is enabling Ethereum validators to provide services for other modular layers without abandoning Ethereum.&lt;/p&gt;

&lt;p&gt;By 2026, the modular paradigm is the dominant architectural direction for new blockchain systems. Monolithic blockchains still exist and still secure significant value, but the innovation and growth is happening in modular designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs: When Monolithic Makes Sense
&lt;/h2&gt;

&lt;p&gt;Despite the dominance of modular thinking, monolithic designs still have advantages.&lt;/p&gt;

&lt;p&gt;A monolithic blockchain is simpler to understand. The security model is unified. There is one place to look to understand how the system works. For a user, this simplicity is valuable. A user on Ethereum knows that their transaction was executed, validated, and finalized by the same set of validators. The trust model is clean.&lt;/p&gt;

&lt;p&gt;Monolithic blockchains may be more capital-efficient for security provision. A single set of validators secures execution, consensus, and data. They are not duplicating security work across multiple layers. If a system can achieve monolithic simplicity without sacrificing scaling ability, it might be more efficient than a modular system.&lt;/p&gt;

&lt;p&gt;Monolithic blockchains with very high throughput (like Solana or a hypothetical future Ethereum with much larger blocks) can serve applications that do not require modular scaling. If a single layer can process all transaction demand at acceptable costs, adding modular complexity adds no value.&lt;/p&gt;

&lt;p&gt;However, as of 2026, most monolithic blockchains have not achieved the throughput and cost profile that makes them clearly superior to rollups. Solana's throughput is higher than Ethereum's, but it sacrifices decentralization. Ethereum Layer 1 remains slow and expensive. Monolithic designs remain useful for specific niches, but they are not the direction of industry growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Patterns for Modular Systems
&lt;/h2&gt;

&lt;p&gt;Developers building on modular systems use several established patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollup as Execution Layer
&lt;/h3&gt;

&lt;p&gt;A rollup is a chain that runs on top of another chain for settlement. The rollup maintains its own sequencer and execution environment. Transactions are ordered by the sequencer, executed off-chain, and batched. The batch is posted to the settlement layer along with a proof or state root. The settlement layer verifies the batch or stores the data for later verification.&lt;/p&gt;

&lt;p&gt;Arbitrum uses a sequencer that batches transactions and posts them every few seconds. Optimism does the same. Both chains process transactions at full speed off-chain, then settle in batches to Ethereum.&lt;/p&gt;

&lt;p&gt;Implementing a rollup requires a sequencer, a node operator that can execute transactions, a bridge contract on the settlement layer, and some mechanism for users to withdraw value from the rollup back to the settlement layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sovereign Rollup with Rollkit
&lt;/h3&gt;

&lt;p&gt;A sovereign rollup uses Rollkit to handle communication with a data availability layer. The developer writes a blockchain using the Cosmos SDK, and Rollkit wraps it for deployment on top of Celestia or another data availability layer.&lt;/p&gt;

&lt;p&gt;The developer's blockchain reaches consensus on its own (either with a validator set or through another consensus mechanism) and executes transactions using the Cosmos SDK virtual machine. Rollkit ensures that blocks are posted to the data availability layer. Users run light clients that verify availability and check state proofs locally.&lt;/p&gt;

&lt;p&gt;This pattern reduces the burden of building a blockchain. The developer does not need to implement consensus, data availability, or light client verification. Rollkit and the data availability layer provide these primitives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Sequencing and MEV Management
&lt;/h3&gt;

&lt;p&gt;A problem emerges in modular systems: who orders transactions at the execution layer? In a monolithic blockchain, the consensus layer orders transactions. In a rollup, a sequencer makes ordering decisions. If the sequencer is a single entity, it can extract maximum extractable value (MEV) by reordering transactions for profit.&lt;/p&gt;

&lt;p&gt;Shared sequencers (being developed by projects like &lt;strong&gt;Espresso&lt;/strong&gt; and &lt;strong&gt;MerlinChain&lt;/strong&gt;) provide a service layer that orders transactions for multiple rollups. The shared sequencer reaches consensus with other sequencers, creating a fairer and more decentralized ordering service. Multiple rollups can use the same shared sequencer without needing to bootstrap their own consensus.&lt;/p&gt;

&lt;p&gt;This is a new pattern specific to modular systems. A shared sequencer is not a settlement layer or a data availability layer; it is a specialized service layer that sits between execution and consensus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Forward: The Modular Stack of 2026 and Beyond
&lt;/h2&gt;

&lt;p&gt;By 2026, it is clear that most new blockchain systems will be modular. The architectural advantages are too significant to ignore. However, the exact configuration of modular stacks varies.&lt;/p&gt;

&lt;p&gt;Ethereum remains the dominant settlement and data availability layer for Layer 2 rollups. Its position provides network effects and security advantages that are hard to replicate. However, Ethereum's dominance is not universal. Alternative data availability layers like &lt;strong&gt;Celestia&lt;/strong&gt;, &lt;strong&gt;Avail&lt;/strong&gt;, and &lt;strong&gt;EigenLayer&lt;/strong&gt; are competing to serve rollups that do not require Ethereum's security.&lt;/p&gt;

&lt;p&gt;The execution layer is where fragmentation is most intense. Each rollup optimizes for different tradeoffs. Arbitrum prioritizes compatibility with Ethereum tooling and the EVM. Optimism makes similar choices. StarkNet uses Cairo to enable more efficient ZK proofs. Other rollups experiment with different virtual machines, different programming languages, and different execution models.&lt;/p&gt;

&lt;p&gt;Consensus is increasingly decoupled from execution. Rollups do not need their own consensus; they can rely on external consensus or sampling-based availability proofs. This reduces the burden of building a blockchain and allows execution layers to compete on execution speed and developer experience rather than consensus mechanism.&lt;/p&gt;

&lt;p&gt;The modular paradigm is likely to continue evolving. New data availability schemes may emerge. New execution environments may become dominant. The exact stack may change. However, the principle that execution, consensus, and data availability should be independent layers is now established and unlikely to reverse.&lt;/p&gt;

&lt;p&gt;For developers, this means opportunity and complexity. Opportunity because new tools and frameworks make it easier to build blockchain systems. Complexity because choosing the right stack for a specific application requires understanding the tradeoffs across multiple layers. The days of deploying to "the blockchain" are over. Developers now must choose: which execution layer, which consensus mechanism, which data availability provider, and how do they combine into a coherent system for their application's needs.&lt;/p&gt;

&lt;p&gt;If you are building blockchain infrastructure or shipping applications on modular systems, I provide professional Web3 documentation and full-stack Next.js development work; you can view my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>systemdesign</category>
      <category>web3</category>
    </item>
    <item>
      <title>Sui Network's Technical Architecture: Analyzing TVL Growth, Stablecoin Throughput, and Horizontal Scalability</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Mon, 04 May 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/sui-networks-technical-architecture-analyzing-tvl-growth-stablecoin-throughput-and-horizontal-104m</link>
      <guid>https://forem.com/mericcintosun/sui-networks-technical-architecture-analyzing-tvl-growth-stablecoin-throughput-and-horizontal-104m</guid>
      <description>&lt;p&gt;As of February 2026, the Sui network has accumulated $2.6 billion in total value locked across its ecosystem and processed $2.03 trillion in stablecoin transfer volume. These metrics reflect not marketing momentum but fundamental architectural decisions that enable scale. Understanding why Sui achieves these numbers requires examining the technical layers that make horizontal scalability workable: the consensus mechanism, object-centric data model, and transaction routing infrastructure. This article dissects the engineering foundations that distinguish Sui from earlier blockchain designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Object Model and Data Parallelism
&lt;/h2&gt;

&lt;p&gt;Most blockchain networks organize state around accounts or UTXO models. Sui diverges fundamentally by treating digital assets as owned objects with explicit ownership semantics. Each object carries an owner field and a unique ID. A user's balance is not a number stored in an account; it is a set of discrete coin objects the user owns. This distinction enables massive parallelism because transactions that operate on different objects can execute concurrently without conflict.&lt;/p&gt;

&lt;p&gt;The Sui runtime checks object ownership at transaction submission time. When a transaction modifies multiple objects, the network verifies that the sender controls those objects or that the operation respects transfer rules encoded in smart contracts. Non-conflicting transactions proceed in parallel; only transactions contending for the same object require ordering. This differs sharply from Ethereum's account model, where all state changes must eventually reach finality through sequential block application, even if they affect unrelated accounts.&lt;/p&gt;

&lt;p&gt;Coin operations illustrate this concretely. If Alice sends SUI to Bob and Carol simultaneously sends SUI to David, these transactions operate on four distinct coin objects. Under Sui's model, both transactions can be validated and executed in parallel against the same ledger state. The network never requires these transactions to serialize through a shared account structure. Stablecoin transfers benefit most dramatically from this design because USDC, USDT, and other stable assets generate high volume with minimal cross-transaction dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mysticeti: Leader-Based Consensus with Pipelined Rounds
&lt;/h2&gt;

&lt;p&gt;Sui implements consensus through &lt;strong&gt;Mysticeti&lt;/strong&gt;, a protocol that extends the Narwhal and Bullshark design with pipelined commitment and a designated leader per round. Mysticeti does not require Byzantine agreement on every transaction. Instead, it batches transactions into certificates of availability and commits them based on quorum signatures from validators. The protocol separates data availability from consensus finality, allowing the network to confirm transactions for wallet display and ordering even before validators reach full consensus.&lt;/p&gt;

&lt;p&gt;A Mysticeti round proceeds as follows: a designated leader proposes a block containing references to prior certificates. Validators sign off on the availability of this block, creating a quorum certificate (QC). Once two-thirds of validators have signed, the block is available to all parties and the leader can proceed to the next round without waiting for Byzantine agreement on which prior blocks to include. This structure achieves consensus with fewer rounds and lower latency than traditional PBFT variants.&lt;/p&gt;

&lt;p&gt;The leader role rotates through validators based on a deterministic schedule, eliminating the need for leader election. Each validator knows which validator is leader in round N, N+1, and beyond. When the current leader fails, the protocol has a skip mechanism: if no certificate arrives within a timeout, validators move to the next round and the next validator becomes leader. This design cuts consensus overhead significantly compared to protocols that must detect leader failure and elect a replacement.&lt;/p&gt;

&lt;p&gt;Pipelining amplifies these gains. While validators process round N, the leader for round N+1 can begin building their block. The blocks do not wait for full finality of earlier rounds before progressing. This overlapping of rounds reduces end-to-end latency from transaction submission to full consensus commitment, allowing Sui to confirm transactions to users quickly while maintaining cryptographic security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Causal Ordering and Commit Chains
&lt;/h2&gt;

&lt;p&gt;Sui establishes a partial order over transactions using causal dependencies rather than requiring total order. A transaction can reference prior transactions that affected its input objects. These references form a directed acyclic graph (DAG) of dependencies. Two transactions with no causal path between them can be finalized concurrently; the network records both alongside a proof that they do not conflict.&lt;/p&gt;

&lt;p&gt;The consensus mechanism builds commit chains of certified transactions. Each committed transaction includes a reference to all prior transactions it depends on, plus a threshold of validator signatures attesting to this dependency. The commit chain grows as new blocks are finalized. Users can verify transaction safety by checking the chain and confirming that validators have attested to both the transaction's availability and its dependency history.&lt;/p&gt;

&lt;p&gt;This causal ordering model handles stablecoin transfers efficiently. A user submitting many small transfers to different recipients does not block behind a single serialized ordering. The transfers causal history preserves their relationship, but the network processes them in parallel. Only if two transfers spend the same coin object does ordering become mandatory; the consensus protocol then sequences them through the commit chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narwhal and Bullshark Data Availability Layer
&lt;/h2&gt;

&lt;p&gt;Underlying Mysticeti is the data availability work from Narwhal and Bullshark. &lt;strong&gt;Narwhal&lt;/strong&gt; is a mempool and data availability engine. Validators in Narwhal advertise the availability of batches of transactions they have stored. These advertisements are lightweight and validators accumulate them into certificates proving that at least two-thirds of the network has stored the batch. Once a Narwhal certificate exists, the batched transactions are guaranteed to be retrievable from at least two-thirds of validators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bullshark&lt;/strong&gt; consumes Narwhal certificates and applies a finality algorithm to order them. Bullshark produces a commit chain by applying the Mysticeti consensus rules to the certified batches. The separation of data availability from ordering allows Sui to commit transactions that may not yet be available on all validators. Validators download missing data lazily; the commit chain serves as a proof obligation that a transaction is available somewhere in the quorum.&lt;/p&gt;

&lt;p&gt;This architecture reduces the storage burden on validators and decreases the latency required for transactions to reach users. A small validator or light client can verify that a transaction is committed to the Sui canonical history without downloading the full transaction; the commit chain is cryptographic proof of availability. Only when executing the transaction or verifying its effects do users fetch the full data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkpoints and Finality
&lt;/h2&gt;

&lt;p&gt;Sui produces checkpoints approximately every 2 seconds, grouping committed transactions into frozen sequences. Each checkpoint contains a set of transaction digests that executed during that period, the resulting state changes, and validator signatures certifying the checkpoint. Checkpoints establish the global ordering of transactions and enable validators to recover from crashes by replaying from the latest checkpoint.&lt;/p&gt;

&lt;p&gt;Finality in Sui is probabilistic and becomes absolute after enough checkpoints have passed. Validators sign the checkpoint, but the Sui protocol does not treat a single checkpoint signature as irreversible. The network exhibits an implicit finality as a function of how far ahead the current checkpoint is from the transaction's checkpoint. After 30 subsequent checkpoints, a transaction is finalized with negligible probability of reversal given the cost to corrupt or replace validators.&lt;/p&gt;

&lt;p&gt;Checkpoint frequency relates directly to throughput capacity. Faster checkpoints commit more transactions per second but increase validator work. Sui's 2-second cadence reflects a balance between confirmation latency (users see transactions included in a checkpoint within 2 seconds) and validator resource consumption. The network adjusts this parameter through protocol updates based on validator hardware capabilities and network bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Horizontal Scalability Through Dynamic Partitioning
&lt;/h2&gt;

&lt;p&gt;Horizontal scalability in Sui comes from dynamic partitioning of the validator set. Rather than increasing the number of validators in a single consensus group, Sui can partition validators into committees that each handle a subset of objects or accounts. Objects in different partitions reach consensus independently; only transactions that touch objects in multiple partitions require inter-partition coordination.&lt;/p&gt;

&lt;p&gt;The protocol currently operates with a single committee for simplicity, but the architecture supports multiple committees. A governance decision can increase the number of committees, each running independent consensus while a lightweight coordination layer ensures global consistency. This design resembles sharding but applies at the validator level rather than requiring clients to track which shard holds which data.&lt;/p&gt;

&lt;p&gt;Stablecoin volume demonstrates why this matters. USDC and USDT transfers, when targeting different recipients, do not conflict. In a multi-committee model, transfers within a single committee finalize without involving validators from other committees. The network scales stablecoin throughput by partitioning the validator set such that more validators collectively process more transfers concurrently. The technical barrier is not consensus latency but validator computation and network bandwidth for data synchronization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gas Pricing and Resource Metering
&lt;/h2&gt;

&lt;p&gt;Sui's gas model charges for computation, storage, and bandwidth used by each transaction. Unlike Ethereum, which charges a single gas price for all resources, Sui separately meters computation steps, memory reads and writes, and network transmission. A transaction's gas cost reflects the actual validator resources it consumed.&lt;/p&gt;

&lt;p&gt;This separation enables fine-grained resource accounting. A transaction that reads 100 objects pays more for storage I/O than a transaction that reads 10. A computation-heavy smart contract operation uses computation gas. Storage gas includes a per-byte cost for new objects created and a reclamation incentive when objects are deleted. These distinctions encourage developers to write efficient contracts because the cost signal is proportional to resource use.&lt;/p&gt;

&lt;p&gt;The gas pricing mechanism also provides natural scaling incentives. As the network experiences higher load, storage and network gas prices rise before computation gas. Validators publish gas price schedules that reflect current resource availability. Transactions submitted when demand is high must pay more, encouraging users to defer non-urgent operations. This congestion pricing prevents the network from accepting more transactions than validators can process.&lt;/p&gt;

&lt;h2&gt;
  
  
  TVL Composition and Why Objects Scale It
&lt;/h2&gt;

&lt;p&gt;The $2.6 billion in TVL as of February 2026 reflects deposits in lending protocols, concentrated liquidity positions, staked tokens, and bridge-locked assets. Each of these asset classes benefits from Sui's parallelism differently.&lt;/p&gt;

&lt;p&gt;Lending protocols such as Scallop and Aptos Lending benefit because loan deposits and borrows operate on disjoint objects. A user depositing SUI does not contend with another user borrowing USDC; their transactions execute in parallel. Interest accrual is computed lazily when users withdraw, avoiding global state updates that serialize operations.&lt;/p&gt;

&lt;p&gt;Concentrated liquidity positions on Sui DEXs like Cetus and Aftermath Finance are individual objects. Each liquidity position is a distinct NFT with its own balance of paired assets. Swaps that trade against different positions execute concurrently. This parallelism is fundamental to the TVL figure; protocols can safely accumulate liquidity without per-position fees becoming a bottleneck.&lt;/p&gt;

&lt;p&gt;Bridge-locked assets accumulate when external blockchains use Sui as a settlement layer via canonical bridges such as wormhole and Layerzero integrations. Bridge validators must lock collateral on each chain to vouch for cross-chain messages. On Sui, bridge deposits are objects; multiple simultaneous bridge operations do not conflict. The network can process bridge volume that would congest other chains because the object model permits parallelism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stablecoin Transfer Volume: Technical Limits
&lt;/h2&gt;

&lt;p&gt;The $2.03 trillion in stablecoin transfer volume in February 2026 represents cumulative transfer value, not a measure of unique stablecoins or balances. Calculating how this translates to daily or monthly throughput illustrates network capacity limits.&lt;/p&gt;

&lt;p&gt;If the 2.6 billion TVL figure implies approximately 1.3 million transactions per month at typical token amounts (roughly $2000 per transfer average), this suggests the network processed approximately 42,000 transactions per second worth of stablecoins during peak periods. Sui's block size and checkpoint frequency support this: with 2-second checkpoints and 10,000-20,000 transaction capacity per checkpoint, the network achieves 5,000-10,000 transactions per second in the mainstream case. Stablecoin operations can saturate this capacity because they are simple coin transfers with minimal smart contract overhead.&lt;/p&gt;

&lt;p&gt;The variance in transfer volume monthly reflects actual adoption patterns. High-volume months typically correspond to market volatility or bridge activity increases, which require more stablecoin swaps and transfers. Low months indicate reduced bridge arbitrage and trading. The aggregated 2.03 trillion figure represents genuine users and bots moving value through the network, not theoretical throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Contract Implications of the Object Model
&lt;/h2&gt;

&lt;p&gt;The object model influences how developers structure smart contracts. Because objects must have a single owner or be shared, mutable state must reside in appropriately scoped objects. A game contract managing player inventories creates an object per player or per inventory slot. A protocol contract managing collateral creates an object per user per collateral type.&lt;/p&gt;

&lt;p&gt;This differs from Solidity's approach, where state is a map in a contract and all map entries are serialized into a single account structure. Sui developers must be more explicit about object boundaries, but this explicitness enables the runtime to identify parallelism statically. The Sui Move language enforces object permissions at compile time; code that would require exclusive access to state is flagged, and the developer must either redesign or accept serial ordering.&lt;/p&gt;

&lt;p&gt;Experienced Move developers structure contracts assuming parallelism. They use object pools or registry patterns where a central object stores references to many user objects, allowing the central registry to be read-only (or modified through immutable updates) while individual user objects are modified concurrently. Patterns like this require intentional design but yield throughput benefits that Solidity developers cannot access without relying on layer-2s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validator Economics and Decentralization
&lt;/h2&gt;

&lt;p&gt;Sui's protocol relies on validator participation, and validator incentives shape network health. Validators earn gas fees from transactions they process and publish. The staking mechanism allows SUI token holders to delegate to validators, who earn a percentage of their validator's gas fee income. Validators must post minimum collateral (currently several million SUI) to join the validator set.&lt;/p&gt;

&lt;p&gt;This structure creates economic incentives for validator participation but also creates barriers to entry. New validators must accumulate sufficient SUI to meet collateral requirements, limiting the pace at which new validators can join. The current validator set comprises approximately 100-150 validators, a smaller number than some competing L1s but larger than early Ethereum or Solana.&lt;/p&gt;

&lt;p&gt;The validator economics relate directly to throughput. Higher transaction volume generates more gas fees, incentivizing more validators to join. However, saturation occurs when validator collateral requirements exceed the profitability of running infrastructure, limiting validator count growth. Sui governance can adjust collateral requirements through proposals, trading off validator count against economic incentives.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Bloat and Storage Incentives
&lt;/h2&gt;

&lt;p&gt;As transactions execute, they create new objects. Without pruning, the total state size grows indefinitely. Sui addresses this through a storage fund mechanism: when a transaction creates an object, it must prepay storage fees for the object's lifetime. These storage fees accumulate in a dedicated fund. Validators withdraw from the fund as storage becomes irrelevant (objects deleted or archived).&lt;/p&gt;

&lt;p&gt;This design incentivizes object deletion. Developers can design contracts that return collateral to users when objects are cleaned up, creating an economic signal to remove stale data. A stablecoin transfer that creates intermediate objects for ordering or routing can be structured to delete those objects immediately after use, reclaiming fees.&lt;/p&gt;

&lt;p&gt;The storage fund prevents state bloat from pricing validators into insolvency. Early blockchains underestimated storage costs; Ethereum's state is now approximately 1 TB per node, making full validation economically unviable for individuals. Sui's storage fee mechanism ensures that state growth is economically sustainable by forcing users to internalize the cost of persistent state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Performance During High Load
&lt;/h2&gt;

&lt;p&gt;Empirical measurements of Sui under load reveal operational characteristics that explain the TVL and stablecoin figures. During high-transaction periods, the network maintains 5,000-10,000 TPS with sub-second finality for simple transfers. This throughput scales approximately linearly with the number of disjoint objects being accessed. Transactions that touch 100 objects can execute in parallel with other transactions touching different objects, provided consensus latency remains acceptable.&lt;/p&gt;

&lt;p&gt;Latency spikes occur when transactions contend for the same objects. A popular NFT mint contract that serializes through a single object experiences increased latency as transactions queue for ownership of that object. Developers observe this and either design contracts to avoid bottlenecks (using multiple objects or randomized assignments) or accept that certain operations will serialize. This visibility into performance trade-offs is itself a feature; developers cannot accidentally design slow contracts and blame the protocol.&lt;/p&gt;

&lt;p&gt;Bridge operations occasionally spike load because bridge validators must commit transactions to multiple chains atomically. Sui's cross-chain message handling currently routes through dedicated validators; bottlenecks at this layer can reduce overall network capacity. Future optimizations to bridge protocols may distribute cross-chain validation across the validator set, improving throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison to Account-Model Blockchains
&lt;/h2&gt;

&lt;p&gt;Ethereum and Solana use account models where all state mutations must eventually serialize. Ethereum achieves this through its blockchain structure; each block includes all transactions that execute in that block, applied sequentially to the state root. This approach is secure and simple to implement but limits parallelism. Even if two transactions touch different accounts, they must be ordered relative to each other within a block.&lt;/p&gt;

&lt;p&gt;Solana improves parallelism by allowing transactions to declare accounts they access upfront. The Sealevel runtime uses these declarations to schedule transactions onto cores, running non-conflicting transactions in parallel. However, the blockchain itself is still a total order; the parallelism occurs at runtime but the consensus mechanism orders transactions sequentially. Solana's throughput ceiling is fundamentally limited by how many transactions the leader can broadcast and how quickly the network can achieve consensus on a single proposer's block.&lt;/p&gt;

&lt;p&gt;Sui's approach is distinct because the consensus mechanism itself produces a partial order, not a total order. Mysticeti and Bullshark commit causally-ordered transactions without requiring global consensus on a single canonical sequence. Multiple transactions with no causal relationship can be finalized in parallel. This architectural difference is why Sui can sustain higher throughput for workloads with minimal cross-transaction dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stablecoin Protocols and Bridge Economics
&lt;/h2&gt;

&lt;p&gt;The $2.03 trillion in stablecoin transfer volume flows through several categories: on-chain swaps via DEXs, bridge transfers between chains, and direct user transfers. DEX operations involving stablecoins dominate in terms of transaction count because each swap is a discrete transfer. Bridge transfers involve fewer transactions but larger amounts; an institutional arbitrage operation might move $100 million in USDC through a bridge in a single transaction.&lt;/p&gt;

&lt;p&gt;Stablecoin protocols on Sui include USD (a native Sui stablecoin), wormhole-wrapped USDC and USDT, and stargate-wrapped LP tokens. Each of these assets follows the coin object model; transfers and swaps execute concurrently when they involve different coins. This parallelism allows protocols to support high volume without increasing the size of the validator set or upgrading hardware.&lt;/p&gt;

&lt;p&gt;The bridge ecosystems benefit from Sui's throughput because cross-chain arbitrage requires rapid settlement. If Sui had lower throughput, arbitrage opportunities would be missed and stablecoin bridges would see less traffic. The feedback loop is direct: higher throughput enables more efficient bridges, which attracts more volume, which justifies validator upgrades and governance investment in throughput improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Scaling and Partitioning
&lt;/h2&gt;

&lt;p&gt;The Sui Foundation and validator community have outlined plans for multi-committee execution. Rather than increasing a single validator committee, the network would partition validators into multiple committees, each responsible for a subset of state. Transactions affecting only objects within a committee would finalize through that committee's consensus. Transactions spanning committees would require lightweight coordination.&lt;/p&gt;

&lt;p&gt;This multi-committee model would provide linear scaling: N committees could process N times the transaction volume of a single committee, provided transactions are distributed such that most touch objects within single committees. In practice, this requires careful object design by protocols. A DEX that creates separate pools per pair rather than a single shared pool would see transactions distribute naturally across committees. A protocol that centralizes state in a single object would become a bottleneck.&lt;/p&gt;

&lt;p&gt;The technical challenges in multi-committee Sui are not consensus or consensus latency but transaction routing, state sharding, and maintaining consistency across partition boundaries. Sui's research on these problems is ongoing, and the foundation expects multi-committee capability within the 2026-2027 timeframe.&lt;/p&gt;

&lt;p&gt;For Web3 documentation or full-stack Next.js development work, visit &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project requirements.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Decentralized Oracle Manipulation and Price Feed Security</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Sat, 02 May 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/decentralized-oracle-manipulation-and-price-feed-security-2mla</link>
      <guid>https://forem.com/mericcintosun/decentralized-oracle-manipulation-and-price-feed-security-2mla</guid>
      <description>&lt;p&gt;Oracle manipulation remains one of the highest-impact attack vectors in decentralized finance. When an oracle reports false prices, the damage propagates through every protocol that depends on those prices for collateral valuation, liquidation thresholds, and yield calculations. This article examines how low-liquidity pools enable price manipulation, why external dependencies create systemic risk, and how time-weighted average price models serve as a critical defense mechanism against flash loan attacks and coordinated price movements.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Oracles Become Attack Surfaces
&lt;/h2&gt;

&lt;p&gt;An oracle is fundamentally a mechanism that brings off-chain data into a blockchain environment where smart contracts can read it. The oracle problem itself is not new: how do you ensure that data reported to an immutable ledger has not been tampered with, delayed, or misrepresented by the entity reporting it? Most decentralized protocols solve this through one of three approaches: centralized data providers like Chainlink that aggregate prices from multiple exchanges, decentralized oracle networks where participants stake capital and earn rewards for honest reporting, or on-chain mechanisms that derive prices from blockchain state itself.&lt;/p&gt;

&lt;p&gt;Each approach exposes different attack surfaces. A centralized oracle can be compromised at the source, losing the decentralization guarantee entirely. A decentralized oracle network with insufficient economic security can be attacked if the cost of corrupting reporters falls below the potential profit. An on-chain price oracle derived from DEX liquidity becomes vulnerable when that liquidity itself can be manipulated through flash loans or large trades in low-liquidity pools.&lt;/p&gt;

&lt;p&gt;The common thread is dependency: if an oracle is wrong, downstream systems fail in predictable ways. A lending protocol using false prices may over-collateralize risky positions or liquidate sound ones. A spot trading platform with stale price data executes trades at mismatched rates. A yield aggregator compounds losses across multiple positions when it rebalances based on manipulated metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mechanics of Price Manipulation Through Low-Liquidity Pools
&lt;/h2&gt;

&lt;p&gt;Flash loan attacks have made price manipulation through DEX liquidity pools a repeatable, often profitable attack. The attack works because on-chain DEX prices are derived directly from the ratio of tokens in a liquidity pool. When you swap tokens in a Uniswap V2 or V3 pool, you move along a curve. The price at any point is determined by the ratio of reserve balances.&lt;/p&gt;

&lt;p&gt;Consider a low-liquidity pool with 1000 WETH and 2,000,000 USDC in reserves. The spot price is 2000 USDC per WETH. An attacker using a flash loan borrows a large amount of USDC and swaps it for WETH in this pool. The swap moves the pool along its curve, pushing the price higher. If the attacker borrows 1,000,000 USDC and swaps it in, the pool ratio changes dramatically. The attacker receives fewer tokens as they move down the curve, but the final spot price in the pool is now much higher.&lt;/p&gt;

&lt;p&gt;Any oracle that reads the price directly from this pool's reserves at the end of the block will record this inflated price. A lending protocol checking collateral value will compute higher valuations. The attacker has now inflated the value of their collateral, borrowed against it, and can repay the flash loan with the profits from that additional borrowing. All of this happens atomically within a single transaction, leaving no time for market corrections or external intervention.&lt;/p&gt;

&lt;p&gt;Low-liquidity pools are particularly vulnerable because the price impact of a large swap is proportional to the trade size relative to reserves. A 1,000,000 USDC swap in a 2,000,000 USDC pool causes a massive price movement. The same swap in a billion-dollar pool would barely move the price at all. Attackers deliberately target small liquidity pools for this reason: they offer higher price impact per unit of capital deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why External Dependencies Create Systemic Risk
&lt;/h2&gt;

&lt;p&gt;Many protocols mitigate price oracle risk by importing prices from well-established sources like Chainlink. Chainlink operates a decentralized network of node operators who fetch prices from multiple exchanges, aggregate them, and report consensus prices to the blockchain. This approach provides strong guarantees against single-exchange price manipulation because one exchange's data is filtered out when computing the consensus.&lt;/p&gt;

&lt;p&gt;However, external dependency creates a different category of risk. When multiple protocols in an ecosystem depend on the same oracle network, they share a common point of failure. If a Chainlink price feed is misconfigured, updates with latency, or reports prices from a moment when liquidity was insufficient, all dependent protocols suffer simultaneously. This is not a systemic failure of the oracle itself but a dependency cascade.&lt;/p&gt;

&lt;p&gt;A real example: during the March 2020 market crash, some Chainlink feeds experienced significant delays reporting lower prices as demand for data spiked and blockchain networks congested. Protocols that had not implemented their own fallback mechanisms experienced liquidations at prices that no longer reflected market conditions. The oracle worked correctly in isolation, but when stress-tested at scale, the dependency became a vulnerability.&lt;/p&gt;

&lt;p&gt;Beyond latency, there is also the question of what constitutes a Chainlink price feed's "correct" value. Chainlink itself aggregates from multiple exchanges. If most major exchanges see a price of $50,000 per BTC but a low-liquidity fork or regional exchange shows $51,000, Chainlink reports closer to $50,000. But if an attacker can manipulate multiple exchanges simultaneously, they can shift the consensus price. This is expensive and requires coordination, but it is not impossible, particularly during periods of network congestion or when the cost of manipulation is small relative to the potential gain.&lt;/p&gt;

&lt;p&gt;The risk is not that external oracles are bad: it is that they introduce centralized trust points masquerading as decentralized solutions. Any protocol that depends on a single oracle feed without fallback logic, circuit breakers, or internal validity checks is accepting that dependency as a single point of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-Weighted Average Price Models as Defense
&lt;/h2&gt;

&lt;p&gt;Time-weighted average price (TWAP) models provide a critical defense against flash loan attacks and spot price manipulation. Instead of reading the current spot price from a DEX pool at the moment a transaction executes, a TWAP mechanism samples the pool's price at multiple points in time and computes the average. An attacker manipulating the spot price at one point in time does not change the historical average price recorded in past blocks.&lt;/p&gt;

&lt;p&gt;Uniswap V2 implements TWAP natively. Every block, it records a cumulative price variable that tracks the sum of all spot prices multiplied by the time elapsed since the last update. A contract can read this cumulative price at two different points in time and compute the mean price over the interval. If you read the cumulative price at block 1000 and block 1100, you can compute the average price across those 100 blocks.&lt;/p&gt;

&lt;p&gt;The attack now requires the attacker to maintain a price manipulation across multiple blocks to move the TWAP average upward. In a flash loan attack, the attacker executes everything in a single transaction, so they can only affect the spot price in one block. The TWAP over the subsequent blocks will average out the manipulated price, pulling the mean back toward the true market price. If the attacker tries to manipulate the price in multiple consecutive blocks, they must either find a way to execute in multiple transactions (which gives other market participants time to arbitrage the false price) or deploy capital across multiple transactions (making the attack expensive).&lt;/p&gt;

&lt;p&gt;This is why secure oracle designs use TWAP models with appropriate averaging windows. Uniswap V2 positions that read a 1-minute TWAP are much safer than positions that read the spot price directly. A 1-minute averaging window means the attacker would need to maintain price manipulation for 60 blocks to significantly move the average. On Ethereum, this would cost significant gas across multiple transactions and expose the manipulation to arbitrageurs.&lt;/p&gt;

&lt;p&gt;Uniswap V3 implemented an even more sophisticated TWAP mechanism. Liquidity is concentrated in specific price ranges, so the spot price can move more dramatically with smaller swaps. However, V3 still records cumulative price observations and allows contracts to compute TWAP across arbitrary time windows. The security guarantee is similar: the attacker must maintain manipulation across time for the attack to work, which becomes economically infeasible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Robust Oracle Logic in Smart Contracts
&lt;/h2&gt;

&lt;p&gt;Secure oracle consumption in smart contracts requires multiple layers of validation. The first layer is to never trust a single price point. When a contract reads a price from an oracle, it should always treat that price as a snapshot that could be stale, manipulated, or incorrect. This means implementing staleness checks that verify prices have been updated recently.&lt;/p&gt;

&lt;p&gt;A lending protocol should not liquidate a position based on a price update older than some threshold, perhaps five minutes or even one hour depending on the asset and market conditions. Staleness checks are a simple sanity test: if the protocol has not received a price update within a reasonable time window, it should pause the price-dependent operations until new data arrives.&lt;/p&gt;

&lt;p&gt;The second layer is to use multiple oracle sources and compare them. If a protocol depends on both Chainlink and a Uniswap V3 TWAP price feed, it can check whether both oracles agree. If the Chainlink price shows BTC at $50,000 and the TWAP shows $52,000, this discrepancy signals that one oracle is unreliable. The contract can trigger a circuit breaker, pause withdrawals, or require additional governance oversight.&lt;/p&gt;

&lt;p&gt;Here is a simplified example of a dual-oracle validation pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

interface IOracle {
    function getPrice(address token) external view returns (uint256);
}

contract DualOracleValidator {
    IOracle public chainlinkOracle;
    IOracle public twapOracle;
    uint256 public maxPriceDivergence = 200; // 2% divergence tolerance

    function getValidatedPrice(address token) external view returns (uint256) {
        uint256 chainlinkPrice = chainlinkOracle.getPrice(token);
        uint256 twapPrice = twapOracle.getPrice(token);

        require(chainlinkPrice &amp;gt; 0 &amp;amp;&amp;amp; twapPrice &amp;gt; 0, "Invalid price");

        uint256 priceDifference = chainlinkPrice &amp;gt; twapPrice
            ? chainlinkPrice - twapPrice
            : twapPrice - chainlinkPrice;

        uint256 avgPrice = (chainlinkPrice + twapPrice) / 2;
        uint256 divergencePercent = (priceDifference * 10000) / avgPrice;

        require(divergencePercent &amp;lt;= maxPriceDivergence, "Oracle disagreement");

        return avgPrice;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This contract reads both prices and checks that they agree within a 2% tolerance. If the oracles diverge more than this threshold, the contract reverts, preventing the operation. The tolerance can be tuned based on the asset volatility and the protocol's risk tolerance.&lt;/p&gt;

&lt;p&gt;The third layer is to implement economic limits on oracle-driven operations. A lending protocol should not allow a single liquidation to occur if it would remove more than a certain percentage of the protocol's collateral in a single transaction. If liquidation becomes too profitable suddenly, the circuit breaker should activate, signaling potential price manipulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stress Testing Oracle Dependencies
&lt;/h2&gt;

&lt;p&gt;Professional Web3 documentation must emphasize stress testing oracle systems before deployment to production. Stress testing means simulating scenarios where oracles provide stale data, temporarily unavailable, disagree with each other, or report prices far outside expected ranges.&lt;/p&gt;

&lt;p&gt;An effective oracle stress test includes the following scenarios: first, simulate a situation where one oracle source goes offline. Does the protocol still function, or does it halt? Second, simulate a time delay in price updates. If prices update every 30 seconds but the protocol expects updates every 10 seconds, what happens when an operation depends on prices older than 10 seconds? Third, simulate a large price movement in a short time window. If BTC moves from $50,000 to $55,000 in a single block, can the oracle handle this jump, or does it report a price so stale it causes further problems?&lt;/p&gt;

&lt;p&gt;Fourth, simulate a flash loan attack against a pool that underpins your price oracle. If the attacker can move the TWAP by 10% through a coordinated swap, what liquidations would trigger and what would be the protocol's loss? Fifth, simulate scenarios where one oracle source disagrees significantly with another. What is the protocol's fallback behavior when consensus is lost?&lt;/p&gt;

&lt;p&gt;These stress tests should be automated in a test suite and run regularly during development and before major upgrades. They should also be part of security audits. An experienced auditor will not only check that your oracle implementation is sound but will also simulate price movements and failure modes to ensure the protocol degrades gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguishing Between Oracle Manipulation and Market Volatility
&lt;/h2&gt;

&lt;p&gt;A critical distinction in oracle design is understanding the difference between legitimate market volatility and oracle manipulation. A real market event can cause the spot price to move 10% in minutes. An oracle manipulation attack also causes the spot price to move, but only on a single exchange or low-liquidity pool while other markets remain stable. The attacker's trade moves the price on the specific pool but does not affect prices on other exchanges where the true market price is still trading higher or lower.&lt;/p&gt;

&lt;p&gt;This distinction is why monitoring the spread between multiple price sources is essential. If Uniswap V2, Uniswap V3, Balancer, Curve, Coinbase, Kraken, and Binance all show different prices for the same token, this is not necessarily suspicious; it indicates liquidity fragmentation and normal market microstructure. But if Uniswap V2 shows a price 20% different from every other major source, and that difference appeared in the last block, this suggests manipulation on the Uniswap V2 pool specifically.&lt;/p&gt;

&lt;p&gt;Separating signal from noise in price data requires understanding the market structure. Token prices on DEXs often lag centralized exchanges by seconds to minutes because arbitrage takes time to execute. Prices on decentralized exchanges reflect the cost of execution plus the friction of swap fees. These are normal deviations, not manipulation.&lt;/p&gt;

&lt;p&gt;A TWAP model handles this distinction naturally. If a price spike is real, it will be reflected in all subsequent blocks and the TWAP will adjust upward. If a spike is an isolated manipulation attempt, the TWAP will ignore it because the price returns to normal in the next block. This is why TWAP models are so effective: they filter out momentary noise while still responding to sustained price movements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Deployment Considerations
&lt;/h2&gt;

&lt;p&gt;When deploying an oracle-dependent protocol to production, several practical considerations apply beyond the code itself. First, ensure that price feed configurations match your actual dependencies. If your contract is hardcoded to read from Chainlink, but the actual price feed address you use points to a stale or deprecated feed, the configuration is broken. Audit the addresses and feed IDs carefully.&lt;/p&gt;

&lt;p&gt;Second, plan for oracle updates to fail. A production system should have graceful degradation when an oracle is temporarily unavailable. This might mean pausing certain operations rather than reverting the entire system, or falling back to an alternative price source. The exact behavior depends on the protocol's risk model and how much temporary stasis it can tolerate.&lt;/p&gt;

&lt;p&gt;Third, implement monitoring that tracks oracle prices in real time and alerts operators when prices move beyond expected ranges or when data becomes stale. This detection layer runs off-chain and informs the team when something unusual is happening so human judgment can be applied if an automated circuit breaker is too conservative.&lt;/p&gt;

&lt;p&gt;Fourth, document the oracle model explicitly in your technical specification. Write down exactly which oracles you depend on, what time windows you use for TWAP calculations, what divergence thresholds trigger circuit breakers, and what happens when an oracle fails. This documentation becomes the reference for auditors, users, and your own team when troubleshooting issues.&lt;/p&gt;

&lt;p&gt;Fifth, plan for oracle feed updates and governance over time. Oracles are not immutable once deployed. Chainlink adds new price feeds, adjusts security parameters, and modifies how prices are computed. Your protocol must have a process for updating oracle integrations without disrupting live positions. This usually means a time lock and governance voting before an oracle change goes live.&lt;/p&gt;

&lt;p&gt;Professional Web3 documentation must emphasize that oracle design is not a one-time implementation decision but an ongoing operational responsibility. The oracle model you ship on mainnet will need maintenance, monitoring, and updates over the protocol's lifetime.&lt;/p&gt;

&lt;p&gt;For professional Web3 documentation or full-stack Next.js development assistance, please review the author's profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>Proxy Contracts and Upgradeability Risks: Storage Collision Analysis and Testing Strategies</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Thu, 30 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/proxy-contracts-and-upgradeability-risks-storage-collision-analysis-and-testing-strategies-3ndo</link>
      <guid>https://forem.com/mericcintosun/proxy-contracts-and-upgradeability-risks-storage-collision-analysis-and-testing-strategies-3ndo</guid>
      <description>&lt;p&gt;Immutable code has historically been a feature of blockchain development, not a bug. Once deployed to mainnet, a smart contract cannot be modified. This immutability guarantees that users interact with exactly what they audited, but it also creates a harsh reality: mistakes are permanent, and new functionality cannot be added. The proxy pattern emerged to solve this problem by decoupling logic from state, allowing developers to upgrade contract behavior without losing historical data or breaking user integrations.&lt;/p&gt;

&lt;p&gt;However, proxy patterns introduce their own class of vulnerabilities. Storage layout collisions between proxy contracts and their implementations, version drift across upgrade paths, and subtle state corruption bugs can emerge only after multiple upgrades in production. These risks are not theoretical—they have caused millions in losses across deployed protocols. The difference between a successful upgrade and a catastrophic failure often comes down to whether storage layout was analyzed correctly before deployment and whether the upgrade path was tested systematically in CI/CD.&lt;/p&gt;

&lt;p&gt;This article examines the mechanics of proxy-based upgradeability, identifies where storage collisions occur, and demonstrates how to test upgrade paths before they reach mainnet. We focus on transparent proxy patterns and UUPS (Universal Upgradeable Proxy Standard) implementations, the two most widely deployed approaches in production systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Proxy Architecture and Storage Layout
&lt;/h2&gt;

&lt;p&gt;The proxy pattern works by separating two contracts: a proxy that receives all calls and delegates them to an implementation, and an implementation contract that contains the actual business logic. The proxy maintains its own storage, while the implementation contract provides the code that modifies it. When an upgrade occurs, the proxy's implementation pointer is updated to reference a new implementation contract, but the proxy's storage persists unchanged.&lt;/p&gt;

&lt;p&gt;This separation creates the core design challenge: both the proxy and the implementation need storage, and they must use the same storage layout. The proxy typically stores its admin address and the address of the current implementation contract. The implementation contract stores application state. When the implementation changes, the new implementation must be able to read and write to the proxy's storage exactly as the old implementation did. Any mismatch in slot allocation causes data corruption.&lt;/p&gt;

&lt;p&gt;Consider a simple example. A proxy contract might use storage slot 0 for its admin address. An implementation contract might declare its first state variable at slot 0, expecting that slot to be unoccupied. When the implementation tries to write to its state variable, it overwrites the admin address. The proxy becomes orphaned—no one can call its upgrade function anymore because the admin slot has been corrupted.&lt;/p&gt;

&lt;p&gt;The Solidity compiler allocates storage slots sequentially starting from slot 0. A state variable declared first occupies slot 0, the next occupies slot 1, and so on. Structs and arrays follow specific packing rules. The vulnerability emerges because developers often do not account for the storage that the proxy itself consumes. They write implementations without knowledge of how much storage the proxy needs, or worse, they inherit from OpenZeppelin's Ownable or Upgradeable base classes in both the proxy and the implementation, effectively double-counting storage variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transparent Proxies vs. UUPS: Storage Implications
&lt;/h2&gt;

&lt;p&gt;Transparent proxies use two distinct storage layouts. The proxy contract stores admin and implementation addresses. The implementation contract stores application state. A key mechanism in transparent proxies is the fallback function: if the caller is the admin, the proxy intercepts the call and handles it directly (for upgrade operations). Otherwise, the proxy delegates the call to the implementation. This prevents admin functions from being shadowed by implementation functions with the same signature.&lt;/p&gt;

&lt;p&gt;However, transparent proxies impose storage overhead in the implementation. If the implementation contract is also Ownable, developers sometimes inherit Ownable twice: once from a base contract and once indirectly through the proxy. This creates storage duplication. The implementation's first few slots are now occupied by proxy-related storage that the implementation never actually uses, but the Solidity compiler allocates them anyway.&lt;/p&gt;

&lt;p&gt;UUPS proxies flip the responsibility. The proxy itself is minimal and contains no admin logic. The implementation contract imports a UUPS mixin that provides the upgrade mechanism. The implementation calls a delegatecall to itself to execute the upgrade, avoiding the double-inheritance trap. The trade-off is that the implementation now carries the upgrade responsibility. If a new implementation fails to properly inherit the UUPS mixin, or if a developer accidentally deploys an implementation that does not support upgrades, the contract becomes stuck at that version forever—no further upgrades are possible.&lt;/p&gt;

&lt;p&gt;Storage layout differs between these patterns because UUPS implementations must reserve a specific storage slot for the implementation pointer. The UUPS standard reserves slot &lt;code&gt;uint256(keccak256('eip1967.proxy.implementation')) - 1&lt;/code&gt; to store the implementation address. Any application state that uses this slot, either by accident or through careless variable declaration, will collide with the upgrade mechanism itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing Storage Collisions
&lt;/h2&gt;

&lt;p&gt;Storage collisions manifest in several ways. The most obvious is state corruption after an upgrade: the proxy's admin becomes unwritable, or application state variables read stale or incorrect values. The corruption may not be immediate. If the new implementation writes to storage locations that the old implementation never touched, the problem remains dormant until those locations are read.&lt;/p&gt;

&lt;p&gt;To diagnose storage layout, developers must understand exactly which slots each contract uses. The Solidity compiler does not expose this information directly in the bytecode. Instead, developers rely on analysis tools and manual inspection of contract source code.&lt;/p&gt;

&lt;p&gt;OpenZeppelin provides a storage layout report tool that generates a JSON schema documenting every state variable and its corresponding storage slot. Running this tool on both the proxy and the implementation reveals whether their layouts align. The tool integrates into Hardhat and Foundry, the two most common development frameworks.&lt;/p&gt;

&lt;p&gt;Here is how we use the tool with Hardhat. First, install the necessary package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @openzeppelin/hardhat-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a Hardhat script, import the storage layout reporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hardhat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getStorageLayout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@openzeppelin/hardhat-upgrades/dist/utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProxyContract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyProxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStorageLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProxyContract&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;implLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStorageLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proxy Layout:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxyLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Implementation Layout:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows each variable's name, type, size in bytes, and slot number. Comparing the two outputs reveals collisions immediately. If the proxy uses slots 0 and 1 for admin and implementation pointers, but the implementation declares its first state variable at slot 0, the collision is obvious.&lt;/p&gt;

&lt;p&gt;Manual inspection also works for smaller contracts. Open the contract source file and count storage manually. Remember that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uint256 variables occupy one slot each.&lt;/li&gt;
&lt;li&gt;Smaller unsigned integers (uint8, uint16, etc.) can be packed together in one slot if they fit.&lt;/li&gt;
&lt;li&gt;Booleans occupy one slot.&lt;/li&gt;
&lt;li&gt;Address variables occupy one slot (20 bytes) plus padding to reach 32 bytes.&lt;/li&gt;
&lt;li&gt;Mappings and dynamic arrays occupy one slot as a storage key and data location.&lt;/li&gt;
&lt;li&gt;Fixed-size arrays of size N occupy N slots if the array element is uint256-sized, or fewer if smaller types pack.&lt;/li&gt;
&lt;li&gt;Structs occupy consecutive slots, with packing applied to struct members.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical example: a proxy with variables &lt;code&gt;address admin&lt;/code&gt; and &lt;code&gt;address implementation&lt;/code&gt; occupies two full slots (two addresses do not pack together because each address is 20 bytes and requires 12 bytes of padding). An implementation contract that declares &lt;code&gt;uint256 value&lt;/code&gt; as its first state variable will place that value at slot 2, not slot 0, because slots 0 and 1 are already allocated by the proxy's variables. This layout is correct.&lt;/p&gt;

&lt;p&gt;However, if the implementation declares &lt;code&gt;address myAddress&lt;/code&gt; as its first state variable, the Solidity compiler allocates it to slot 0, colliding with the proxy's admin address. This is incorrect and will cause corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Namespacing Patterns
&lt;/h2&gt;

&lt;p&gt;To avoid collisions systematically, developers adopt storage namespacing patterns. The most reliable approach reserves a range of slots for the proxy and another range for the implementation, documenting the boundary clearly.&lt;/p&gt;

&lt;p&gt;The EIP-1967 standard defines storage slot names for common proxy variables. For a transparent proxy, the admin is stored at &lt;code&gt;keccak256("eip1967.proxy.admin") - 1&lt;/code&gt; and the implementation is stored at &lt;code&gt;keccak256("eip1967.proxy.implementation") - 1&lt;/code&gt;. These hashes are so large (well into the 2^256 range) that accidental collisions with application state declared at low slot numbers are virtually impossible.&lt;/p&gt;

&lt;p&gt;Using EIP-1967 slots looks like this in an implementation contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

contract MyImplementation {
    // Reserve the low slots for application state
    uint256 public counter;
    address public owner;

    // The proxy stores its admin and implementation at EIP-1967 slots (very high)
    // No collision risk

    function increment() public {
        counter++;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy, in turn, stores its admin and implementation at the EIP-1967 slots, not at slot 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

contract MyProxy {
    // Not stored at slot 0 or 1, but at EIP-1967 slots
    // This is handled by assembly or a library function

    function _setImplementation(address newImpl) internal {
        bytes32 slot = keccak256("eip1967.proxy.implementation") - 1;
        assembly {
            sstore(slot, newImpl)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using EIP-1967 slots, the proxy and implementation no longer compete for the low slot numbers. The implementation can declare state variables starting from slot 0 without fear. This pattern has become the standard in production deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Upgrade Paths Before Mainnet
&lt;/h2&gt;

&lt;p&gt;An upgrade path consists of multiple versions. Version 1 is deployed first. Version 2 adds new features and modifies existing state. Version 3 builds on V2, and so on. Testing must verify that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Storage layout is correct for each version transition.&lt;/li&gt;
&lt;li&gt;State is preserved through the upgrade (no corruption or loss).&lt;/li&gt;
&lt;li&gt;New functionality works correctly after the upgrade.&lt;/li&gt;
&lt;li&gt;Old functionality still works after the upgrade.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Comprehensive upgrade path testing requires simulating the entire sequence of upgrades in a test environment before any version reaches mainnet. This means deploying V1, upgrading to V2, verifying state, upgrading to V3, verifying state again, and so on.&lt;/p&gt;

&lt;p&gt;Here is a Hardhat-based test suite that covers an upgrade path with three versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hardhat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upgrade Path: V1 → V2 → V3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;implV1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;implV3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &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="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSigners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Deploy V1 implementation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;implV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Deploy proxy pointing to V1&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;Proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyProxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should initialize V1 state correctly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCounter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should upgrade to V2 without state loss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Deploy V2 implementation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Upgrade proxy to V2&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify V1 state is preserved&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCounter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should upgrade from V2 to V3 without state loss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Upgrade to V2&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add V2-specific state&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Upgrade to V3&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;implV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify both V1 and V2 state are preserved&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCounter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should prevent upgrades from non-admin accounts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;attacker&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSigners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsAttacker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attacker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxyAsAttacker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revertedWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Caller is not the admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test suite validates the upgrade path from V1 to V2 to V3. Notice that we perform initialization in V1, upgrade to V2, verify state preservation, add new state in V2, upgrade to V3, and verify that both old and new state persist. This approach catches storage collisions and state corruption bugs.&lt;/p&gt;

&lt;p&gt;Foundry provides similar testing capabilities using Solidity-based tests. Here is the same suite in Foundry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../contracts/MyImplementationV1.sol";
import "../contracts/MyImplementationV2.sol";
import "../contracts/MyImplementationV3.sol";
import "../contracts/MyProxy.sol";

contract UpgradePathTest is Test {
    MyProxy proxy;
    MyImplementationV1 implV1;
    MyImplementationV2 implV2;
    MyImplementationV3 implV3;
    address owner = address(0x1234);

    function setUp() public {
        implV1 = new MyImplementationV1();
        proxy = new MyProxy(address(implV1), owner);
    }

    function testV1StateInitialization() public {
        vm.prank(owner);
        MyImplementationV1(address(proxy)).initialize(100);

        uint256 counter = MyImplementationV1(address(proxy)).getCounter();
        assertEq(counter, 100);
    }

    function testUpgradeToV2PreservesState() public {
        vm.prank(owner);
        MyImplementationV1(address(proxy)).initialize(100);

        implV2 = new MyImplementationV2();

        vm.prank(owner);
        proxy.upgradeTo(address(implV2));

        uint256 counter = MyImplementationV2(address(proxy)).getCounter();
        assertEq(counter, 100);
    }

    function testUpgradeV2ToV3PreservesAllState() public {
        vm.prank(owner);
        MyImplementationV1(address(proxy)).initialize(100);

        implV2 = new MyImplementationV2();
        vm.prank(owner);
        proxy.upgradeTo(address(implV2));

        vm.prank(owner);
        MyImplementationV2(address(proxy)).setName("Test");

        implV3 = new MyImplementationV3();
        vm.prank(owner);
        proxy.upgradeTo(address(implV3));

        uint256 counter = MyImplementationV3(address(proxy)).getCounter();
        string memory name = MyImplementationV3(address(proxy)).getName();

        assertEq(counter, 100);
        assertEq(name, "Test");
    }

    function testNonAdminCannotUpgrade() public {
        implV2 = new MyImplementationV2();

        address attacker = address(0x5678);
        vm.prank(attacker);
        vm.expectRevert("Caller is not the admin");
        proxy.upgradeTo(address(implV2));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both test suites validate the upgrade path, but Foundry's tests execute within the EVM directly, providing more accurate gas measurements and state simulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Storage Layout Validation in CI/CD
&lt;/h2&gt;

&lt;p&gt;Manual testing catches many issues, but automated storage validation in CI/CD catches collisions before human review. Several tools can be integrated into the CI/CD pipeline to check storage layout automatically.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;code&gt;@openzeppelin/hardhat-upgrades&lt;/code&gt; package includes a validation function that compares storage layouts between versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validateUpgrade&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@openzeppelin/hardhat-upgrades&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplV1Factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplV2Factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateUpgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ImplV1Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ImplV2Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;unsafeAllow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// Allow constructors in upgrade scenarios if needed&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Storage layout validation passed!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Storage layout validation failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script compares the storage layouts of V1 and V2, checking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added state variables (allowed).&lt;/li&gt;
&lt;li&gt;Removed state variables (forbidden—causes state shift for remaining variables).&lt;/li&gt;
&lt;li&gt;Reordered state variables (forbidden—causes existing state to be read as wrong types).&lt;/li&gt;
&lt;li&gt;Type changes in existing variables (forbidden in most cases).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integrating this into GitHub Actions or GitLab CI ensures every pull request that modifies a contract is checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Storage Layout Validation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;contracts/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;test/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx hardhat run scripts/validateStorageLayout.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this workflow runs and storage validation fails, the pull request is blocked from merging until the issue is resolved. This prevents storage collision bugs from reaching production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Storage Mistakes and How to Avoid Them
&lt;/h2&gt;

&lt;p&gt;Several patterns repeatedly cause storage issues in production deployments. Understanding these mistakes helps developers avoid them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Double-inheriting Ownable.&lt;/strong&gt; Many implementations inherit Ownable both directly and indirectly through a base contract. Solidity linearizes inheritance using C3 linearization, but if not carefully managed, a contract can declare the Ownable state variables twice. The second declaration silently overrides the first in the linearization order, causing unexpected storage allocation. To avoid this, inherit Ownable only once, at the top level of the inheritance hierarchy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Forgetting proxy storage overhead.&lt;/strong&gt; Developers often write implementations assuming storage starts at slot 0. If the proxy uses the low slots, the implementation's state ends up at the wrong offsets. The solution is to use EIP-1967 slots for proxy storage, ensuring the proxy and implementation never collide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Adding variables to base contracts after deployment.&lt;/strong&gt; If a base contract that implementations inherit from is modified to add new state variables, all implementations that use that base will have shifted storage. This is difficult to debug because the change is not in the implementation itself. A best practice is to freeze base contracts after the first production deployment. If new functionality is needed, create a new version of the base contract and migrate implementations to inherit from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Changing array or mapping types.&lt;/strong&gt; A state variable declared as &lt;code&gt;uint256[]&lt;/code&gt; occupies a storage slot as a key. If you later change it to &lt;code&gt;mapping(uint256 =&amp;gt; uint256)&lt;/code&gt;, both constructs use the same storage location, but interpret the data differently. This causes silent corruption. Never change the type of existing state variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: Upgrading without testing the full path.&lt;/strong&gt; Developers sometimes test V1 → V2 but assume V2 → V3 will work automatically. Each upgrade introduces new opportunities for state shift. Test every consecutive upgrade before mainnet deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: Upgrading a Token Contract
&lt;/h2&gt;

&lt;p&gt;Consider a token contract that has been deployed for several months. It implements the ERC20 standard and has billions of tokens in circulation. The governance decides to add a fee mechanism: when tokens are transferred, a small percentage goes to a treasury address. A new implementation contract is developed, tested, and deemed ready for upgrade.&lt;/p&gt;

&lt;p&gt;The original V1 implementation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TokenV1 is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** 18);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new V2 implementation adds a treasury and fee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract TokenV2 is ERC20, Ownable {
    address public treasury;
    uint256 public feePercentage;

    function initialize(address _treasury, uint256 _feePercentage) public {
        treasury = _treasury;
        feePercentage = _feePercentage;
    }

    function transfer(address to, uint256 amount) public override returns (bool) {
        uint256 fee = (amount * feePercentage) / 100;
        uint256 amountAfterFee = amount - fee;

        _transfer(msg.sender, to, amountAfterFee);
        _transfer(msg.sender, treasury, fee);

        return true;
    }

    function setFeePercentage(uint256 _feePercentage) public onlyOwner {
        feePercentage = _feePercentage;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the TokenV2 implementation is deployed directly (not behind a proxy), it will have a different storage layout than TokenV1. The ERC20 base contract's internal variables (balances, allowances, totalSupply) occupy the low slots. TokenV2 adds &lt;code&gt;treasury&lt;/code&gt; and &lt;code&gt;feePercentage&lt;/code&gt;, which will be placed at the next available slots. Upgrading from TokenV1 to TokenV2 will preserve ERC20 state but initialize the new treasury and feePercentage to zero (or to whatever values are already in those storage slots from previous contract deployments or noise).&lt;/p&gt;

&lt;p&gt;However, if TokenV2 also inherits from Ownable, and Ownable is not carefully placed in the inheritance hierarchy, the Solidity compiler might place Ownable's owner variable at a storage location that overlaps with one of ERC20's internal variables. This causes the new implementation to read and write to the wrong storage slots.&lt;/p&gt;

&lt;p&gt;To handle this safely, the upgrade process should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy TokenV2 to a testnet and verify storage layout.&lt;/li&gt;
&lt;li&gt;Run storage layout validation to confirm no collisions with TokenV1.&lt;/li&gt;
&lt;li&gt;Initialize a test instance of the proxy with TokenV1 implementation.&lt;/li&gt;
&lt;li&gt;Call ERC20 transfer functions to create state.&lt;/li&gt;
&lt;li&gt;Upgrade to TokenV2.&lt;/li&gt;
&lt;li&gt;Call transfer again and verify balances are correct.&lt;/li&gt;
&lt;li&gt;Call setFeePercentage and verify the fee is applied correctly.&lt;/li&gt;
&lt;li&gt;Only after all tests pass, deploy TokenV2 to mainnet and execute the upgrade.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Storage Layout Tools and Best Practices
&lt;/h2&gt;

&lt;p&gt;Several tools help developers manage storage layout correctly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenZeppelin Hardhat Upgrades&lt;/strong&gt; provides storage inspection and validation. It integrates directly into the Hardhat testing framework and reports detailed storage layouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foundry's &lt;code&gt;forge storage&lt;/code&gt; command&lt;/strong&gt; displays the storage layout of a compiled contract. Running &lt;code&gt;forge storage &amp;lt;contract_name&amp;gt;&lt;/code&gt; shows every state variable and its storage slot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slither&lt;/strong&gt;, a static analysis tool from Trail of Bits, can detect some storage-related issues during code review, though it is not specialized for upgrade paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual inspection&lt;/strong&gt; remains the most reliable method for critical contracts. Taking time to manually count storage slots and document the layout in code comments prevents surprises. Consider adding a storage layout diagram as a comment in the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

/**
 * MyImplementationV2
 * Storage Layout:
 * Slot 0: uint256 counter (from V1)
 * Slot 1: address owner (from V1)
 * Slot 2: address treasury (new in V2)
 * Slot 3: uint256 feePercentage (new in V2)
 */

contract MyImplementationV2 {
    uint256 public counter;
    address public owner;
    address public treasury;
    uint256 public feePercentage;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This explicit documentation ensures future developers (or your future self) understand the storage layout and respect it during the next upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing for Mainnet: A Checklist
&lt;/h2&gt;

&lt;p&gt;Before deploying any upgrade to mainnet, verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Storage layout has been validated using automated tools.&lt;/li&gt;
&lt;li&gt;The upgrade path has been tested from the current production version through all planned upgrade steps.&lt;/li&gt;
&lt;li&gt;All tests pass in a local fork of mainnet (using Hardhat's &lt;code&gt;--fork&lt;/code&gt; flag or Foundry's &lt;code&gt;--rpc-url&lt;/code&gt; flag).&lt;/li&gt;
&lt;li&gt;No state variables have been removed from previous versions.&lt;/li&gt;
&lt;li&gt;No state variables have been reordered.&lt;/li&gt;
&lt;li&gt;No state variable types have changed.&lt;/li&gt;
&lt;li&gt;The new implementation has been audited if it introduces significant logic changes.&lt;/li&gt;
&lt;li&gt;An upgrade path rollback plan is documented (e.g., how to upgrade to a previous version if the new version is discovered to be broken).&lt;/li&gt;
&lt;li&gt;The admin key is secure and not held by a single person (consider using a multisig or time-lock contract).&lt;/li&gt;
&lt;li&gt;All team members understand the storage layout and upgrade mechanism.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Following this checklist dramatically reduces the risk of storage-related failures in production.&lt;/p&gt;




&lt;p&gt;If you are building Web3 systems that require production-grade documentation or developing a full-stack Next.js application, I am available for both Web3 technical writing and end-to-end web development. Visit my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Strengthening Protocol Architecture Against Flash Loan Attacks</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Tue, 28 Apr 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/strengthening-protocol-architecture-against-flash-loan-attacks-238d</link>
      <guid>https://forem.com/mericcintosun/strengthening-protocol-architecture-against-flash-loan-attacks-238d</guid>
      <description>&lt;p&gt;Flash loan attacks represent one of the most distinctive vulnerabilities in DeFi, exploiting the atomicity of blockchain transactions to borrow and manipulate assets within a single block without putting up collateral. Unlike traditional lending attacks that require sustained capital commitment, flash loans allow attackers to amplify price movements or manipulate oracle feeds by accessing enormous sums of liquidity for exactly 140 milliseconds (the average Ethereum block time). Understanding these attacks requires examining the transaction-level mechanics that enable them, the economic incentives that motivate attackers, and the architectural patterns that protect protocols from exploitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Flash Loans Create Attack Surface
&lt;/h2&gt;

&lt;p&gt;Flash loans function as a liquidity primitive. Aave, the protocol that pioneered this feature, made it publicly accessible in 2020. The mechanics are straightforward: a user calls the flash loan function on a lending pool, receives an arbitrary amount of tokens in the same transaction, executes any logic they choose, and must repay the borrowed amount plus a fee (typically 0.05% on Aave) before the transaction completes. If repayment fails, the entire transaction reverts.&lt;/p&gt;

&lt;p&gt;The critical insight is that atomicity creates the vulnerability. Within a single transaction, an attacker borrows a massive amount of an asset, uses it to manipulate a price oracle or drain a liquidity pool, and then repays the loan. Since the transaction either succeeds completely or fails completely, the attacker has zero downside risk if the exploit fails. They only pay the flash loan fee if they successfully extract value from the protocol.&lt;/p&gt;

&lt;p&gt;Consider a concrete scenario: an attacker executes a flash loan attack on a decentralized exchange (DEX) that prices assets based on spot price from a liquidity pool. The attacker borrows a large amount of Token A, sells it immediately on the DEX, crashing the price. They then use this manipulated price to liquidate a user's position or drain collateral from a lending protocol. Finally, they repay the flash loan and keep the difference. The DEX received the sale, its price was moved, and the protocol that relied on that price was exploited, all in a single block.&lt;/p&gt;

&lt;p&gt;The second factor that enables flash loan attacks is oracle dependency. Most DeFi protocols use price feeds from DEXs or chainlink oracles to value assets and manage collateral. When a protocol depends on spot price from a DEX, an attacker with access to temporary capital can move that price. Even protocols that use time-weighted average prices (TWAP) can be vulnerable if the attacker controls enough liquidity to move the TWAP across multiple blocks, or if the protocol uses a short time window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Economic Incentives and Attack Motivation
&lt;/h2&gt;

&lt;p&gt;Flash loan attacks succeed because the economics align perfectly for attackers. The cost is minimal: a single transaction fee and the flash loan fee, typically totaling less than $1000 in Ethereum gas. The upside can be millions of dollars. This asymmetry has attracted sophisticated attackers who use flash loans as one tool in a larger MEV extraction strategy.&lt;/p&gt;

&lt;p&gt;The attacks that generate the highest returns are those that extract value from multiple layers at once. The 2020 bZx attack, which generated approximately $1 million in profit, combined a flash loan with manipulation of a price oracle, forced liquidations, and arbitrage across multiple markets. The attacker didn't just manipulate one price; they orchestrated a cascade of failures across several protocols.&lt;/p&gt;

&lt;p&gt;However, not every flash loan is an attack. Some flash loans are used legitimately for arbitrage: borrowing assets, executing a trade at better prices elsewhere, and repaying the loan. Others are used for atomic refinancing or collateral swaps. The challenge for protocol architects is distinguishing between legitimate flash loan usage and exploitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack Vectors Against Common Protocol Architectures
&lt;/h2&gt;

&lt;p&gt;Different types of protocols face different flash loan risks. Lending protocols are vulnerable to oracle manipulation attacks that artificially inflate or deflate collateral values. A protocol where a user has $100 of collateral backing $80 of borrowing normally has a 1.25x collateralization ratio. If an attacker uses a flash loan to crash the collateral price to $50, the user's position becomes under-collateralized and can be liquidated for profit.&lt;/p&gt;

&lt;p&gt;DEXs that use constant product formulas (like Uniswap v2) are inherently vulnerable to spot price manipulation. If a DEX is the primary price feed for a lending protocol, an attacker can drain liquidity or move price with a flash loan, affecting all positions that depend on that price. Uniswap v3, which added concentrated liquidity, introduced additional complexity: an attacker might target a specific price range where liquidity is scarce.&lt;/p&gt;

&lt;p&gt;Protocols that allow uncollateralized borrowing or rely on governance votes are also at risk. If a flash loan can be used to acquire voting power temporarily, an attacker might vote on a malicious proposal that executes in the same block. This attack was used against bZx and other lending protocols where voting power was tied to token holdings.&lt;/p&gt;

&lt;p&gt;The common thread across all flash loan attacks is that they exploit the gap between the atomic world of blockchain transactions and the assumption of lasting state change. Protocols are often built with the assumption that prices move slowly and that attackers cannot accumulate arbitrary capital instantly. Flash loans violate both assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Price Oracle Design
&lt;/h2&gt;

&lt;p&gt;The first layer of defense is robust price oracle design. Instead of relying on spot prices from a single DEX, protocols should use multiple oracle sources and aggregation mechanisms. Chainlink price feeds, which aggregate prices from multiple sources and use economic incentives to penalize manipulation, are significantly more resistant to flash loan attacks than spot prices from a single liquidity pool.&lt;/p&gt;

&lt;p&gt;When implementing oracle logic, the contract must handle the case where the oracle goes stale or reports an unreasonable price. Many protocols implement a price deviation check: if the current price differs from the previous price by more than a threshold (say, 5%), the transaction reverts. This prevents an attacker from moving the price too far in a single transaction or block.&lt;/p&gt;

&lt;p&gt;Time-weighted average prices (TWAP) add another layer of defense. Instead of using the current price, a protocol can use the average price over the past N blocks. An attacker with a flash loan can move the price in one block, but cannot affect the average over 100 blocks unless they control multiple consecutive blocks. Uniswap v2 made TWAP data available on-chain for exactly this reason.&lt;/p&gt;

&lt;p&gt;However, TWAP is not unbreakable. An attacker who can control several consecutive blocks (through MEV or 51% attack) can still manipulate TWAP prices. Additionally, a protocol that uses a short TWAP window (say, 3 blocks instead of 30) remains vulnerable.&lt;/p&gt;

&lt;p&gt;The most robust approach combines multiple mechanisms: use Chainlink or similar decentralized oracle networks as the primary price feed, implement TWAP as a secondary feed, and add price deviation checks. When all three sources conflict, the protocol should fail conservatively, reverting the transaction rather than proceeding with potentially incorrect prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Transaction Ordering and Rate Limiting
&lt;/h2&gt;

&lt;p&gt;The second layer of defense addresses the mechanics of atomic execution. Some protocols implement guards that prevent a single transaction from moving prices by more than a certain percentage. Uniswap v2 introduced slippage tolerance, which requires that the output of a swap meets a minimum threshold; if the price has moved more than the user specified, the swap reverts.&lt;/p&gt;

&lt;p&gt;However, slippage protection is a user-facing feature, not a protocol-level defense. A protocol that relies entirely on users to set slippage correctly will fail if users are careless or if an attacker controls the user interface.&lt;/p&gt;

&lt;p&gt;Protocol-level rate limiting is more effective. A protocol can track the maximum price movement within a single block or short time window, and reject transactions that exceed it. For example, Uniswap v3 implements a mechanism where concentrated liquidity positions are only created at specific price ticks; large price movements require moving across multiple ticks, which becomes more expensive.&lt;/p&gt;

&lt;p&gt;Some protocols implement timelocks: a state-changing action is not immediately executed, but queued for later execution. For example, a liquidation that affects a user's collateral might require waiting one block before execution. This prevents an attacker from liquidating a position and extracting collateral in a single atomic transaction.&lt;/p&gt;

&lt;p&gt;Timelocks are not a perfect defense against flash loans—an attacker can still move prices within a single block—but they prevent the most rapid attack patterns and allow monitoring systems to detect and respond to anomalous behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Collateral and Liquidation Design
&lt;/h2&gt;

&lt;p&gt;The third layer involves how protocols manage collateral and liquidation mechanics. Lending protocols should maintain over-collateralization ratios that account for realistic price movements. If a protocol allows positions with a 1.1x collateralization ratio (meaning $110 of collateral for $100 of borrowing), a 10% price drop triggers liquidation. An attacker with a flash loan can force this scenario.&lt;/p&gt;

&lt;p&gt;Protocols that use longer liquidation windows are more resilient. Instead of allowing instant liquidation, a protocol might queue a liquidation and wait one block before executing it. This delay gives the protocol an opportunity to detect and respond to flash loan attacks, and gives the under-collateralized user a chance to add more collateral.&lt;/p&gt;

&lt;p&gt;Some protocols implement dynamic collateral requirements that tighten during periods of high volatility or high flash loan usage. If the protocol detects that its price oracle has moved significantly in recent blocks, it increases collateral requirements for new positions. This increases the capital required to exploit the protocol via flash loans.&lt;/p&gt;

&lt;p&gt;Liquidation mechanisms should also require that the liquidated collateral be exchanged fairly. Some protocols allow liquidators to purchase collateral at a discount (this is the liquidation incentive). If the discount is too large, an attacker can use a flash loan to create an under-collateralized position, liquidate it themselves at a discount, and extract the difference. Protocols that limit the liquidation discount or require the discount to be earned over time reduce this attack vector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Governance and Access Control
&lt;/h2&gt;

&lt;p&gt;Governance tokens and voting power are another flash loan attack surface. If a protocol allows an attacker to acquire voting power with a flash loan, they can vote on a proposal that benefits them, even if they don't actually hold any tokens on a persistent basis.&lt;/p&gt;

&lt;p&gt;The simplest defense is to use block height snapshots: voting power is measured at the beginning of a block, not at the current block height. When a proposal is created, voting power is recorded at the block before voting began. This prevents flash loans from granting voting power, since the flash loan exists for only one transaction and cannot affect the snapshot from the previous block.&lt;/p&gt;

&lt;p&gt;Some protocols implement a voting delay, requiring that users have held their tokens for a minimum period (say, one block) before they gain voting power. This further limits the time window in which a flash loan can influence governance.&lt;/p&gt;

&lt;p&gt;More sophisticated governance systems use multi-sig delays or timelock mechanisms, where governance actions are not immediately executed but queued for execution after a delay period. This allows other governance participants to notice and potentially veto malicious proposals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Patterns in Technical Documentation
&lt;/h2&gt;

&lt;p&gt;Integrating flash loan defense into technical documentation requires clarity about assumptions and constraints. The documentation should explicitly state which price sources the protocol trusts, and under what conditions prices might be manipulated. For a lending protocol, the documentation should specify the assumed worst-case price movement in a single block, and explain how collateralization ratios were chosen to account for this movement.&lt;/p&gt;

&lt;p&gt;When documenting liquidation mechanisms, the documentation should explain the liquidation incentive structure and the rationale behind it. If the protocol allows a 5% liquidation discount, the documentation should state that this discount compensates liquidators for executing the transaction, and explain why the chosen percentage is resilient to flash loan attacks.&lt;/p&gt;

&lt;p&gt;For governance systems, the documentation should explicitly describe the voting power snapshot mechanism, the voting delay, and the timelock. Developers building on top of the protocol need to understand these constraints to safely integrate the protocol's governance.&lt;/p&gt;

&lt;p&gt;When a protocol's security depends on external price feeds like Chainlink, the documentation must describe how Chainlink prices are accessed, how stale prices are handled, and what happens if Chainlink becomes unavailable. A fallback mechanism should exist, and it should degrade gracefully rather than putting the protocol into an unsafe state.&lt;/p&gt;

&lt;p&gt;The documentation should also include attack scenarios and explain how the protocol's defenses mitigate them. Rather than claiming the protocol is "flash loan resistant," the documentation should describe a specific attack (e.g., "an attacker uses a flash loan to acquire 10% of token supply and vote for a malicious proposal"), explain why this attack fails (e.g., "voting power is measured at block height N-1, before the flash loan was executed"), and quantify the minimum cost and risk to the attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestration Rules and Monitoring
&lt;/h2&gt;

&lt;p&gt;Beyond the protocol architecture itself, defending against flash loan attacks requires orchestration rules and monitoring systems. These rules define what constitutes normal behavior and what should trigger alerts or protective actions.&lt;/p&gt;

&lt;p&gt;An orchestration rule might specify: "If a single transaction moves the DEX price by more than 20%, queue all liquidations for the next block and alert the risk management team." This rule codifies the assumption that prices should not move more than a threshold amount in a single transaction, and responds automatically to violations.&lt;/p&gt;

&lt;p&gt;Another rule might specify: "If governance voting power changes by more than 10% in a single block, pause governance proposals until the change is explained." This prevents governance attacks even if the snapshot mechanism has a bug.&lt;/p&gt;

&lt;p&gt;Monitoring systems should track historical price movements, liquidation activity, and flash loan usage. A spike in any of these metrics might indicate an attack in progress. For example, if the protocol normally processes 10 liquidations per day and suddenly processes 100 liquidations in a single block, this is anomalous and should trigger investigation.&lt;/p&gt;

&lt;p&gt;These monitoring rules should be explicitly documented, including the thresholds that trigger alerts and the responses that are taken. This allows developers building on top of the protocol to understand the protocol's expected behavior and to design their own applications accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Case Study: The bZx Attack
&lt;/h2&gt;

&lt;p&gt;The February 2020 bZx attack demonstrates how flash loan vulnerabilities combine with design flaws to create exploitable conditions. The attack had several stages. First, the attacker used a flash loan to borrow a large amount of sUSD from dYdX. Second, they used this capital to dump sUSD on Uniswap, crashing the sUSD price. Third, bZx's oracle reported the crashed price, triggering liquidations of positions that were collateralized in sUSD. Fourth, the attacker liquidated these positions themselves, collecting liquidation fees. Finally, they repaid the flash loan and kept the profit.&lt;/p&gt;

&lt;p&gt;The attack worked because bZx relied on Uniswap spot prices, which were vulnerable to flash loan manipulation. The protocol did not implement TWAP or multiple price sources. Additionally, the liquidation mechanism allowed the attacker to be the liquidator, removing the incentive to liquidate fairly.&lt;/p&gt;

&lt;p&gt;The bZx team responded by implementing multiple price sources and more robust liquidation mechanics. This case is instructive not because it shows an unbeatable attack, but because it shows how flash loan attacks exploit the intersection of several vulnerabilities. No single defense would have prevented the attack, but multiple layers of defense made it possible to block it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Testing and Documentation of Defenses
&lt;/h2&gt;

&lt;p&gt;Documenting flash loan defenses requires including security testing strategies in the technical documentation. The documentation should describe how the protocol verifies its oracle implementation, how it tests liquidation mechanics under stress conditions, and how it validates that governance mechanisms are resilient to flash loan attacks.&lt;/p&gt;

&lt;p&gt;Security testing for oracle defenses should include simulating flash loan attacks where the attacker tries to move prices beyond the protocol's thresholds. The test should verify that the protocol either rejects the attack or handles the price movement safely. The documentation should include the attack scenarios tested and the results.&lt;/p&gt;

&lt;p&gt;For liquidation defenses, testing should simulate scenarios where many positions become under-collateralized simultaneously. The documentation should show that liquidation mechanisms do not create the cascading failures that benefit attackers.&lt;/p&gt;

&lt;p&gt;Governance testing should simulate acquiring voting power via flash loan and attempt to vote on malicious proposals. The documentation should verify that the voting power snapshot mechanism prevents this attack.&lt;/p&gt;

&lt;p&gt;The documentation should also describe the assumptions underlying each defense. For example, if a protocol assumes that a liquidation delay of one block is sufficient to allow monitoring and response, the documentation should explain this assumption and the scenarios under which it holds or fails.&lt;/p&gt;

&lt;p&gt;Professional Web3 documentation requires that these testing strategies and assumptions be explicitly stated, not hidden. Developers need to understand not just what defenses exist, but how they were validated and what edge cases might expose weaknesses.&lt;/p&gt;

&lt;p&gt;If you need professional Web3 documentation for your protocol or a full-stack Next.js application to manage your blockchain infrastructure, visit &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project requirements.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>security</category>
    </item>
    <item>
      <title>Business Logic Failures in Smart Contracts: SC02:2026 and Mathematical Verification</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Sun, 26 Apr 2026 15:21:23 +0000</pubDate>
      <link>https://forem.com/mericcintosun/business-logic-failures-in-smart-contracts-sc022026-and-mathematical-verification-3p4k</link>
      <guid>https://forem.com/mericcintosun/business-logic-failures-in-smart-contracts-sc022026-and-mathematical-verification-3p4k</guid>
      <description>&lt;p&gt;Smart contract security extends far beyond syntax correctness and gas optimization. A contract can execute without reverting, process transactions according to its code, and still destroy economic value through flawed business logic. These invariant failures represent the most insidious class of vulnerability because they leave no runtime errors, trigger no access control checks, and operate within the written rules of the system.&lt;/p&gt;

&lt;p&gt;The SC02:2026 classification captures a critical gap in development practice: the gap between what code does and what code should do. This distinction separates syntactically valid contracts from economically sound ones. A Solidity contract compiles. It may pass unit tests. It can fail spectacularly in production because the underlying mathematical model that governs token transfers, collateral calculations, or protocol incentives is fundamentally broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Invariants and Their Violations
&lt;/h2&gt;

&lt;p&gt;An invariant is a property that must remain true at all times. In financial protocols, invariants encode the bedrock assumptions of the system: the total supply should never exceed the cap, collateral value should always exceed debt, or user balances should equal the sum of all individual transfers plus minting minus burning. When code execution violates these properties, invariant failure occurs.&lt;/p&gt;

&lt;p&gt;Invariant failures differ from traditional vulnerabilities in their nature and detection. A reentrancy bug triggers an explicit security failure; an invariant failure silently accumulates mathematical debt. The contract executes every function call correctly according to its logic. The ledger updates properly. The balance transfers work. Yet the system drifts into a state where the fundamental economic model collapses, often invisibly, until external systems detect the damage.&lt;/p&gt;

&lt;p&gt;Consider a lending protocol with a simple rule: total borrows must never exceed total deposits times a collateral ratio. This invariant defines the protocol's solvency. A developer implements proper access controls on borrow functions, validates inputs, and updates state consistently. Yet if the deposit function fails to account for a specific token type, or if liquidation logic miscalculates interest accrual, the invariant breaks. Users can borrow more than is mathematically defensible. The protocol becomes insolvent while its code executes flawlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Categories of Business Logic Failures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mathematical Model Errors
&lt;/h3&gt;

&lt;p&gt;The most common source of invariant violations stems from mathematical misimplications of the business requirements. A protocol designer wants to distribute rewards proportionally to stake size. The developer implements this using integer arithmetic without understanding truncation effects. After thousands of transactions, the sum of all distributed rewards exceeds the total allocation due to rounding errors introduced at each step. The invariant "total distributed equals allocation" fails quietly.&lt;/p&gt;

&lt;p&gt;Interest calculation presents another classic failure mode. A lending protocol compounds interest per block. The developer multiplies the outstanding balance by an interest rate and adds it to the ledger. Over time, the accumulated interest grows exponentially. But if the implementation fails to handle edge cases, such as blocks with zero elapsed time or deposits made within the same block, the interest calculation breaks. The protocol can overpay or underpay interest in ways that persist through dozens of transactions before detection.&lt;/p&gt;

&lt;p&gt;Price oracle dependencies introduce mathematical brittleness at scale. A protocol relies on external price feeds to determine collateral adequacy. If the oracle updates asynchronously and the smart contract lacks proper staleness checks, liquidation logic operates on stale prices. A collateral price crashes, but the contract still considers it safe because it hasn't seen the new price yet. Users maintain undercollateralized positions until the oracle catches up. The invariant "all positions are adequately collateralized" is violated.&lt;/p&gt;

&lt;h3&gt;
  
  
  State Transition Errors
&lt;/h3&gt;

&lt;p&gt;Business logic failures often emerge from incorrect state transitions rather than incorrect calculations. A token swap protocol maintains a reserve of each asset and expects the product of reserves to never decrease. The constant product formula, xy = k, is the invariant. A developer implements the swap function but forgets to update reserves after token transfers complete. Users receive tokens from the pool, but the pool's recorded reserves remain unchanged. Future swaps operate on an incorrect basis. The invariant collapses as the effective exchange rate drifts further from equilibrium.&lt;/p&gt;

&lt;p&gt;Governance systems are particularly prone to state transition errors. A DAO allows members to vote on proposals and execute them once a threshold is met. The voting state tracks proposal counts, voting power distributed, and execution status. If the implementation forgets to decrement available voting power after a vote is cast, users can vote multiple times. Or if execution fails to mark a proposal as executed, it can be executed again. The invariant "each vote counts exactly once" is violated.&lt;/p&gt;

&lt;p&gt;Upgrade mechanisms introduce subtle state transition risks. A protocol uses a proxy pattern to allow logic upgrades. The invariant states that state variables maintain their meaning across upgrades. But if a new implementation assumes a different variable layout or interprets existing values differently, the state becomes corrupted. A uint256 intended to represent total supply gets reinterpreted as a price oracle address. The invariant "state variables are correctly initialized" fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Control and Authorization Failures
&lt;/h3&gt;

&lt;p&gt;Not all invariant violations stem from mathematical errors. Some arise from who is allowed to modify state. A protocol grants minting privileges to an external address with the assumption that it will mint only under specific conditions. If that address is compromised or behaves unexpectedly, the total supply invariant breaks. The contract code is correct; the authorization model is flawed.&lt;/p&gt;

&lt;p&gt;Delegated authority compounds this risk. A contract allows users to delegate their voting power but fails to prevent self-delegation or circular delegation chains. A single user delegates to themselves, and the system double-counts their power. Or a chain of delegations creates an unbounded recursion that exhausts gas. The invariant "voting power is counted exactly once" fails due to delegation logic errors.&lt;/p&gt;

&lt;p&gt;Upgrade authority presents the highest-stakes authorization risk. If a contract owner can upgrade critical logic without timelock or governance review, they can break invariants arbitrarily. A sophisticated protocol might grant upgrade rights to a multisig, but if the multisig is poorly secured or governance is concentrated, the invariant "protocol behavior is decentralized" is violated in practice, even if the code is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Invariant Failures
&lt;/h2&gt;

&lt;p&gt;Detecting invariant violations requires explicit verification beyond the tests that verify happy-path behavior. Developers must articulate invariants formally, then prove the code maintains them or identify cases where the code breaks them.&lt;/p&gt;

&lt;p&gt;Formal verification via tools like &lt;strong&gt;Certora&lt;/strong&gt; or &lt;strong&gt;Mythril&lt;/strong&gt; can check invariants statically. These tools translate Solidity into higher-order logic and prove properties across all possible execution paths. A developer specifies that "the sum of all balances equals total supply" and runs the prover. The tool either confirms the invariant always holds or produces a counterexample showing a specific sequence of calls that violates it. This approach catches many mathematical failures but requires expertise to set up correctly and can be expensive to run on large systems.&lt;/p&gt;

&lt;p&gt;Practical invariant testing augments formal verification. Instead of trying to prove properties for all cases, tests exercise the contract across representative scenarios and check invariants at each step. A test mints tokens, transfers them, burns them, and verifies at every stage that total supply equals the sum of balances. Another test exercises liquidation logic and confirms that after liquidation, the system's total collateral always exceeds total borrows times the safety margin.&lt;/p&gt;

&lt;p&gt;Invariant testing frameworks like &lt;strong&gt;Foundry&lt;/strong&gt;'s property-based testing (via differential fuzzing) allow developers to specify invariants once and then generate hundreds of random call sequences to verify the invariant holds. The test runner generates random operations, executes them, checks the invariant, and reports any violation with the sequence of calls that caused it. This approach catches many edge cases that manual tests miss.&lt;/p&gt;

&lt;p&gt;Static analysis of state transitions provides a lighter-weight check. A developer manually traces how each function modifies state and ensures that functions either maintain invariants or explicitly transition from one valid state to another. This is tedious but crucial for critical-path functions like liquidation or token transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documenting Invariants for Audit
&lt;/h2&gt;

&lt;p&gt;Professional security audits depend on clear articulation of invariants. An auditor cannot verify something that has never been stated. Many breaches occur because the protocol's invariants exist only in the designer's head, not in code comments, specification documents, or tests.&lt;/p&gt;

&lt;p&gt;A proper audit documentation package includes an explicit invariants section. For each invariant, the documentation specifies what the invariant is, why it matters, which functions are responsible for maintaining it, and where violations would manifest. An example for a lending protocol might read:&lt;/p&gt;

&lt;p&gt;"Invariant 1: Total supply of borrowed tokens must never exceed total supply of lent tokens. This invariant ensures the protocol never owes more assets than it holds. The borrow() function maintains this invariant by checking that requested amount plus existing borrows does not exceed available liquidity. The repay() function decreases the borrowed total. Interest accrual increases the borrowed total via the accrue() function, which must validate that after interest is added, the borrowed total still does not exceed available liquidity."&lt;/p&gt;

&lt;p&gt;This documentation forces the developer to identify exactly which functions touch the invariant and in what ways. An auditor can then focus their review on those specific paths. If a function is missing from the list, that gap is immediately apparent.&lt;/p&gt;

&lt;p&gt;Invariant documentation should also include mathematical proofs for critical invariants. For a constant product market maker, the documentation proves that the product formula xy = k is maintained across all valid swaps. For a lending protocol, it proves that collateral value remains adequate under all liquidation scenarios. These proofs need not use formal notation but must be precise enough that a competent engineer can verify them or identify their flaws.&lt;/p&gt;

&lt;p&gt;The documentation should address edge cases explicitly. If an invariant is "all user balances must be non-negative", the documentation should clarify what happens with zero balances, how rounding affects balance calculations, and whether the invariant applies before or after state updates. This precision prevents misunderstandings that lead to code that maintains a slightly different version of the intended invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Patterns That Violate Invariants
&lt;/h2&gt;

&lt;p&gt;Certain coding patterns and design decisions repeatedly lead to invariant failures. Recognizing these patterns allows developers and auditors to focus scrutiny appropriately.&lt;/p&gt;

&lt;p&gt;State updates that happen out of order introduce failures when the intermediate states violate invariants. A swap function receives tokens from the user, calculates the output, and transfers output to the user. If the developer transfers output before recording the receipt of input, an intermediate state exists where the reserves are wrong. If a reentrancy or other unexpected behavior occurs during the token transfer, the invariant is already violated.&lt;/p&gt;

&lt;p&gt;Unchecked external calls provide vectors for invariant violations. A contract calls an external token's transfer function and assumes it either succeeds or reverts. If the token returns false instead of reverting, the contract's state updates as if the transfer succeeded, but the tokens never actually moved. The invariant "balance changes equal token transfers" breaks.&lt;/p&gt;

&lt;p&gt;Approximations and truncation in calculations accumulate over time. A developer uses division to convert units, accepting that some precision is lost. Across thousands of transactions, these small losses compound. Interest calculations, fee collections, and reward distributions are particularly vulnerable. A fee collection function calculates: fee = amount / 100 (treating amount in cents, collecting 1 cent per dollar). After processing 301 cents of transactions, the fee tally shows 3 cents. But the actual fees collected were only 3 cents total, since 1 cent, 1 cent, and 1 cent are lost to truncation. The invariant "fees collected equal amount divided by 100" fails.&lt;/p&gt;

&lt;p&gt;Forgetting to update related state is catastrophic. A transfer function decrements the sender's balance and increments the recipient's balance. But if the function forgets to update a separate "total supply" counter, and that counter is used elsewhere, the invariant "sum of individual balances equals total supply" breaks. This is especially common in protocols that maintain multiple representations of the same logical state.&lt;/p&gt;

&lt;p&gt;Hard-coded values that should be derived introduce brittleness. A protocol hard-codes the interest rate as 5% annually, calculated per block. But if governance changes the rate to 7%, the on-chain calculation is not updated. The invariant "interest accrued matches the current governance-set rate" breaks until code is redeployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Steps for Preventing Invariant Failures
&lt;/h2&gt;

&lt;p&gt;Preventing invariant violations requires discipline throughout the development lifecycle. It begins before code is written.&lt;/p&gt;

&lt;p&gt;First, articulate invariants explicitly before implementation. Write them in plain English, backed by mathematical notation where relevant. For each invariant, identify the state variables it depends on and the functions that must maintain it. Create a single document that serves as a reference for development and audit. This forces clarity and prevents misunderstandings.&lt;/p&gt;

&lt;p&gt;Second, design state management around invariants. If an invariant requires that balances sum to total supply, design the code so that every balance update is a single atomic operation that also updates total supply, or use a computed property that recalculates total supply from balances. Avoid redundant state variables that represent the same information in different ways unless there is a strong reason, and document why.&lt;/p&gt;

&lt;p&gt;Third, write invariant tests that execute alongside unit tests. For each invariant, write a test that verifies it after each major operation. Use property-based testing to generate random sequences of operations and verify invariants hold throughout. Integrate these tests into CI/CD so they run on every commit.&lt;/p&gt;

&lt;p&gt;Fourth, perform a dedicated invariants review during code review. Rather than just checking for syntax errors and logic bugs, reviewers explicitly verify that each function maintains the invariants it should maintain. Create a checklist of invariants and mark which ones each function touches.&lt;/p&gt;

&lt;p&gt;Fifth, use external audits to validate invariant maintenance. Provide the auditors with the invariants documentation and request that their review specifically address whether the code provably maintains each invariant. If an auditor asks "why does this function do X?", and the answer is "to maintain invariant Y", the invariants documentation should already explain that.&lt;/p&gt;

&lt;p&gt;Sixth, consider formal verification for critical invariants. If the protocol manages significant user funds, spending the time and expense to formally verify core invariants is justified. Focus formal verification on the highest-stakes invariants first, such as those guaranteeing solvency or preventing fund loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study: A Liquidation Invariant Failure
&lt;/h2&gt;

&lt;p&gt;Consider a hypothetical lending protocol with collateralized borrowing. Users deposit collateral and borrow stablecoins. The protocol's core invariant is: sum of collateral value of all users, times safety margin (e.g., 1.5), must exceed total borrowed stablecoins. This ensures every dollar borrowed is backed by at least 1.5 dollars in collateral.&lt;/p&gt;

&lt;p&gt;The developer implements a liquidation function that an external party can call when a user's collateral falls below the threshold. The liquidation function checks the current collateral value using an oracle, compares it to the user's debt, and if undercollateralized, transfers the collateral to the liquidator and reduces the user's debt.&lt;/p&gt;

&lt;p&gt;However, the oracle price updates asynchronously. Between the time the user's collateral crashes and the oracle updates, the price is stale. A liquidator sees stale data and liquidates the user incorrectly, taking collateral at an outdated price. Or the oracle never updates for a specific asset, and the collateral is considered safe forever even though its real value is zero. The invariant "collateral value times margin exceeds borrows" is violated because the contract's representation of collateral value is wrong.&lt;/p&gt;

&lt;p&gt;To fix this, the developer implements oracle staleness checks. The liquidation function verifies that the price is less than N blocks old before accepting it. For assets with no recent price, the function treats them as zero value, forcing liquidation immediately if a user holds them. Additionally, the developer writes a test that simulates price crashes and verifies that the invariant is maintained across thousands of liquidations.&lt;/p&gt;

&lt;p&gt;The developer also documents the invariant explicitly: "Invariant: totalCollateralValue() * 1.5 &amp;gt;= totalBorrowed. The liquidate() function maintains this by ensuring only users with collateral value less than debt / 1.5 can be liquidated, and liquidation removes sufficient debt to restore the ratio. The getCollateralValue() function must use recent oracle prices, validated by staleness checks. If an oracle is stale for more than N blocks, collateral denominated in that asset is valued at zero."&lt;/p&gt;

&lt;p&gt;An auditor reviews this documentation, sees that the liquidation logic depends entirely on oracle correctness, and focuses their review there. They test oracle edge cases, verify staleness checks, and confirm that the liquidation math is sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Role of Auditors in Invariant Verification
&lt;/h2&gt;

&lt;p&gt;External audits serve a critical function in catching invariant violations that development teams miss. But audit effectiveness depends on how clearly teams communicate invariants to auditors.&lt;/p&gt;

&lt;p&gt;An audit that receives no invariants documentation can only review the code for obvious bugs and common vulnerability patterns. It cannot answer the question, "Does this code do what it should?" because the "should" is never specified. The auditor might spend time reviewing a function that is correct given the invariant it maintains, wasting time that could be spent on a different function that actually violates its invariant.&lt;/p&gt;

&lt;p&gt;An audit that receives comprehensive invariants documentation can perform targeted review. The auditor reads the documented invariants, understands the protocol's intended behavior, and then reviews the code to verify it actually maintains those invariants. If a documented invariant is not maintained, the auditor reports it as a critical issue. If the code maintains invariants that are not documented, the auditor questions why, ensuring that undocumented behavior is not accidentally relied upon.&lt;/p&gt;

&lt;p&gt;Auditors should also request formal specifications where feasible. A specification in pseudo-code or structured English describes the intended behavior at a higher level than Solidity. The auditor can then verify that the Solidity implementation correctly translates the specification. Mismatches between specification and implementation reveal invariant violations.&lt;/p&gt;

&lt;p&gt;Additionally, auditors should request data on test coverage and property-based testing results. If the development team has run property-based tests on the contract and verified invariants across thousands of random call sequences, that provides strong evidence that invariants are maintained. If testing is minimal, the audit should be more thorough and more skeptical.&lt;/p&gt;

&lt;p&gt;The most effective audit processes involve regular communication between auditors and developers. As the auditor reviews code and identifies potential invariant violations, the developer can clarify the intent and the auditor can verify whether the concern is real. This iterative process catches issues that a one-way code review might miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment and Monitoring
&lt;/h2&gt;

&lt;p&gt;Even after development and audit, invariant monitoring must continue post-deployment. Many invariant violations only become apparent when the contract operates at scale, under conditions the development team did not anticipate.&lt;/p&gt;

&lt;p&gt;Deployment should include monitoring hooks that check key invariants periodically. An off-chain monitor can call view functions that verify invariants. For a lending protocol, the monitor calls getCollateralValue(), getTotalBorrowed(), and verifies that the invariant holds. If a violation is detected, alerts trigger so the team can investigate.&lt;/p&gt;

&lt;p&gt;Logging should record enough information to reconstruct invariant state at any point in time. Every transaction that affects a critical invariant should log the before and after states of that invariant. Analysis tools can then replay the transaction history and identify precisely when and how an invariant was violated.&lt;/p&gt;

&lt;p&gt;Frontend and indexing layers should compute invariants independently and compare against on-chain state. A subgraph indexing the protocol can calculate the sum of all balances from events and compare it to the total supply recorded on-chain. Discrepancies indicate an invariant violation.&lt;/p&gt;

&lt;p&gt;Governance should include mechanisms to pause critical functionality if an invariant violation is detected. If monitoring detects that collateral value has fallen below required levels, the protocol should be able to pause new borrowing until the situation is resolved. This limits damage from invariant failures.&lt;/p&gt;

&lt;p&gt;Security contact information should be public and monitored. If invariant violations are discovered by third parties, the team needs a rapid way to receive reports and respond. Responsible disclosure processes are essential; developers who discover violations should have a clear path to report them to the team before public disclosure.&lt;/p&gt;




&lt;p&gt;Professional Web3 documentation and full-stack Next.js development require deep technical precision and clear communication with engineering teams. Reach out via &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; for specialized documentation or application development work.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>security</category>
      <category>softwareengineering</category>
      <category>web3</category>
    </item>
    <item>
      <title>OWASP 2026 Smart Contract Vulnerabilities: Access Control (SC01:2026) Analysis</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Fri, 24 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/owasp-2026-smart-contract-vulnerabilities-access-control-sc012026-analysis-2jen</link>
      <guid>https://forem.com/mericcintosun/owasp-2026-smart-contract-vulnerabilities-access-control-sc012026-analysis-2jen</guid>
      <description>&lt;p&gt;Access control failures represent the highest-severity class of smart contract vulnerabilities in the OWASP 2026 Top 10 list. These vulnerabilities occur when authentication and authorization mechanisms fail to properly restrict what users or roles can do within a contract, leading to unauthorized state changes, fund theft, or protocol manipulation. Unlike traditional web application security where access control failures often affect user privacy, access control failures in smart contracts directly enable theft of assets or permanent protocol compromise.&lt;/p&gt;

&lt;p&gt;The severity stems from smart contracts operating in an adversarial environment where attackers can directly call any public function with any input. Without proper access control, an attacker needs only to identify a sensitive function and call it. The immutability of blockchain means once an exploit succeeds, the damage is permanent and cannot be rolled back by developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Access Control in Smart Contracts
&lt;/h2&gt;

&lt;p&gt;Access control in smart contracts requires three distinct layers: authentication (verifying who is calling), authorization (determining what that caller is permitted to do), and state isolation (ensuring the caller cannot directly manipulate state that should be protected).&lt;/p&gt;

&lt;p&gt;Authentication in smart contracts relies on the sender address, which is cryptographically verified by the blockchain itself. The &lt;code&gt;msg.sender&lt;/code&gt; global variable returns the address of the immediate caller. However, authentication alone proves only who initiated a transaction; it does not establish what they should be permitted to do. Authorization builds on authentication by implementing role-based permissions, time-based restrictions, or function-specific guards.&lt;/p&gt;

&lt;p&gt;State isolation requires that mutable state changes only occur through controlled entry points. Many vulnerabilities arise when developers create administrative functions without protecting them, or when they implement delegation patterns that inadvertently expose control to unintended parties. The core principle is that any function modifying critical state must first verify that &lt;code&gt;msg.sender&lt;/code&gt; holds the required permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Failure Patterns
&lt;/h2&gt;

&lt;p&gt;Analysis of post-2023 DeFi exploits reveals consistent patterns in how access control failures occur. The most common involve unprotected administrative functions, specifically functions that mint tokens, transfer reserves, change protocol parameters, or pause the system. In the Poly Network exploit (August 2021), attackers modified keeper addresses because the contract allowed any caller to execute administrative state changes without verification. While this attack predates the 2026 OWASP classification, it demonstrates the pattern that persists in modern contracts.&lt;/p&gt;

&lt;p&gt;A second pattern involves delegation vulnerabilities where proxies or delegatecall patterns expose administrative functionality to unintended callers. The Wormhole incident (January 2022) involved an uninitialized proxy contract where any caller could claim admin status. The vulnerability existed because initialization logic used delegatecall without protecting the target contract, allowing attackers to assume control. This represents a cross-layer access control failure where the proxy infrastructure failed to enforce authorization.&lt;/p&gt;

&lt;p&gt;A third pattern emerges in protocols that implement role-based access control but incorrectly assign or transfer roles. The BadgerDAO incident (December 2021) exploited approval functions by compromising private keys of addresses holding elevated permissions. While the root cause was key compromise rather than contract logic, the impact was possible only because a single compromised key granted unlimited authority. Lack of multi-signature requirements or time-lock mechanisms for sensitive operations increased the blast radius.&lt;/p&gt;

&lt;p&gt;The 2024 Liquid Staking Security Report identified fourteen distinct staking protocols with access control vulnerabilities, ranging from unprotected withdrawal functions to improper role validation in reward distribution. These vulnerabilities persisted in live protocols because developers had not implemented comprehensive authorization checks across all state-changing functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Role-Based Authorization
&lt;/h2&gt;

&lt;p&gt;Implementing role-based authorization requires explicit design during the risk modeling phase, before writing any code. The first step is to enumerate all sensitive operations in the protocol: parameter changes, fund transfers, role assignments, and emergency controls. Each operation requires an associated permission level.&lt;/p&gt;

&lt;p&gt;Define roles as semantic categories rather than boolean flags. Instead of a single &lt;code&gt;isAdmin&lt;/code&gt; boolean, create roles such as &lt;code&gt;GOVERNANCE_ROLE&lt;/code&gt;, &lt;code&gt;LIQUIDATOR_ROLE&lt;/code&gt;, &lt;code&gt;PAUSER_ROLE&lt;/code&gt;, and &lt;code&gt;OPERATOR_ROLE&lt;/code&gt;. Each role should encapsulate a specific set of capabilities. This design prevents a single compromise from granting universal access and enables fine-grained revocation.&lt;/p&gt;

&lt;p&gt;The Open Zeppelin AccessControl library implements role-based access using a mapping structure where each role is a bytes32 identifier, and the contract tracks which addresses hold each role. The implementation pattern checks that &lt;code&gt;hasRole(ROLE, msg.sender)&lt;/code&gt; returns true before executing sensitive code. This approach scales efficiently and allows for role-based administration.&lt;/p&gt;

&lt;p&gt;Here is how a basic role structure looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts/access/AccessControl.sol";

contract StakingPool is AccessControl {
    bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE");
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    uint256 public rewardRate;
    bool public paused;

    constructor() {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(GOVERNANCE_ROLE, msg.sender);
    }

    function setRewardRate(uint256 newRate) external onlyRole(GOVERNANCE_ROLE) {
        rewardRate = newRate;
    }

    function pauseStaking() external onlyRole(PAUSER_ROLE) {
        paused = true;
    }

    function delegateOperatorRole(address newOperator) external onlyRole(GOVERNANCE_ROLE) {
        grantRole(OPERATOR_ROLE, newOperator);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference from a simpler owner-based pattern is that this design allows governance to delegate specific capabilities to multiple addresses without granting full control. The &lt;code&gt;DEFAULT_ADMIN_ROLE&lt;/code&gt; acts as the top-level role that can manage all other roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control in Proxy Patterns
&lt;/h2&gt;

&lt;p&gt;Proxy contracts introduce a critical access control dimension because the proxy contract itself controls which implementation contract receives delegatecalls. If the proxy initialization is unprotected, an attacker can claim admin status or redirect delegatecalls to a malicious implementation.&lt;/p&gt;

&lt;p&gt;The UUPS (Universal Upgradeable Proxy Standard) pattern requires the implementation contract to define the upgrade authority, preventing a proxy from being exploited independently. The pattern enforces that only the designated authority can call &lt;code&gt;upgradeTo&lt;/code&gt;, and this check occurs in the implementation contract, not the proxy.&lt;/p&gt;

&lt;p&gt;Here is the authorization check in a UUPS implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract StakingImplementation is UUPSUpgradeable, OwnableUpgradeable {
    uint256 public rewardRate;

    function initialize(address owner) public initializer {
        __Ownable_init();
        _transferOwnership(owner);
    }

    function setRewardRate(uint256 newRate) external onlyOwner {
        rewardRate = newRate;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_authorizeUpgrade&lt;/code&gt; function implements the authorization check. This function is called by the proxy before executing any upgrade, and it reverts if &lt;code&gt;msg.sender&lt;/code&gt; is not the owner. Without this override, the proxy cannot enforce authorization at all.&lt;/p&gt;

&lt;p&gt;Transparent proxies separate the admin interface from the implementation interface, ensuring that administrative functions called through the proxy never reach the implementation. This adds complexity but provides stronger isolation. The trade-off is that transparent proxies require more careful testing to verify that function selectors do not collide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-Locks and Multi-Signature Controls
&lt;/h2&gt;

&lt;p&gt;For critical operations, a single authorized address creates a single point of failure. Time-locks introduce a delay between when an operation is initiated and when it executes, allowing the community or security teams to detect and potentially block malicious proposals. Multi-signature schemes require multiple independent parties to approve an action, distributing the trust requirement.&lt;/p&gt;

&lt;p&gt;The pattern combines both mechanisms: a governance function initiates a proposal, which enters a time-locked queue. After a delay period (typically 48 to 72 hours in production protocols), any caller can execute the proposal if no veto has occurred.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract TimeLockController {
    bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
    bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

    uint256 public minDelay = 2 days;
    mapping(bytes32 =&amp;gt; bool) public isOperationReady;
    mapping(bytes32 =&amp;gt; uint256) public operationTimestamp;

    function schedule(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt,
        uint256 delay
    ) external onlyRole(PROPOSER_ROLE) {
        bytes32 id = hashOperation(target, value, data, predecessor, salt);
        require(!isOperationReady[id], "Operation already scheduled");
        require(delay &amp;gt;= minDelay, "Delay too short");

        operationTimestamp[id] = block.timestamp + delay;
    }

    function execute(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt
    ) external payable onlyRole(EXECUTOR_ROLE) {
        bytes32 id = hashOperation(target, value, data, predecessor, salt);
        require(block.timestamp &amp;gt;= operationTimestamp[id], "Operation not ready");

        isOperationReady[id] = true;
        (bool success, ) = target.call{value: value}(data);
        require(success, "Execution failed");
    }

    function hashOperation(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt
    ) public pure returns (bytes32) {
        return keccak256(abi.encode(target, value, data, predecessor, salt));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When integrated with a multi-signature wallet, the proposer role is assigned to the multi-signature contract. The multi-signature contract requires N of M signers to approve before it calls &lt;code&gt;schedule&lt;/code&gt;. This layered approach ensures that at least two separate authorization mechanisms must fail before an attacker gains control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control in Token Contracts
&lt;/h2&gt;

&lt;p&gt;ERC-20 tokens and their variants require careful authorization because they directly control asset transfer. The standard pattern protects the &lt;code&gt;mint&lt;/code&gt; and &lt;code&gt;burn&lt;/code&gt; functions with role checks, and the &lt;code&gt;approve&lt;/code&gt; function grants allowance only to the caller themselves.&lt;/p&gt;

&lt;p&gt;The vulnerability arises when developers create token contracts that allow unrestricted minting or burning. The OnePlus incident (a hypothetical case in security research) would involve a token contract where the &lt;code&gt;mint&lt;/code&gt; function lacks the &lt;code&gt;onlyMinter&lt;/code&gt; guard, allowing any caller to create tokens indefinitely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract GovernanceToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC20("Gov Token", "GOV") {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(uint256 amount) external onlyRole(BURNER_ROLE) {
        _burn(msg.sender, amount);
    }

    function burnFrom(address account, uint256 amount) external onlyRole(BURNER_ROLE) {
        uint256 currentAllowance = allowance(account, msg.sender);
        require(currentAllowance &amp;gt;= amount, "Insufficient allowance");
        _approve(account, msg.sender, currentAllowance - amount);
        _burn(account, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;burnFrom&lt;/code&gt; function requires both the &lt;code&gt;BURNER_ROLE&lt;/code&gt; and existing allowance from the account being burned. This prevents a burner from destroying arbitrary user tokens; it can only burn tokens that the user explicitly allowed it to burn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Access Control
&lt;/h2&gt;

&lt;p&gt;Comprehensive testing must verify that protected functions revert when called by unpermitted addresses and succeed when called by permitted ones. Test coverage should include boundary cases: calling with the owner address, calling with an unauthorized address, calling with address zero, and calling with a contract that receives a permission grant but is not properly initialized.&lt;/p&gt;

&lt;p&gt;Here is a test pattern that validates access control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "forge-std/Test.sol";
import "../src/StakingPool.sol";

contract StakingPoolTest is Test {
    StakingPool pool;
    address governance = address(0x1);
    address operator = address(0x2);
    address unauthorized = address(0x3);

    function setUp() public {
        vm.startPrank(governance);
        pool = new StakingPool();
        pool.grantRole(pool.OPERATOR_ROLE(), operator);
        vm.stopPrank();
    }

    function testGovernanceCanSetRewardRate() public {
        vm.prank(governance);
        pool.setRewardRate(100);
        assertEq(pool.rewardRate(), 100);
    }

    function testOperatorCannotSetRewardRate() public {
        vm.prank(operator);
        vm.expectRevert();
        pool.setRewardRate(100);
    }

    function testUnauthorizedCannotSetRewardRate() public {
        vm.prank(unauthorized);
        vm.expectRevert();
        pool.setRewardRate(100);
    }

    function testGovernanceCanDelegateOperatorRole() public {
        address newOperator = address(0x4);
        vm.prank(governance);
        pool.grantRole(pool.OPERATOR_ROLE(), newOperator);

        vm.prank(newOperator);
        bool hasRole = pool.hasRole(pool.OPERATOR_ROLE(), newOperator);
        assertTrue(hasRole);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test function validates a single authorization scenario. The test suite should also verify that role revocation works correctly and that the default admin cannot be accidentally removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Implementation Mistakes
&lt;/h2&gt;

&lt;p&gt;The most frequent implementation error is forgetting access control entirely on a newly created function. When developers add functionality during an upgrade or emergency response, they often write the function logic without wrapping it in an authorization check. Code review processes must specifically look for any state-changing function that lacks a require statement or modifier checking permissions.&lt;/p&gt;

&lt;p&gt;A second mistake involves checking &lt;code&gt;msg.sender&lt;/code&gt; against a stored address rather than using role-based patterns. Code like &lt;code&gt;require(msg.sender == admin, "Not admin")&lt;/code&gt; cannot scale and prevents delegation of specific capabilities. Role-based patterns using AccessControl or similar libraries are more composable and maintainable.&lt;/p&gt;

&lt;p&gt;A third mistake occurs when developers implement custom authorization logic instead of using established patterns. Custom logic is difficult to audit and often contains subtle flaws. For example, checking role membership using a boolean mapping is weaker than using OpenZeppelin's AccessControl because it does not support hierarchical roles or efficient revocation.&lt;/p&gt;

&lt;p&gt;A fourth mistake involves not protecting initialization functions in upgradeable contracts. If an initializer can be called by any address, an attacker can reinitialize the contract with different settings. The &lt;code&gt;initializer&lt;/code&gt; modifier from OpenZeppelin Upgradeable Contracts prevents this by ensuring that &lt;code&gt;initialize&lt;/code&gt; executes only once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control and Front-Running
&lt;/h2&gt;

&lt;p&gt;Access control mechanisms do not protect against front-running, where an attacker observes a pending transaction and submits a competing transaction that executes first. If a protocol uses access control to limit who can execute liquidations or arbitrage opportunities, a front-runner can include a permissionless function that grants themselves the required role immediately before the liquidation function executes.&lt;/p&gt;

&lt;p&gt;The solution requires separating authorization from execution. Instead of checking roles directly in the liquidation function, the liquidation can accept a signed authorization from an authorized address. The function then verifies the signature before proceeding. This prevents a front-runner from claiming a role they do not actually hold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract FrontRunProtectedLiquidation {
    bytes32 public LIQUIDATION_TYPEHASH = 
        keccak256("Liquidation(address borrower,uint256 repayAmount,uint256 nonce)");

    address public authorityAddress;
    mapping(address =&amp;gt; uint256) public nonces;

    function liquidateWithSignature(
        address borrower,
        uint256 repayAmount,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            keccak256(abi.encode(LIQUIDATION_TYPEHASH, borrower, repayAmount, nonces[borrower]))
        ));

        address recovered = ecrecover(digest, v, r, s);
        require(recovered == authorityAddress, "Invalid signature");

        nonces[borrower]++;
        _executeLiquidation(borrower, repayAmount);
    }

    function _executeLiquidation(address borrower, uint256 amount) internal {
        // Liquidation logic
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern ensures that only a pre-authorized signer can initiate liquidations, and the signature includes a nonce to prevent replay attacks. The front-runner cannot forge a valid signature without the private key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit Checklist for Access Control
&lt;/h2&gt;

&lt;p&gt;When reviewing a smart contract for access control vulnerabilities, validate the following items:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Every state-changing function has an authorization check. This includes mint, burn, transfer from authority, parameter changes, and emergency functions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authorization checks occur before state modifications. If a function modifies state and then checks authorization, the function is vulnerable to re-entrancy or partial execution attacks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role definitions are semantic and specific. Using multiple small roles (like MINTER_ROLE and BURNER_ROLE) rather than a single ADMIN_ROLE prevents over-privileged addresses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role assignment uses established patterns like OpenZeppelin AccessControl or role managers, not custom boolean mappings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Administrative functions like grantRole and revokeRole themselves have authorization checks to prevent unauthorized role changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Proxy initialization is protected, either through the initializer modifier or by having the proxy deploy with a pre-set admin address that cannot be changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time-locks or multi-signature approvals protect critical parameter changes and upgrades.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signature-based authorization includes replay protection through nonces or domain separators.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test coverage includes both positive cases (authorized caller succeeds) and negative cases (unauthorized caller reverts).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access control is documented in comments explaining which roles have which capabilities and why those capabilities are necessary.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The development and deployment of secure smart contracts depends on implementing access control during the design phase, not as an afterthought. Developers who define roles, authorization requirements, and delegation patterns before writing code produce contracts that withstand adversarial scrutiny. The OWASP 2026 SC01 classification reflects the reality that access control failures remain the highest-impact vulnerability class in production deployments, and their prevention requires discipline and established patterns.&lt;/p&gt;

&lt;p&gt;I am available for professional Web3 documentation and smart contract analysis, as well as full-stack Next.js development work. You can find my availability at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>Sui Move Fundamentals for Developers: Object-Oriented Blockchain Architecture</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Wed, 22 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/sui-move-fundamentals-for-developers-object-oriented-blockchain-architecture-48ho</link>
      <guid>https://forem.com/mericcintosun/sui-move-fundamentals-for-developers-object-oriented-blockchain-architecture-48ho</guid>
      <description>&lt;h2&gt;
  
  
  Understanding Sui's Object Model
&lt;/h2&gt;

&lt;p&gt;Ethereum pioneered the account-based model, where smart contracts maintain global state tied to addresses, and all state transitions flow through centralized account abstractions. This architecture has served the ecosystem well, but it forces a mental model where "everything is a balance" or "everything is a mapping." Sui departs fundamentally from this approach by adopting an object-centric paradigm where blockchain state consists of discrete, independently-owned objects rather than accounts holding collections of data.&lt;/p&gt;

&lt;p&gt;In Sui, objects are first-class citizens. Each object has an immutable ID, a type, an owner (which can be an address, another object, or shared), and version metadata. When you transfer an object, you are not updating a central ledger; you are changing which address holds the reference to that object. This distinction matters because it enables parallel execution of transactions that touch different objects, whereas account-based systems must serialize all transactions that involve the same account.&lt;/p&gt;

&lt;p&gt;The object model also changes how you think about data access control. In Ethereum, a smart contract controls access to data through function modifiers and permission checks. In Sui, ownership is enforced by the blockchain itself. If you own an object, you can include it as a mutable input to a transaction. If you do not own it, the network rejects the transaction at the VM level, before any code executes. This shift moves security enforcement from the contract layer to the consensus layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Move Language: Linearity and Type Safety
&lt;/h2&gt;

&lt;p&gt;Move is the language that powers Sui's smart contracts, but Move originated at Facebook (now Meta) as a language designed around linear types and resource semantics. Unlike Solidity, where a variable can be copied, referenced, or modified freely, Move enforces strict rules about how values move through code. Every value must be used exactly once or explicitly ignored. This is not a style choice; it is enforced by the compiler.&lt;/p&gt;

&lt;p&gt;To understand Move's power, consider a common vulnerability in Solidity: double-spending within the same transaction. A Solidity contract might check a user's balance, transfer tokens to them, and then later in the same function use that balance again because the state update happens asynchronously. Move prevents this at compile time. Once you pass a value to a function or bind it to a new variable, you can no longer use the old binding. The compiler rejects the code before it ever runs.&lt;/p&gt;

&lt;p&gt;Move's type system distinguishes between three categories of values: copy types, drop types, and resource types. Copy types (like booleans and integers) can be duplicated implicitly. Drop types can be discarded implicitly. Resource types must be explicitly handled; they cannot be copied or dropped. When you define a struct as a resource in Move, the compiler ensures that every instance is accounted for. You cannot accidentally lose a resource or create copies without explicit permission. This is why Move is often called a "resources first" language.&lt;/p&gt;

&lt;p&gt;The linear type system has profound implications for security. Integer overflows, a plague in earlier smart contracts, are caught by Move's type checker in safe mode. Reentrancy exploits become structurally impossible because once you pass mutable access to a value into a function, you lose the ability to access it elsewhere in your current call stack. Notional problems like "what if this contract balance increases unexpectedly during a call" are eliminated at the language level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objects and Abilities in Move
&lt;/h2&gt;

&lt;p&gt;Move objects are structs that can carry capabilities, called abilities. These abilities determine what the compiler allows you to do with instances of that struct. The four abilities are copy, drop, store, and key. A struct with the copy ability can be silently duplicated. A struct with drop can be silently discarded. The store ability determines whether a struct can be stored as a field inside another struct. The key ability signals that this struct is an on-chain object that can be owned and transferred.&lt;/p&gt;

&lt;p&gt;Most on-chain objects in Sui are defined with the key ability and lack copy. This forces the compiler to track unique instances. When you create a resource struct, you are creating something that the blockchain will monitor. Only one address can hold it at a time. You cannot replicate it through normal assignment. If you want to transfer it, you must explicitly call a function that changes its ownership.&lt;/p&gt;

&lt;p&gt;Consider a simple example: defining an asset that represents a claim or a digital item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::asset {
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    struct Asset has key {
        id: UID,
        value: u64,
    }

    public fun create_asset(value: u64, ctx: &amp;amp;mut TxContext) -&amp;gt; Asset {
        Asset {
            id: object::new(ctx),
            value,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This struct has the key ability, which means instances live on the blockchain and have identity. The &lt;code&gt;id&lt;/code&gt; field is required; every key struct must have a &lt;code&gt;UID&lt;/code&gt; field named &lt;code&gt;id&lt;/code&gt;. Without the copy ability, passing this struct into a function transfers ownership rather than duplicating it. If a function consumes the Asset (takes it by value), the caller loses access to that instance.&lt;/p&gt;

&lt;p&gt;The ability system is more expressive than it might first appear. It directly maps to what the Move type system can enforce. By removing the copy ability from an Asset struct, we tell the compiler: "every instance of this struct matters, track it carefully." By including the key ability, we tell the blockchain: "this is something that someone owns, index it and make it transferable."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership and Access Control
&lt;/h2&gt;

&lt;p&gt;Ownership in Sui is literal and enforced by the protocol. When a transaction creates an object, that object becomes owned by the transaction sender or by an address specified in the transaction. Ownership is not a convention that the contract checks; it is a property that the network verifies. If you are not the owner of an object, you cannot include it as a mutable input to any transaction.&lt;/p&gt;

&lt;p&gt;This creates a radically different access control model than Ethereum. In Ethereum, you call a function and the function checks permission. In Sui, you construct a transaction, and if you lack the necessary ownership, the transaction fails before execution. The blockchain rejects it at the networking layer, before consensus even considers it.&lt;/p&gt;

&lt;p&gt;Shared objects are Sui's mechanism for public state. When you make an object shared, any address can include it in a transaction. Shared objects require consensus to coordinate access, so transactions affecting shared objects may be serialized. Owned objects, by contrast, can be modified in parallel because ownership prevents conflicts. This architectural choice means developers must think about which data should be shared (requiring consensus coordination) and which should be owned (enabling parallel execution).&lt;/p&gt;

&lt;p&gt;An owned object can also be transferred to another address or wrapped inside another object. Wrapped objects are nested within their parent and cannot be accessed directly; any operation on them must go through the parent. This nesting captures complex ownership hierarchies and permission patterns without requiring runtime checks in contract code. The type system enforces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions and Inputs in Sui
&lt;/h2&gt;

&lt;p&gt;Sui transactions are explicitly structured. You specify which objects you want to read, which you want to mutate, and which are pure inputs (like numbers or strings). The transaction declares its needs upfront, and the blockchain verifies that you have the right to access what you declared.&lt;/p&gt;

&lt;p&gt;A basic transaction might look like this in pseudocode:&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;Transaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;object_id_1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;object_id_2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;immutable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="mi"&gt;0x123456&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;immutable&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="nn"&gt;module&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;function&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 execution, Sui's transaction validation layer checks: does the sender own object_id_1? Is object_id_2 shared or owned by the sender? The pure value (a static address) requires no permission. Only if all checks pass does the transaction enter execution.&lt;/p&gt;

&lt;p&gt;This explicit input specification enables Sui to parallelize transaction execution aggressively. The network can run thousands of transactions in parallel if they touch disjoint sets of objects. Ethereum achieves sequentiality for safety; Sui achieves parallelism without sacrificing safety because ownership prevents conflicts by construction.&lt;/p&gt;

&lt;p&gt;Within a transaction, you pass objects to functions by reference or by value. Passing by value transfers ownership. Passing by immutable reference (&lt;code&gt;&amp;amp;T&lt;/code&gt;) allows reading but not mutation. Passing by mutable reference (&lt;code&gt;&amp;amp;mut T&lt;/code&gt;) allows both reading and mutation, and this is how you modify objects within a transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Safety and Scarcity
&lt;/h2&gt;

&lt;p&gt;Move's linear type system creates genuine scarcity on the blockchain. A resource cannot be duplicated, lost, or created out of thin air. Every token, every NFT, every key or certificate must flow through the system via explicit operations. This is enforced by the compiler, not by contract logic.&lt;/p&gt;

&lt;p&gt;Consider a token type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::token {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    struct Token has key, store {
        id: UID,
        amount: u64,
    }

    public fun transfer(token: Token, to: address, ctx: &amp;amp;mut TxContext) {
        transfer::public_transfer(token, to);
    }

    public fun merge(t1: Token, t2: Token): Token {
        Token {
            id: t1.id,
            amount: t1.amount + t2.amount,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, if you want to merge two tokens, you must write explicit logic to handle the merge. Once merged, the first token's ID is kept and the second is discarded. The compiler accepts this because we explicitly destroyed t2. There is no hidden path where t2 persists. There is no "ghost balance" that could linger unnoticed.&lt;/p&gt;

&lt;p&gt;If someone tries to call merge but then later use t2 again, the compiler rejects it with an error. The borrow checker catches this before the transaction is submitted. This guarantees at the language level that Token instances are accounted for.&lt;/p&gt;

&lt;p&gt;This contrasts sharply with Solidity, where token contracts use a mapping to track balances. If a contract has a bug in that mapping logic, tokens can be created or destroyed silently. The system has no intrinsic guarantee that total supply is preserved. With Move tokens, scarcity is a language property, not a contract invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Contract Patterns and Best Practices
&lt;/h2&gt;

&lt;p&gt;Building robust Sui contracts requires understanding how Move's features map to common patterns. The most important pattern is the separation between owned and shared state. Owned objects should be used for user-specific data: wallets, inventories, positions. Shared objects should be reserved for truly shared state: token reserves, global counters, orderbooks.&lt;/p&gt;

&lt;p&gt;A practical example is a simple escrow contract. In Ethereum, you might have a single contract that holds all escrowed assets in a mapping. In Sui, each escrow agreement is a separate object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::escrow {
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};
    use sui::transfer;

    struct EscrowAgreement&amp;lt;T: store&amp;gt; has key {
        id: UID,
        item: T,
        seller: address,
        buyer: address,
        price: u64,
    }

    public fun create_escrow&amp;lt;T: store&amp;gt;(
        item: T,
        seller: address,
        buyer: address,
        price: u64,
        ctx: &amp;amp;mut TxContext,
    ): EscrowAgreement&amp;lt;T&amp;gt; {
        EscrowAgreement {
            id: object::new(ctx),
            item,
            seller,
            buyer,
            price,
        }
    }

    public fun release&amp;lt;T: store&amp;gt;(
        escrow: EscrowAgreement&amp;lt;T&amp;gt;,
        _payment: Token,
        ctx: &amp;amp;mut TxContext,
    ): T {
        let EscrowAgreement { id, item, seller: _, buyer: _, price: _ } = escrow;
        object::delete(id);
        transfer::public_transfer(item, tx_context::sender(ctx));
        item
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, each escrow is a distinct object. The seller or a mediator can verify the agreement's terms before releasing it. Because the item is stored within the EscrowAgreement, it cannot be accessed or moved except through this contract's functions. The type system ensures that.&lt;/p&gt;

&lt;p&gt;The use of generics (like &lt;code&gt;&amp;lt;T: store&amp;gt;&lt;/code&gt;) is powerful in Sui. A single escrow contract can hold any type of asset, as long as that type has the store ability. This avoids the code duplication and bytecode bloat that comes from writing separate contracts for each asset type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing to Account-Based Models
&lt;/h2&gt;

&lt;p&gt;The conceptual gap between object-based and account-based systems manifests clearly in token handling. An Ethereum ERC-20 token is a contract that maintains a mapping from addresses to balances. Transferring requires calling the token contract, which updates the mapping. Parallel execution is difficult because every transfer touches shared state.&lt;/p&gt;

&lt;p&gt;A Sui token, by contrast, is a distributed collection of owned objects. Each Token object is owned by a specific address. Transferring a token means changing its owner field, which is an operation on that specific object. Thousands of different token transfers can happen in parallel if they involve different token objects.&lt;/p&gt;

&lt;p&gt;From a developer experience perspective, this means Sui contracts are smaller and more focused. You do not need to build elaborate permission systems because ownership is enforced at the protocol level. You do not need to worry about reentrancy because once you pass an object to another function, you cannot access it in your current scope. You do not need to track "total supply" in a central location because the language guarantees that resources are conserved.&lt;/p&gt;

&lt;p&gt;The learning curve is inverted compared to Ethereum. Beginners can write safer contracts faster because the language prevents whole categories of vulnerabilities. But developers must unlearn some habits: there is no centralized state to query, no permission modifiers to write, no balance checks that might be bypassed. Instead, developers work with object references and move values explicitly through call chains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Development Workflow
&lt;/h2&gt;

&lt;p&gt;Setting up a Sui development environment requires the Sui CLI and a local test validator. The typical workflow involves writing Move modules, testing them with Sui's built-in testing framework, and then deploying to testnet or mainnet.&lt;/p&gt;

&lt;p&gt;Testing in Sui is more explicit than in Ethereum. You construct test transactions, supply the objects they need, and verify the results. The Sui CLI provides a REPL where you can interact with live contracts on testnet, inspect object state, and simulate transactions before paying gas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[test]
fun test_escrow_creation() {
    let mut scenario = test_scenario::begin(@0x1);
    let ctx = test_scenario::ctx(&amp;amp;mut scenario);

    let item = MyItem { id: object::new(ctx), value: 100 };
    let escrow = create_escrow(
        item,
        @0x1,
        @0x2,
        1000,
        ctx,
    );

    assert!(escrow.price == 1000, 0);
    test_scenario::end(scenario);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test_scenario module simulates a blockchain environment where you control who is performing transactions. You begin a scenario with a specific sender address, obtain the transaction context, and execute contract functions. After each step, you can inspect the resulting objects and verify invariants.&lt;/p&gt;

&lt;p&gt;Gas costs in Sui are predictable because the network computes them upfront based on transaction size and storage usage. Unlike Ethereum, where gas depends on the execution path taken, Sui charges based on the computational work declared in the transaction. This makes gas budgeting straightforward and eliminates the class of surprises where a transaction costs more than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leveraging Move's Strengths in Production
&lt;/h2&gt;

&lt;p&gt;Production Sui contracts should follow patterns that leverage the language's safety guarantees. First, use owned objects for user-specific data. Do not create shared objects unless you need global coordination. Second, use generic types to write reusable contracts that work with multiple asset types. Third, use the module system to organize code logically; Move packages can contain multiple modules, and modules can depend on each other to form coherent abstractions.&lt;/p&gt;

&lt;p&gt;One pattern that simplifies contract design is the "Capability Object." A capability is a token (a struct with the key ability) that grants permission to perform an action. For example, an admin capability object proves that the holder is an administrator. Only the initial deployer receives this object, and they can transfer it to others or destroy it. This pattern moves authorization from function-level checks to object-level proofs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::admin {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    struct AdminCap has key {
        id: UID,
    }

    public fun create_admin_cap(ctx: &amp;amp;mut TxContext): AdminCap {
        AdminCap {
            id: object::new(ctx),
        }
    }

    public fun verify_admin(_cap: &amp;amp;AdminCap) {
        // If we reach here, the caller passed a valid AdminCap
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functions that require admin access take a reference to AdminCap as a parameter. Only someone who owns the AdminCap object can call these functions. The blockchain enforces this at the transaction validation layer. No runtime permission check is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Implications of Structural Enforcement
&lt;/h2&gt;

&lt;p&gt;The shift from runtime permission checks to compile-time and protocol-level enforcement has profound security implications. Many of the vulnerabilities that plagued early smart contracts are impossible in Move because the language prevents them at compile time. You cannot accidentally reuse a value after passing it to another function. You cannot overflow an integer in safe mode. You cannot create or destroy resources outside of explicit operations.&lt;/p&gt;

&lt;p&gt;This does not mean Sui contracts are automatically secure. Logic errors are still possible. An escrow contract could have a bug in its release conditions. A token contract could mint more than intended if the minting logic is incorrect. But entire classes of vulnerabilities are eliminated by design. The developer can focus on business logic rather than fighting the language.&lt;/p&gt;

&lt;p&gt;Testing practices should reflect this. Because the type system prevents many bugs, tests can focus on correctness of business logic rather than defensive coding. You do not need exhaustive tests for integer overflow scenarios because Move prevents overflow at compile time. You can write clearer tests that specify what should happen in happy-path and edge cases, knowing the language is watching for memory safety and resource leaks.&lt;/p&gt;

&lt;p&gt;The object model also simplifies auditing. Because objects are self-contained and ownership is explicit, an auditor can trace value flows through the contract more directly. There is no hidden state scattered across multiple storage slots. No ambiguous permission logic buried in modifiers. Objects and functions declare their inputs and outputs explicitly, making the contract's surface area clear.&lt;/p&gt;

&lt;p&gt;Professional Web3 documentation and full-stack Next.js development work are available through my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;, should your project require technical writing or application development expertise.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>programming</category>
      <category>web3</category>
    </item>
    <item>
      <title>Wisp CMS and Next.js Blog Template Integration for Independent Developer Portfolios</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Mon, 20 Apr 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/wisp-cms-and-nextjs-blog-template-integration-for-independent-developer-portfolios-e6m</link>
      <guid>https://forem.com/mericcintosun/wisp-cms-and-nextjs-blog-template-integration-for-independent-developer-portfolios-e6m</guid>
      <description>&lt;h2&gt;
  
  
  Building Portfolio Blogs with Wisp CMS and Next.js
&lt;/h2&gt;

&lt;p&gt;Independent developers need to demonstrate their technical knowledge and project experience. A well-designed blog serves as both a portfolio showcase and a platform for technical writing. Wisp CMS, paired with Next.js, provides a lightweight, performant approach to building content-driven developer blogs without the overhead of traditional headless CMS platforms.&lt;/p&gt;

&lt;p&gt;This article examines the Wisp CMS Next.js blog template, focusing on server-side optimizations, dark mode implementation, and RSS feed generation. These features solve specific problems that arise when independent developers need to maintain a production-grade blog while keeping complexity low and build times fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Wisp CMS Architecture
&lt;/h2&gt;

&lt;p&gt;Wisp CMS operates as a file-based content management system designed specifically for Next.js applications. Rather than storing content in a database, Wisp uses the filesystem as its primary content store. This approach eliminates the need for external services, reduces infrastructure costs, and makes content portable and version-controllable.&lt;/p&gt;

&lt;p&gt;The system reads Markdown and MDX files from a designated content directory, parses frontmatter metadata, and exposes this data through a JavaScript API that Next.js can consume during build time or request time. Content files live alongside application code in the repository, making the entire project self-contained.&lt;/p&gt;

&lt;p&gt;Wisp integrates directly with Next.js's file-based routing and build process. When a developer adds a new post to the content directory, Wisp automatically detects it and makes it available through API functions. This means there is no separate deployment process for content—pushing code to production automatically publishes new posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server Components and Performance Optimization
&lt;/h2&gt;

&lt;p&gt;Next.js server components represent a shift in how React applications render content. Server components execute on the server and send only the rendered HTML to the browser, rather than shipping JavaScript that runs client-side. This reduces the JavaScript payload sent to users and improves initial page load time.&lt;/p&gt;

&lt;p&gt;Wisp blog templates leverage server components by rendering blog list pages and individual post pages entirely on the server. When a user requests the blog homepage, the server fetches all posts from the filesystem, renders the HTML, and sends the complete page to the browser. The browser receives no React component tree to hydrate, no state management code, and no unnecessary JavaScript.&lt;/p&gt;

&lt;p&gt;Consider the practical difference. A traditional client-side rendered blog loads a JavaScript bundle that contains the entire React framework, routing logic, state management, and component code. The browser must parse and execute this bundle before rendering anything. A server component-based blog sends only the HTML the user needs to see. The bundle size difference is substantial: server-rendered blog pages ship 40-60% less JavaScript than their client-rendered equivalents, measured using standard bundling tools.&lt;/p&gt;

&lt;p&gt;The performance gain extends beyond initial load. Server components avoid the "JavaScript execution cost" that impacts slower devices. On a mid-range Android phone with limited CPU power, executing 200KB of JavaScript takes measurable time. The same content delivered as pre-rendered HTML displays instantly.&lt;/p&gt;

&lt;p&gt;Implementing this in a Wisp Next.js blog requires marking post list and detail pages as server components. This is the default behavior in the Next.js app directory—components are server components unless explicitly marked as client components with the &lt;code&gt;"use client"&lt;/code&gt; directive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/page.js - rendered on server&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wisp-cms/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PostCard&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/PostCard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog-list&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PostCard&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This page executes entirely on the server. The &lt;code&gt;getPosts()&lt;/code&gt; function reads from the filesystem, and the component tree renders to HTML without any client-side JavaScript. The returned HTML includes the complete blog list structure, ready for the browser to display.&lt;/p&gt;

&lt;p&gt;Individual post pages follow the same pattern. The server fetches post metadata and content, renders the Markdown or MDX to HTML, and sends the final document to the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPostBySlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wisp-cms/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MDXContent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/MDXContent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&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="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPostBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MDXContent&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;generateStaticParams()&lt;/code&gt; function tells Next.js to pre-render every post at build time. When the build process runs, it fetches all posts, renders each one as static HTML, and outputs files that serve instantly when users request them. This is static site generation—every post becomes a pre-built HTML file.&lt;/p&gt;

&lt;p&gt;Static generation provides the best performance for blogs. The server does no work at request time. Users receive cached HTML files served by a CDN. Load times drop to single-digit milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown and MDX Processing
&lt;/h2&gt;

&lt;p&gt;Wisp supports both Markdown and MDX—a format that combines Markdown syntax with embedded React components. This distinction matters for blog content.&lt;/p&gt;

&lt;p&gt;Markdown files contain only text, headings, code blocks, and standard formatting. A Markdown processor converts this to HTML. This approach works well for most blog posts: the content is safe, fast to parse, and produces semantically clean HTML.&lt;/p&gt;

&lt;p&gt;MDX extends Markdown by allowing developers to embed React components directly in content files. This enables dynamic interactivity within posts: interactive code editors, data visualizations, embedded forms, and custom components that respond to user input.&lt;/p&gt;

&lt;p&gt;The trade-off exists in build complexity and client-side JavaScript. A post written in Markdown renders completely on the server—no client-side code is needed. A post written in MDX must include the React component definitions and any client state management those components require. This increases the JavaScript bundle size.&lt;/p&gt;

&lt;p&gt;For developer portfolios, the choice depends on the content style. Educational posts with code examples work fine in Markdown. Interactive posts that showcase specific functionality benefit from MDX components.&lt;/p&gt;

&lt;p&gt;Wisp handles both formats transparently. The blog template can include posts in either format in the same blog directory. The system detects the file type and processes it accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Building&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Smart&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Contract&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Auditor"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Implementing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;automated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;security&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;checks"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Building a Smart Contract Auditor&lt;/span&gt;

When building production smart contracts, security analysis must be systematic. This post walks through the architecture of an automated auditor.

&lt;span class="gu"&gt;## The Audit Pipeline&lt;/span&gt;

The auditor processes contracts in stages:
&lt;span class="p"&gt;
1.&lt;/span&gt; Lexical analysis tokenizes the Solidity code
&lt;span class="p"&gt;2.&lt;/span&gt; Syntax analysis builds an abstract syntax tree
&lt;span class="p"&gt;3.&lt;/span&gt; Semantic analysis performs type checking and flow analysis
&lt;span class="p"&gt;4.&lt;/span&gt; Pattern matching identifies known vulnerabilities

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

&lt;/div&gt;



&lt;p&gt;This Markdown file contains standard prose and code blocks. When Wisp processes it, the frontmatter metadata (title, description, published status) becomes accessible through the API. The Markdown content converts to HTML.&lt;/p&gt;

&lt;p&gt;An MDX equivalent could include interactive components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Building a Smart Contract Auditor&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Implementing automated security checks&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

import { CodeDifference } from '@/components/CodeDifference';

&lt;span class="gh"&gt;# Building a Smart Contract Auditor&lt;/span&gt;

&amp;lt;CodeDifference 
  before={vulnerableCode} 
  after={fixedCode}
/&amp;gt;

When building production smart contracts...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the &lt;code&gt;CodeDifference&lt;/code&gt; component renders interactively, allowing users to toggle between vulnerable and fixed versions of code. This requires React on the client to function, increasing the JavaScript bundle size.&lt;/p&gt;

&lt;p&gt;For independent developer portfolios, Markdown alone provides sufficient functionality in most cases. The focus should remain on clear technical writing rather than interactive components. Interactive elements distract from the content and increase maintenance burden—components must remain compatible with the blog template across framework versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dark Mode Implementation
&lt;/h2&gt;

&lt;p&gt;Dark mode has become standard for developer-focused applications. Implementing it properly requires handling both server-side and client-side rendering, persisting user preference, and ensuring all text and components maintain sufficient contrast in both themes.&lt;/p&gt;

&lt;p&gt;The Wisp Next.js template implements dark mode using CSS custom properties (variables) that change based on a theme class on the document root element. This approach separates theme logic from component styling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* styles/theme.css */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e5e7eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f3f4f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0a0a0a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2d2d2d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;code&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-border&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;Components reference these variables through standard CSS. This means a single CSS file controls theme colors across the entire application.&lt;/p&gt;

&lt;p&gt;The critical challenge with dark mode in Next.js is preventing flash of unstyled content (FOUC) during page load. When a user visits the site with dark mode enabled, the initial HTML sent by the server renders in light mode (the default). The JavaScript then loads, detects the user's preference, and switches to dark mode. The user sees a flash of light mode before dark mode appears.&lt;/p&gt;

&lt;p&gt;Solving this requires detecting the theme preference before rendering any content. Wisp's Next.js template uses a small script injected into the document head, before any styling or component rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; 
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script runs synchronously before the page renders. It checks localStorage for a stored theme preference. If no preference exists, it checks the system's color scheme preference using the media query API. Then it sets the theme attribute on the document root immediately.&lt;/p&gt;

&lt;p&gt;Because this script runs before any visual content renders, the correct theme applies from the start. No flash occurs.&lt;/p&gt;

&lt;p&gt;A client-side component handles theme switching when the user clicks a toggle button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/ThemeToggle.js&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeToggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggleTheme&lt;/span&gt; &lt;span class="o"&gt;=&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Toggle theme&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌙&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;☀️&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&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 component must only render after mounting (the &lt;code&gt;if (!mounted) return null&lt;/code&gt; check). This prevents server-side rendering the theme toggle in one state while the client expects another. The toggle updates both the DOM attribute and localStorage, so the preference persists across sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  RSS Feed Generation
&lt;/h2&gt;

&lt;p&gt;RSS feeds provide a subscription mechanism for blog readers. Developers often subscribe to relevant blogs using RSS readers, making RSS support valuable for reaching technical audiences.&lt;/p&gt;

&lt;p&gt;Wisp generates RSS feeds by collecting all published posts, formatting them according to RSS specification, and serving them from a well-known URL (typically &lt;code&gt;/feed.xml&lt;/code&gt; or &lt;code&gt;/rss.xml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;RSS feed generation happens at build time using a simple Node.js script. The script fetches all posts from Wisp, iterates through them, and generates valid RSS XML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/generate-rss.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wisp-cms/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateRSS&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rssItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&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;return&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;item&amp;gt;
      &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/title&amp;gt;
      &amp;lt;description&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/description&amp;gt;
      &amp;lt;link&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/link&amp;gt;
      &amp;lt;guid&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/guid&amp;gt;
      &amp;lt;pubDate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/pubDate&amp;gt;
    &amp;lt;/item&amp;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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;rss version="2.0"&amp;gt;
  &amp;lt;channel&amp;gt;
    &amp;lt;title&amp;gt;Your Blog Title&amp;lt;/title&amp;gt;
    &amp;lt;link&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/link&amp;gt;
    &amp;lt;description&amp;gt;Your blog description&amp;lt;/description&amp;gt;
    &amp;lt;language&amp;gt;en-us&amp;lt;/language&amp;gt;
    &amp;lt;lastBuildDate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/lastBuildDate&amp;gt;
    &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rssItems&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  &amp;lt;/channel&amp;gt;
&amp;lt;/rss&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feed.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RSS feed generated at /feed.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/'/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;apos;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;generateRSS&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script integrates into the Next.js build process by adding it to the &lt;code&gt;package.json&lt;/code&gt; build script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node scripts/generate-rss.js &amp;amp;&amp;amp; next build"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time the application builds, the RSS feed regenerates with the latest posts. The generated XML file sits in the public directory, where Next.js serves it as a static asset.&lt;/p&gt;

&lt;p&gt;RSS feeds require proper XML formatting and date handling. The script escapes special XML characters in post titles and descriptions to prevent invalid XML. It formats publication dates in RFC 2822 format (the RSS standard), using &lt;code&gt;toUTCString()&lt;/code&gt; to ensure consistency regardless of the server's timezone.&lt;/p&gt;

&lt;p&gt;For maximum discoverability, the blog layout template should include a link tag in the document head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/rss+xml"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/feed.xml"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Your Blog RSS Feed"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells feed readers where to find the RSS feed automatically. When readers visit the site, their reader application detects the feed link and offers to subscribe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages Over MDX-Based Systems
&lt;/h2&gt;

&lt;p&gt;Several blog systems use MDX as their primary content format, combining Markdown writing with embedded React components. While flexible, this approach introduces complexity that doesn't serve independent developer portfolios well.&lt;/p&gt;

&lt;p&gt;MDX-based systems require the entire React framework to ship to the browser for each post. Interactive components need client-side JavaScript to function, increasing bundle size. A post with a single interactive code block still requires loading the full React runtime and all component dependencies. This overhead accumulates across pages.&lt;/p&gt;

&lt;p&gt;The Wisp template keeps client-side JavaScript to a minimum. Most pages render entirely on the server. Only interactive elements like the theme toggle require client-side code. A simple blog post is pure HTML—no JavaScript executes on the user's browser.&lt;/p&gt;

&lt;p&gt;Build time differs significantly. MDX compilation adds processing overhead during the build. Every MDX file must be parsed, AST-transformed, and compiled. For a blog with 50-100 posts, this can extend build times to several seconds or more. Wisp's filesystem approach with standard Markdown processes faster, typically completing blog builds in under one second.&lt;/p&gt;

&lt;p&gt;Content migration becomes simpler with Markdown. If an independent developer needs to switch blog platforms in the future, Markdown files remain compatible across systems. MDX files tie content to the React ecosystem, making portability harder.&lt;/p&gt;

&lt;p&gt;For most independent developer blogs, the advantages of simplicity and performance outweigh the flexibility of embedded interactive components. Technical writing benefits from focused, readable prose and clear code examples rather than interactive distractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up a Blog with the Wisp Template
&lt;/h2&gt;

&lt;p&gt;Creating a new blog with Wisp and Next.js starts with scaffolding a project from the official template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app@latest my-blog &lt;span class="nt"&gt;--template&lt;/span&gt; https://github.com/wisp-cms/nextjs-blog-template
&lt;span class="nb"&gt;cd &lt;/span&gt;my-blog
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command clones the template repository, installs dependencies, and starts the development server. The template includes a &lt;code&gt;content/blog&lt;/code&gt; directory with example posts in Markdown format.&lt;/p&gt;

&lt;p&gt;Adding a new post requires creating a Markdown file in the &lt;code&gt;content/blog&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Understanding Async/Await in Solidity&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A guide to asynchronous patterns in smart contracts&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2024-01-15&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Understanding Async/Await in Solidity&lt;/span&gt;

Solidity smart contracts operate in a synchronous execution model...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wisp detects the new file immediately in development mode. Refreshing the browser shows the new post in the blog list. The post is accessible at &lt;code&gt;/blog/understanding-async-await-in-solidity&lt;/code&gt; (the slug is derived from the filename).&lt;/p&gt;

&lt;p&gt;For deployment, the typical approach is pushing the repository to a hosting service like Vercel, GitHub Pages, or a self-hosted server. The build process generates static HTML files for all posts and deploys them alongside the Next.js application code.&lt;/p&gt;

&lt;p&gt;Vercel provides the smoothest deployment experience for Next.js applications. Connecting a GitHub repository to Vercel enables automatic deployments: every push to the main branch triggers a build, runs the RSS generation script, and deploys the updated site. New blog posts published via a repository push go live within seconds.&lt;/p&gt;

&lt;p&gt;The combination of server components, static generation, and filesystem-based content storage makes Wisp blogs ideal for independent developers. The setup requires minimal configuration, scales to thousands of posts without performance degradation, and keeps deployment simple.&lt;/p&gt;




&lt;p&gt;For Web3 documentation needs or full-stack Next.js development work, I'm available for consulting. Visit my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
