<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pavel Espitia</title>
    <description>The latest articles on DEV Community by Pavel Espitia (@pavelespitia).</description>
    <link>https://dev.to/pavelespitia</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F337213%2Fb21fb081-ae15-4041-9ab6-829aea593a28.jpeg</url>
      <title>DEV Community: Pavel Espitia</title>
      <link>https://dev.to/pavelespitia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pavelespitia"/>
    <language>en</language>
    <item>
      <title>Oracle Manipulation: How a $392K Silo Finance Loss Happened in 2026</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:09:03 +0000</pubDate>
      <link>https://dev.to/pavelespitia/oracle-manipulation-how-a-392k-silo-finance-loss-happened-in-2026-2051</link>
      <guid>https://dev.to/pavelespitia/oracle-manipulation-how-a-392k-silo-finance-loss-happened-in-2026-2051</guid>
      <description>&lt;p&gt;While the nine-figure hacks of 2026 grabbed headlines, the smaller ones are more instructive because they are the bugs you and I might actually write. On April 3, 2026, lending protocol Silo Finance lost about $392,000 to a misconfigured oracle. No exotic attack. A price feed that lied, and a contract that believed it. Here is how oracle manipulation works and why it keeps draining protocols.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of every oracle bug
&lt;/h2&gt;

&lt;p&gt;A lending protocol needs to know what your collateral is worth. It asks an oracle. If the oracle can be fooled, the protocol can be fooled: borrow against collateral that is suddenly "worth" far more than it is, or get liquidated when it is suddenly "worth" far less.&lt;/p&gt;

&lt;p&gt;Every oracle exploit reduces to the same question: can the attacker move the number the contract trusts, cheaply enough that the manipulation costs less than the profit?&lt;/p&gt;

&lt;h2&gt;
  
  
  The classic mistake: pricing off a spot DEX pool
&lt;/h2&gt;

&lt;p&gt;The most common version reads the price directly from an AMM pool's reserves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getPrice() public view returns (uint256) {
    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
    return (reserve1 * 1e18) / reserve0; // spot price, right now
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the spot price at this instant. An attacker with a flash loan can swap a huge amount into the pool, move the reserves, call the function that reads this price, and unwind, all in one transaction. For the duration of that transaction the price is whatever they pushed it to. They borrow against the inflated value and walk away.&lt;/p&gt;

&lt;p&gt;The cost is the swap fees and the flash loan fee. The profit is the over-borrowed amount. When the pool is shallow, the cost is tiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TWAP helps and where it does not
&lt;/h2&gt;

&lt;p&gt;The standard defense is a time-weighted average price. Instead of the instant reserves, you read the price averaged over a window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Uniswap V3 style: average tick over `secondsAgo`
function getTwap(uint32 secondsAgo) public view returns (uint256) {
    uint32[] memory secondsAgos = new uint32[](2);
    secondsAgos[0] = secondsAgo;
    secondsAgos[1] = 0;
    (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
    int24 avgTick = int24((tickCumulatives[1] - tickCumulatives[0]) / int56(uint56(secondsAgo)));
    return tickToPrice(avgTick);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A flash loan cannot move a multi-minute average in one transaction, because the average includes blocks the attacker does not control. That kills the single-transaction flash-loan version.&lt;/p&gt;

&lt;p&gt;But TWAP is not a magic word. Two things still bite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A short window.&lt;/strong&gt; A 30-second TWAP on a shallow pool can still be moved by an attacker willing to hold the position across a couple of blocks. Longer windows are safer but lag real price, which creates its own risk during fast moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A thin pool.&lt;/strong&gt; TWAP averages the manipulation; it does not prevent it. On a low-liquidity pair, sustained manipulation across the window is affordable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "misconfigured" usually means
&lt;/h2&gt;

&lt;p&gt;When a post-mortem says "misconfigured oracle," it is rarely that the team did not know what an oracle is. It is usually one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They used a robust oracle (like Chainlink) for the main asset but a spot DEX price for a newer or thinner asset, and the attacker hit the weak one.&lt;/li&gt;
&lt;li&gt;They set a TWAP window too short for the pool's liquidity.&lt;/li&gt;
&lt;li&gt;They did not validate the oracle's freshness, so a stale price from a paused or laggy feed was treated as current.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The freshness check is the one people forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(price &amp;gt; 0, "bad price");
require(block.timestamp - updatedAt &amp;lt; MAX_STALENESS, "stale price"); // often missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I audit for this
&lt;/h2&gt;

&lt;p&gt;When I review a protocol that prices assets, I trace every price read back to its source and ask three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can a single transaction move this number?&lt;/strong&gt; If it reads spot reserves, yes. Flag it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the averaging window appropriate for the pool's depth?&lt;/strong&gt; A 30-second TWAP on a $50K pool is not safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is staleness checked?&lt;/strong&gt; If the code trusts a feed without checking &lt;code&gt;updatedAt&lt;/code&gt;, a paused feed becomes an attack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a place where AI-assisted review genuinely helps, because the vulnerability spans multiple functions and requires reasoning about economics, not just syntax. The prompt I use asks the model to "trace each price source, determine whether it is manipulable within one transaction or one block, and estimate the cost of manipulation relative to the protocol's borrow limits." That economic framing is what separates "I see a &lt;code&gt;getReserves&lt;/code&gt; call" from "this $50K pool gates a $1M borrow cap, which is exploitable."&lt;/p&gt;

&lt;p&gt;The Silo loss was small by 2026 standards, but the lesson scales: the protocol is only as honest as the cheapest number it trusts. Find the cheapest number, and you have found the bug.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>solidity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built a Coding Agent With the Manual Tool-Use Loop. Here's What It Taught Me</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Sun, 05 Jul 2026 15:20:02 +0000</pubDate>
      <link>https://dev.to/pavelespitia/i-built-a-coding-agent-with-the-manual-tool-use-loop-heres-what-it-taught-me-4gjn</link>
      <guid>https://dev.to/pavelespitia/i-built-a-coding-agent-with-the-manual-tool-use-loop-heres-what-it-taught-me-4gjn</guid>
      <description>&lt;p&gt;Everyone reaches for the SDK's tool runner because it hides the agentic loop. I wrote the loop by hand instead, for a small tool that edits files based on instructions. It was more code, and it taught me exactly what an agent is doing under the hood, which paid off the first time I needed human approval before a destructive action. Here is the manual loop, and what controlling it directly buys you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the loop actually is
&lt;/h2&gt;

&lt;p&gt;An "agent" sounds mysterious. It is a while loop. The model responds, and either it is done (&lt;code&gt;end_turn&lt;/code&gt;) or it wants to call a tool (&lt;code&gt;tool_use&lt;/code&gt;). If it wants a tool, you run the tool, feed the result back, and call the model again. Repeat until done.&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&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;client&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;Anthropic&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;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&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="cm"&gt;/* your tool defs */&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;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageParam&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adaptive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;output_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;xhigh&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;messages&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end_turn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Preserve the assistant turn, including tool_use blocks&lt;/span&gt;
  &lt;span class="nx"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolUses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ToolUseBlock&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&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="s2"&gt;tool_use&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;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ToolResultBlockParam&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;toolUses&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;output&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;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire thing. The tool runner does this for you. Writing it yourself means you own every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two rules that took me longest to internalize
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Always append the full &lt;code&gt;response.content&lt;/code&gt;, not just the text.&lt;/strong&gt; The assistant turn contains the &lt;code&gt;tool_use&lt;/code&gt; blocks, and the next request needs them so the &lt;code&gt;tool_result&lt;/code&gt; blocks can be matched by &lt;code&gt;tool_use_id&lt;/code&gt;. If you strip to just text, the model has no record it asked for a tool, and the conversation breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every &lt;code&gt;tool_result&lt;/code&gt; needs the matching &lt;code&gt;tool_use_id&lt;/code&gt;.&lt;/strong&gt; The model fired N tool calls; you return N results, each tagged with the id it answers. Send them all in one user message. Mismatch the ids and you get a 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I did not use the tool runner
&lt;/h2&gt;

&lt;p&gt;The tool runner is great until you need to do something &lt;em&gt;between&lt;/em&gt; the model deciding to call a tool and the tool actually running. For my file editor, I wanted a human to approve any write before it happened. The manual loop makes that a one-line insert:&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="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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;toolUses&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write_file&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;approved&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;askHuman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Write to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&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="s2"&gt;? (y/n)`&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;approved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;results&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User denied the write. Suggest an alternative.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;is_error&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="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="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&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;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;results&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&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;Notice the denial goes back to the model as a tool result with &lt;code&gt;is_error: true&lt;/code&gt; and a message. The model reads "user denied the write" and adapts, maybe proposing a different file or asking why. With the tool runner hiding the loop, inserting that gate is awkward. With the manual loop it is obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gates worth adding
&lt;/h2&gt;

&lt;p&gt;Once you own the loop, the natural gates appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Approval&lt;/strong&gt; for destructive or external actions (writes, sends, deletes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt; of every tool call and result, which is gold for debugging why an agent did something weird.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A turn limit&lt;/strong&gt;, so a confused agent does not loop forever burning tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional execution&lt;/strong&gt;, like "skip this tool call if we already ran it this session."
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;turns&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_TURNS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&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;turns&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_TURNS&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hit turn limit, agent may be stuck&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to go back to the runner
&lt;/h2&gt;

&lt;p&gt;I am not saying always hand-roll. For an agent with no approval gates, no special logging, and tools that are all safe to auto-run, the tool runner is less code and just as correct. The decision is whether you need to intervene between decision and action. If yes, manual loop. If no, runner.&lt;/p&gt;

&lt;p&gt;What writing it by hand gave me, beyond the approval gate, was a real mental model. When an agent misbehaves now, I do not think "the magic broke." I think "the model returned a tool_use I did not handle" or "I fed back a malformed result." The loop is small enough to hold in your head, and holding it in your head is worth the extra code at least once.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Proxy Contracts and Storage Collisions: The Upgrade That Corrupts Your State</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Sat, 04 Jul 2026 15:14:47 +0000</pubDate>
      <link>https://dev.to/pavelespitia/proxy-contracts-and-storage-collisions-the-upgrade-that-corrupts-your-state-31eh</link>
      <guid>https://dev.to/pavelespitia/proxy-contracts-and-storage-collisions-the-upgrade-that-corrupts-your-state-31eh</guid>
      <description>&lt;p&gt;Upgradeable contracts are everywhere in DeFi, and they hide a category of bug that has nothing to do with the logic you wrote and everything to do with where Solidity puts your variables. Storage collisions corrupt state silently: no revert, no error, just a balance that is suddenly an address or an owner that is suddenly a number. Here is how proxies store data, how collisions happen, and how to avoid them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a proxy works in 30 seconds
&lt;/h2&gt;

&lt;p&gt;An upgradeable contract is actually two contracts. The proxy holds the &lt;em&gt;state&lt;/em&gt; (the storage) and the address of the &lt;em&gt;implementation&lt;/em&gt;. When you call the proxy, it &lt;code&gt;delegatecall&lt;/code&gt;s into the implementation. The implementation's code runs, but it runs against the &lt;em&gt;proxy's&lt;/em&gt; storage.&lt;/p&gt;

&lt;p&gt;That last part is the whole source of the danger. The logic contract's code reads and writes storage slots, but those slots belong to the proxy. So the proxy and every implementation must agree, exactly, on what lives in each storage slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Solidity puts variables
&lt;/h2&gt;

&lt;p&gt;Solidity assigns state variables to storage slots in declaration order, starting 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;contract V1 {
    address public owner;    // slot 0
    uint256 public total;    // slot 1
    mapping(address =&amp;gt; uint256) public balances; // slot 2
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;owner&lt;/code&gt; is slot 0, &lt;code&gt;total&lt;/code&gt; is slot 1, and so on. The implementation reads slot 0 expecting an address. As long as that is true, everything works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The collision: reordering on upgrade
&lt;/h2&gt;

&lt;p&gt;Now you ship V2 and, innocently, reorder the variables or insert a new one at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract V2 {
    uint256 public total;    // slot 0  ← was address owner!
    address public owner;    // slot 1  ← was uint256 total!
    mapping(address =&amp;gt; uint256) public balances; // slot 2
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You did not change the proxy's storage. The proxy still has the old &lt;code&gt;owner&lt;/code&gt; address sitting in slot 0. But V2's code now reads slot 0 as &lt;code&gt;total&lt;/code&gt;, a &lt;code&gt;uint256&lt;/code&gt;. So &lt;code&gt;total&lt;/code&gt; is now the numeric value of the old owner's address, and &lt;code&gt;owner&lt;/code&gt; is whatever number used to be &lt;code&gt;total&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No revert. The contract runs. It is just operating on garbage. An attacker who notices can often exploit the corrupted &lt;code&gt;owner&lt;/code&gt; slot to take control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that prevent it
&lt;/h2&gt;

&lt;p&gt;The fix is discipline about storage layout across versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never reorder existing variables.&lt;/strong&gt; Their slots are fixed forever once deployed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never change a variable's type&lt;/strong&gt; in a way that changes its slot size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only append new variables&lt;/strong&gt; at the end, after all existing ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never remove a variable.&lt;/strong&gt; Leave it (you can rename it to &lt;code&gt;deprecated_x&lt;/code&gt; for clarity), or its slot gets reused by the next variable and you have a collision.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract V2 {
    address public owner;    // slot 0, unchanged
    uint256 public total;    // slot 1, unchanged
    mapping(address =&amp;gt; uint256) public balances; // slot 2, unchanged
    uint256 public feeRate;  // slot 3, NEW, appended at the end. Safe.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Storage gaps and namespaced storage
&lt;/h2&gt;

&lt;p&gt;Two patterns make this safer. The older one is a storage gap: reserve empty slots in a base contract so child contracts have room to add variables without colliding with the next contract in the inheritance chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract Base {
    address public owner;
    uint256[49] private __gap; // reserved slots for future variables
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The modern one, ERC-7201 namespaced storage, sidesteps the problem by putting each module's storage at a hashed, collision-resistant slot rather than packing everything from slot 0. If you are starting a new upgradeable contract in 2026, prefer namespaced storage; it makes whole classes of collision structurally impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I check for it in an audit
&lt;/h2&gt;

&lt;p&gt;When I review an upgradeable contract, the implementation logic is only half the job. The other half is comparing the storage layout of the new version against the deployed one. Foundry and Hardhat both have tooling that dumps the storage layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;forge inspect V2 storageLayout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I diff that against V1's layout and look for any variable whose slot changed. A changed slot for an existing variable is a collision, full stop. The check is mechanical, which is exactly why it gets skipped under deadline pressure, and exactly why it bites.&lt;/p&gt;

&lt;p&gt;The unsettling part of storage collisions is that your code can be perfect and your upgrade still corrupts everything, because the bug is in the layout, not the logic. Treat the storage layout as a contract in its own right: append-only, never reordered, diffed on every upgrade. The compiler will not warn you. You have to look.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>solidity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Effort Levels in Practice: I Benchmarked low Through max on Real Tasks</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Fri, 03 Jul 2026 15:56:37 +0000</pubDate>
      <link>https://dev.to/pavelespitia/effort-levels-in-practice-i-benchmarked-low-through-max-on-real-tasks-7lf</link>
      <guid>https://dev.to/pavelespitia/effort-levels-in-practice-i-benchmarked-low-through-max-on-real-tasks-7lf</guid>
      <description>&lt;p&gt;The current Claude models give you an &lt;code&gt;effort&lt;/code&gt; knob with five settings: &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;. The docs tell you what each is for. I wanted numbers, so I ran the same three real tasks across all five levels and measured tokens, latency, and quality. The results changed how I set effort, and one of them surprised me. Here is the data and what I do with it now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What effort controls
&lt;/h2&gt;

&lt;p&gt;Effort is not just "how much the model thinks." It controls overall token spend: how much it thinks &lt;em&gt;and&lt;/em&gt; how it acts. Lower effort means fewer, more consolidated tool calls, less preamble, terser output. Higher effort means more exploration before answering. The default is &lt;code&gt;high&lt;/code&gt; if you omit it.&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;response&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;thinking&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adaptive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;output_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// the knob&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The three tasks
&lt;/h2&gt;

&lt;p&gt;I picked tasks that span the range of what I actually do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classification&lt;/strong&gt;: label a contract finding as low/medium/high/critical. Short, scoped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code generation&lt;/strong&gt;: write a TypeScript function with edge-case handling. Medium difficulty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-step audit&lt;/strong&gt;: analyze a 200-line contract for vulnerabilities across functions. Hard, agentic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I ran each at all five effort levels, three times, and averaged. I scored quality against a known-correct answer for tasks 1 and 3, and by manual review for task 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Task 1, classification.&lt;/strong&gt; Quality was flat across every effort level. The right label is the right label, and the model nailed it at &lt;code&gt;low&lt;/code&gt; just as well as at &lt;code&gt;max&lt;/code&gt;. But token usage climbed steeply: &lt;code&gt;max&lt;/code&gt; used roughly 8x the tokens of &lt;code&gt;low&lt;/code&gt; for an identical answer. Latency tracked tokens.&lt;/p&gt;

&lt;p&gt;The lesson: for genuinely simple, scoped tasks, high effort is pure waste. I set classification to &lt;code&gt;low&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task 2, code generation.&lt;/strong&gt; Quality improved from &lt;code&gt;low&lt;/code&gt; to &lt;code&gt;high&lt;/code&gt;, then plateaued. At &lt;code&gt;low&lt;/code&gt; the model sometimes skipped an edge case. At &lt;code&gt;high&lt;/code&gt; it caught them. &lt;code&gt;xhigh&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; produced essentially the same code as &lt;code&gt;high&lt;/code&gt; but spent more tokens getting there. Sweet spot: &lt;code&gt;high&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task 3, the multi-step audit. This is the one that surprised me.&lt;/strong&gt; I expected token usage to climb monotonically with effort, like task 1. Instead, total tokens were &lt;em&gt;lower&lt;/em&gt; at &lt;code&gt;xhigh&lt;/code&gt; than at &lt;code&gt;medium&lt;/code&gt; for this task. At &lt;code&gt;medium&lt;/code&gt;, the model explored less per step, took more turns, hit some dead ends, and re-derived things. At &lt;code&gt;xhigh&lt;/code&gt;, it planned better up front and finished in fewer turns. Higher per-step effort, fewer steps, lower total cost. And the quality was clearly best at &lt;code&gt;xhigh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counterintuitive bit
&lt;/h2&gt;

&lt;p&gt;I had been treating effort as a cost dial: turn it up, pay more. For one-shot tasks, that holds. For multi-step agentic work, it does not. Higher effort can &lt;em&gt;reduce&lt;/em&gt; total cost because better planning means fewer wasted turns. The relationship is not monotonic once a feedback loop is involved.&lt;/p&gt;

&lt;p&gt;That matches what Anthropic says about the agentic-coding default being &lt;code&gt;xhigh&lt;/code&gt;. I had read it as "they want the best quality regardless of cost." After the benchmark, I read it as "for agentic work, xhigh is often cheaper &lt;em&gt;and&lt;/em&gt; better." Both, really.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I set effort now
&lt;/h2&gt;

&lt;p&gt;A per-call-site decision, not a global default:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task type&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Classification, routing, extraction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quality flat, tokens scale, no reason to pay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-shot code or content&lt;/td&gt;
&lt;td&gt;&lt;code&gt;high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quality plateaus here; higher is waste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic loops, multi-step audits&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xhigh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Better planning, fewer turns, often cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness over everything, rare&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only when a wrong answer costs more than the tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Run your own
&lt;/h2&gt;

&lt;p&gt;My numbers are for my tasks. Yours will differ. The point is not to copy my table; it is to stop guessing. Pick three representative tasks, run them across the five levels, and measure tokens and quality. It took me an afternoon and saved me from two wrong defaults: paying high effort for classification (waste) and being scared of high effort on agentic work (wrong, it was cheaper).&lt;/p&gt;

&lt;p&gt;The knob is cheap to test and expensive to leave on the wrong setting. Measure it once.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Reading a Verified Contract You Didn't Write: A Systematic Approach</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Thu, 02 Jul 2026 14:53:40 +0000</pubDate>
      <link>https://dev.to/pavelespitia/reading-a-verified-contract-you-didnt-write-a-systematic-approach-54d4</link>
      <guid>https://dev.to/pavelespitia/reading-a-verified-contract-you-didnt-write-a-systematic-approach-54d4</guid>
      <description>&lt;p&gt;Opening a 600-line Solidity contract you have never seen is intimidating. Where do you even start? Over time I developed a reading order that turns the wall of code into something I can reason about in an hour instead of a day. It is the same order whether I am auditing for vulnerabilities, integrating against the contract, or just trying to understand a protocol. Here it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't read top to bottom
&lt;/h2&gt;

&lt;p&gt;The instinct is to read the file from line 1. That is the worst approach, because the important parts (who can do what, where money moves) are scattered and the top is usually imports and boilerplate. You burn your attention before you reach anything that matters.&lt;/p&gt;

&lt;p&gt;Read in order of &lt;em&gt;power&lt;/em&gt;, not order of appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: find the money
&lt;/h2&gt;

&lt;p&gt;Before anything else, I search for every place value moves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transfer, transferFrom, send, call{value:, safeTransfer, mint, burn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the lines where the contract's assets change hands. Everything else exists to gate these. If I understand who can trigger each of these and under what conditions, I understand the contract's risk surface. So I list them first and treat them as the destinations I am working backward from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: map the access control
&lt;/h2&gt;

&lt;p&gt;Next I find every modifier and every &lt;code&gt;require&lt;/code&gt; that checks &lt;code&gt;msg.sender&lt;/code&gt; or a role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;onlyOwner, onlyRole, require(msg.sender ==, hasRole, _checkOwner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I build a small mental (or literal) table: function, who can call it, what it does. The functions that move money and have weak or missing access control are the first thing I look at hard. This is where the access-control bugs live, the single most common cause of hacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setFeeRecipient   → anyone        → sets where fees go   ⚠️
withdrawFees      → feeRecipient  → drains fee balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two minutes of this table and a privilege-escalation path (anyone sets themselves as recipient, then withdraws) jumps out, where reading line by line you would never connect the two functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: trace the state variables
&lt;/h2&gt;

&lt;p&gt;Now I look at the storage: what state exists, and which functions write it. The dangerous pattern is a critical variable (a balance, an owner, a price) that more than one function can write, especially if one of those functions is less protected than the others. State that can be set from an unexpected path is where logic bugs hide.&lt;/p&gt;

&lt;p&gt;For upgradeable contracts I also check the storage layout here, because a proxy that reorders variables corrupts state silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: external calls and the reentrancy question
&lt;/h2&gt;

&lt;p&gt;For every external call I found in step 1, I ask: does the contract update its state &lt;em&gt;before&lt;/em&gt; or &lt;em&gt;after&lt;/em&gt; the call? Checks-effects-interactions means state first, external call last. If a balance is decremented after a &lt;code&gt;.call&lt;/code&gt;, that is a reentrancy flag. I also note any &lt;code&gt;delegatecall&lt;/code&gt;, because that runs foreign code against this contract's storage and is a much sharper edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: the math and the edges
&lt;/h2&gt;

&lt;p&gt;Last, I look at the arithmetic. Division before multiplication (precision loss), decimal mismatches between tokens (USDC has 6, WETH has 18), array operations that could divide by zero on an empty array, and anything that assumes a value cannot be zero. These are quieter bugs but they are real, and they cluster in pricing and accounting code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI fits in this flow
&lt;/h2&gt;

&lt;p&gt;I do not hand the whole file to a model and say "find bugs." That produces a wall of maybe-findings with no priority. Instead I use the model to accelerate each step. After step 2, I ask it to "list every function that can move assets and the exact access control gating each one," which gives me the table faster than I can build it by hand. After step 4, I ask it to "trace each external call and report whether state is updated before or after it."&lt;/p&gt;

&lt;p&gt;The structure comes from me. The model fills in the structure faster than I can. That division of labor is the whole point of AI-assisted auditing: it is a force multiplier on a method, not a replacement for having one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;This order works because it follows the actual risk. Money first (the targets), then access control (the gates), then state (the levers), then external calls (the escape hatches), then math (the quiet errors). By the time I have done steps 1 and 2, I usually already have a hypothesis about where the contract is weak, and steps 3 through 5 confirm or kill it.&lt;/p&gt;

&lt;p&gt;A 600-line contract is not 600 lines of equal importance. Maybe 30 of them matter. The reading order is how you find those 30 fast.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>solidity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned Submitting a Chrome Extension to the Web Store</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:08:29 +0000</pubDate>
      <link>https://dev.to/pavelespitia/what-i-learned-submitting-a-chrome-extension-to-the-web-store-194l</link>
      <guid>https://dev.to/pavelespitia/what-i-learned-submitting-a-chrome-extension-to-the-web-store-194l</guid>
      <description>&lt;p&gt;I built Argus, an AI transaction firewall as a Chrome extension, and submitting it to the Web Store was its own small adventure separate from writing the code. The review process, the permissions justifications, the manifest gotchas: none of it is hard, but all of it is undocumented in the way that matters. Here is what I wish I had known before I hit submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extension, briefly
&lt;/h2&gt;

&lt;p&gt;Argus inspects Web3 transactions before you sign them and warns you if something looks dangerous: an unlimited approval, a drainer pattern, an address that does not match what the dApp claims. It is built with WXT (a modern extension framework) and TypeScript, with an LLM-backed analysis step behind a freemium proxy. The technical part was the part I knew how to do. The store submission was the part that surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manifest V3 is the only option now, and it changes your architecture
&lt;/h2&gt;

&lt;p&gt;If you are starting an extension in 2026, it is Manifest V3, period. The biggest practical consequence: no persistent background page. You get a service worker that the browser can kill at any time and restart on the next event. State does not survive.&lt;/p&gt;

&lt;p&gt;This bit me. I had assumed a long-lived background context where I could cache analysis results in memory. In MV3 that cache evaporates whenever the worker sleeps. The fix was to treat the service worker as stateless and put anything that needs to persist into &lt;code&gt;chrome.storage&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong assumption: this Map is gone when the worker sleeps&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&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;Analysis&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Right: persist to chrome.storage, which survives worker restarts&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;cacheAnalysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txHash&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;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Analysis&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`tx:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;txHash&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;result&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;getCached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txHash&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="nx"&gt;Analysis&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;data&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&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="s2"&gt;`tx:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;txHash&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;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`tx:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;txHash&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Design as if the background context can vanish between any two events, because it can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions are where reviews stall
&lt;/h2&gt;

&lt;p&gt;The store review scrutinizes permissions hard, and rightly so. Every permission you request, you have to justify in the submission form, in plain language, tied to a specific feature. Requesting a broad permission "just in case" is the fastest way to a rejection or a long back-and-forth.&lt;/p&gt;

&lt;p&gt;I went through my manifest and cut every permission I was not actively using. For the ones I kept, I wrote the justification before submitting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;storage&lt;/code&gt;: cache transaction analyses so we do not re-analyze the same transaction.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;activeTab&lt;/code&gt; instead of broad host permissions: only read the page the user is actively on, only when they invoke the extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle is least privilege, and the store enforces it. If you cannot write a one-sentence justification tying a permission to a feature, you do not need the permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy disclosure is not optional
&lt;/h2&gt;

&lt;p&gt;Because Argus sends transaction data to a backend for analysis, I had to disclose exactly what data leaves the user's machine and why. The store wants a privacy policy URL and a data-use declaration. "We send transaction details to our server for security analysis and do not retain them beyond the request" is the kind of specific, honest statement they want. Vague or missing disclosures get flagged.&lt;/p&gt;

&lt;p&gt;Write this for the user, not for the lawyer. The people reading it are deciding whether to trust your extension with their wallet activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review takes longer than you expect, plan for it
&lt;/h2&gt;

&lt;p&gt;I submitted and then waited. Review is not instant, and if they have a question about a permission or the privacy disclosure, that is another round trip. Build the wait into your launch timeline. I had a soft launch date in mind and the review queue did not care about it.&lt;/p&gt;

&lt;p&gt;The lesson: submit earlier than you think you need to, with the cleanest possible permission set and a clear privacy disclosure, so there is nothing to question.&lt;/p&gt;

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

&lt;p&gt;The code was 90% of the effort and 10% of the risk to launch. The submission was 10% of the effort and most of the launch risk, because a rejection there blocks everything. So front-load it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with MV3 and a stateless service-worker design.&lt;/li&gt;
&lt;li&gt;Request the minimum permissions and write the justification for each one as you add it, not at submission time.&lt;/li&gt;
&lt;li&gt;Write the privacy disclosure honestly and specifically.&lt;/li&gt;
&lt;li&gt;Submit with buffer time before any date you have promised anyone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is hard once you know it. All of it is friction if you discover it at submission time. Now you know it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>RAG for Code: Why Chunking by Function Beats Chunking by Lines</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:03:05 +0000</pubDate>
      <link>https://dev.to/pavelespitia/rag-for-code-why-chunking-by-function-beats-chunking-by-lines-njc</link>
      <guid>https://dev.to/pavelespitia/rag-for-code-why-chunking-by-function-beats-chunking-by-lines-njc</guid>
      <description>&lt;p&gt;I built a retrieval system over a codebase so an LLM could answer questions about it, and my first version was nearly useless. The problem was not the model or the embeddings. It was how I cut the code into chunks. Splitting source by line count shreds the very structure that makes code meaningful. Here is why function-aware chunking works so much better, and how to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach and why it fails
&lt;/h2&gt;

&lt;p&gt;The standard RAG tutorial says: split your documents into fixed-size chunks (say 500 tokens), embed each chunk, retrieve the closest ones to the query. For prose, fine. For code, this is destructive.&lt;/p&gt;

&lt;p&gt;A 500-token window does not respect function boundaries. You end up with chunks like "the last third of &lt;code&gt;transfer()&lt;/code&gt; and the first half of &lt;code&gt;approve()&lt;/code&gt;." Neither function is complete. The embedding represents a fragment that means nothing on its own, and when you retrieve it, you hand the model half a function with no signature and no context.&lt;/p&gt;

&lt;p&gt;My early system would confidently answer questions about functions it had only seen the middle of. The retrieval was the bottleneck, and the chunking was the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chunk by structure, not by size
&lt;/h2&gt;

&lt;p&gt;Code has natural units: functions, methods, classes, contracts. Those are the units a developer reasons about, so those are the units to chunk by. One function, one chunk. The chunk includes the full signature, the body, and ideally the doc comment above it.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CodeChunk&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="c1"&gt;// function or method name&lt;/span&gt;
  &lt;span class="nl"&gt;signature&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="c1"&gt;// full signature for context&lt;/span&gt;
  &lt;span class="nl"&gt;body&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="c1"&gt;// the complete function body&lt;/span&gt;
  &lt;span class="nl"&gt;filePath&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="c1"&gt;// where it lives&lt;/span&gt;
  &lt;span class="nl"&gt;startLine&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each chunk is a complete, meaningful thing. Retrieve it and the model gets a whole function it can reason about, with its name and signature intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting functions
&lt;/h2&gt;

&lt;p&gt;For Solidity or TypeScript, you can get a long way with a parser rather than regex. For TypeScript I use the compiler API or a tool like &lt;code&gt;ts-morph&lt;/code&gt;; for Solidity, a proper parser that gives you the AST. The point is to walk the syntax tree and emit one chunk per function-level node, rather than slicing the raw text.&lt;/p&gt;

&lt;p&gt;A simplified shape of the extractor:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Project&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="s2"&gt;ts-morph&lt;/span&gt;&lt;span class="dl"&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;chunkByFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;CodeChunk&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;project&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;Project&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSourceFileAtPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CodeChunk&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;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;fn&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFunctions&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSignature&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getDeclaration&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;getText&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="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;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;          &lt;span class="c1"&gt;// the whole function, intact&lt;/span&gt;
      &lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;startLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStartLineNumber&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;// also walk classes/methods the same way&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;chunks&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;Each function comes out whole. No more half-functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedding and retrieval, locally
&lt;/h2&gt;

&lt;p&gt;I run this entirely on a local model so a private codebase never leaves my machine. Ollama serves an embedding model; I embed each function chunk and store the vectors:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ollama&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="s2"&gt;ollama&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;ollama&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;Ollama&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;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="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;const&lt;/span&gt; &lt;span class="nx"&gt;r&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;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedding&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;I embed &lt;code&gt;${chunk.name}\n${chunk.signature}\n${chunk.body}&lt;/code&gt; so the function name and signature are part of the vector, not just the body. That makes name-based queries ("what does &lt;code&gt;withdraw&lt;/code&gt; do") retrieve well, because the name is in the embedded text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff in retrieval quality
&lt;/h2&gt;

&lt;p&gt;After switching to function chunks, the same questions that used to get fragmented, half-wrong answers got crisp ones. "How does this contract handle reentrancy in withdrawals?" now retrieves the &lt;em&gt;complete&lt;/em&gt; &lt;code&gt;withdraw&lt;/code&gt; function plus the modifier it uses, and the model can actually reason about the checks-effects-interactions order because it can see the whole thing.&lt;/p&gt;

&lt;p&gt;The model did not get smarter. The retrieval got honest. I was handing it complete units of meaning instead of arbitrary text windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small refinement: include callers
&lt;/h2&gt;

&lt;p&gt;One thing I added later: for a retrieved function, I also pull in the one-line signatures of functions that call it. That gives the model a sense of how the function is used without bloating the chunk. It is cheap context that often answers the follow-up question before it is asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;RAG quality is mostly retrieval quality, and retrieval quality is mostly chunking quality. The instinct to chunk by size comes from text-document tutorials, but code is not prose. It has structure, and that structure is exactly what carries the meaning. Chunk along the structure, embed the name and signature with the body, and run it locally if the code is private. The embeddings and the model were never the problem. The scissors were.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>ollama</category>
    </item>
    <item>
      <title>The Drift Protocol Hack: A Six-Month Social Engineering Operation</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Mon, 29 Jun 2026 15:19:00 +0000</pubDate>
      <link>https://dev.to/pavelespitia/the-drift-protocol-hack-a-six-month-social-engineering-operation-2ne1</link>
      <guid>https://dev.to/pavelespitia/the-drift-protocol-hack-a-six-month-social-engineering-operation-2ne1</guid>
      <description>&lt;p&gt;On April 1, 2026, attackers drained about $285 million from Drift Protocol on Solana. It was the second-largest exploit in Solana's history, and the post-mortem is the most important security reading of the year, because there was no exploit in the code. It was six months of patient social engineering against the people who held the admin keys. Here is the timeline and the uncomfortable lesson for everyone who builds in this space.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened, in order
&lt;/h2&gt;

&lt;p&gt;Drift confirmed the drain on April 1. TVL collapsed from $550 million to under $300 million within an hour. The laundering was aggressive, each bridging transaction moving hundreds of thousands or millions in USDC, faster and more aggressive than even the Bybit laundering of 2025.&lt;/p&gt;

&lt;p&gt;But the drain on April 1 was the &lt;em&gt;end&lt;/em&gt; of the operation, not the beginning. The post-mortem revealed it was a six-month campaign targeting the humans who controlled the admin keys. The attackers, linked to the Lazarus Group, did not find a bug in the Solana programs. They found a path to the keys, and they took six months to walk it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the pattern, not an exception
&lt;/h2&gt;

&lt;p&gt;Pair Drift with KelpDAO, the $292M LayerZero bridge hack two weeks later, which also traced to a developer being socially engineered six weeks before the drain. Two of the largest hacks of 2026, both human-targeted, both patient, both attributed to state-backed actors.&lt;/p&gt;

&lt;p&gt;The numbers back up the pattern. North Korea-linked actors accounted for 76% of crypto hack losses in the first months of 2026, up from 64% in 2025 and under 10% in 2020. Private-key compromise, not code exploits, is now the dominant loss vector. The attack moved up the stack: from the contract to the keys to the people.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "six months of social engineering" actually looks like
&lt;/h2&gt;

&lt;p&gt;It is not a single phishing email. State-backed campaigns against crypto teams have a recognizable shape, drawn from incident reports across the year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fake recruiter or collaborator builds a relationship over weeks.&lt;/li&gt;
&lt;li&gt;A "coding test" or "demo project" arrives as a repo. (I have written before about catching one of these aimed at me.)&lt;/li&gt;
&lt;li&gt;The repo, or a dependency it pulls in, runs code at install or build time that exfiltrates secrets.&lt;/li&gt;
&lt;li&gt;Or the target is socially engineered into approving a transaction, sharing a credential, or running a script on a machine that can reach a hot key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The throughline: the attacker exploits trust and time, two things no audit covers and no linter detects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable lesson for builders
&lt;/h2&gt;

&lt;p&gt;I find smart contract bugs for a living, and I have to be honest about the limits of that work. A flawless audit would not have saved Drift or KelpDAO. The code was not the weak point. The team was.&lt;/p&gt;

&lt;p&gt;That does not make security work pointless. It relocates the most important part of it. The questions that would have mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many independent humans must approve a privileged action? If the answer is one, you are one phished engineer away from a drain.&lt;/li&gt;
&lt;li&gt;Are admin keys in hardware, behind a time delay, with a circuit breaker? KelpDAO's 46-minute freeze cut the loss by $100M. Operational controls bought what code could not.&lt;/li&gt;
&lt;li&gt;Does the team treat every unsolicited repo, recruiter, and "quick favor" as a potential attack? Because in 2026, they are.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I changed personally
&lt;/h2&gt;

&lt;p&gt;After the year we have had, I tightened my own operational security, not my Solidity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unsolicited repos run only in a sandbox, never on a machine with any key or credential. I built a static scanner specifically because I got targeted by one of these lures.&lt;/li&gt;
&lt;li&gt;Secrets live in one place with strict permissions, loaded explicitly by the scripts that need them, never auto-exported into every shell. A malicious build in one project cannot slurp every key at once.&lt;/li&gt;
&lt;li&gt;I assume any "job opportunity" that leads with a coding test is a lure until proven otherwise. The friction of being paranoid is small. The cost of being wrong is $285 million if you are Drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where AI cuts both ways
&lt;/h2&gt;

&lt;p&gt;The same year that AI made me a faster auditor, reports note it is lowering the bar for exploit discovery, with automated reconnaissance scanning old and unverified contracts. Attackers use AI too. But the Drift and KelpDAO hacks are a reminder that the frontier of attack is not better code analysis. It is better social engineering, and AI helps there as well, generating more convincing personas and lures.&lt;/p&gt;

&lt;p&gt;The defense is not a tool. It is a culture that treats people and keys as the primary attack surface, because in 2026 they demonstrably are. Audit the contract. Then audit who can drain it, and how hard you have made their day if they get phished.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>career</category>
      <category>ai</category>
    </item>
    <item>
      <title>Token Counting Done Right: Stop Using tiktoken for Claude</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Sun, 28 Jun 2026 14:41:31 +0000</pubDate>
      <link>https://dev.to/pavelespitia/token-counting-done-right-stop-using-tiktoken-for-claude-383c</link>
      <guid>https://dev.to/pavelespitia/token-counting-done-right-stop-using-tiktoken-for-claude-383c</guid>
      <description>&lt;p&gt;I had a cost estimator that was wrong by 20%, and the reason was embarrassing: I was counting Claude tokens with &lt;code&gt;tiktoken&lt;/code&gt;, which is OpenAI's tokenizer. Different model, different tokenizer, different counts. If you are estimating Claude costs or context budgets with a borrowed tokenizer, your numbers are fiction. Here is how to count correctly, and where the wrong way bites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tiktoken is wrong for Claude
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tiktoken&lt;/code&gt; tokenizes for OpenAI models. Claude uses a different tokenizer. They do not agree on how text splits into tokens. On typical English prose, &lt;code&gt;tiktoken&lt;/code&gt; undercounts Claude tokens by roughly 15 to 20%. On code or non-English text, the gap is worse, because tokenizers diverge most on the inputs they were not each optimized for.&lt;/p&gt;

&lt;p&gt;So a "cost estimate" or "will this fit in context" check built on &lt;code&gt;tiktoken&lt;/code&gt; is systematically off. It told me a prompt was 8,000 tokens when Claude saw closer to 9,500. Multiply that across a busy day and the budget projection is meaningfully wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right way: the count_tokens endpoint
&lt;/h2&gt;

&lt;p&gt;Claude has a dedicated endpoint for this, and the SDK wraps it. Counts are model-specific, so you pass the same model you will use for inference:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&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;client&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;Anthropic&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contractSource&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;log&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;input_tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the real count Claude will charge for&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the actual count, from the actual tokenizer, for the actual model. No approximation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting a cost estimate
&lt;/h2&gt;

&lt;p&gt;Once you have the real input count, the cost math is straightforward. For Opus 4.8 at $5 per million input tokens:&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;tokens&lt;/span&gt; &lt;span class="o"&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;input_tokens&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;inputCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// $5/M for Opus 4.8 input&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;`Estimated input cost: $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputCost&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;4&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are deciding between tiers, the per-million rates that matter in 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input $/M&lt;/th&gt;
&lt;th&gt;Output $/M&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 4.8&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The count is the same per model only on the input side; remember output tokens dominate cost on generation-heavy tasks, and you do not know those until you run the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the model-specific drift
&lt;/h2&gt;

&lt;p&gt;One subtlety that surprised me: token counts changed between Claude model versions. The same input text produces a &lt;em&gt;higher&lt;/em&gt; count on Opus 4.7 than on Opus 4.6, because they count differently. So if you cached a token count from an older model and reused it, you would be wrong again, just less wrong than tiktoken.&lt;/p&gt;

&lt;p&gt;The fix is to never cache a count across a model change. Re-run &lt;code&gt;countTokens&lt;/code&gt; against the model you are actually using. Do not apply a blanket multiplier to convert between models; the divergence is not uniform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diffing a file across versions
&lt;/h2&gt;

&lt;p&gt;A handy pattern for "how many tokens did this change add" is to count both versions and subtract. The endpoint is stateless, so you just count each and diff:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&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="s2"&gt;node:child_process&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="s2"&gt;node: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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="kr"&gt;number&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;r&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;countTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&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;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git show HEAD:CLAUDE.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;after&lt;/span&gt; &lt;span class="o"&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;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CLAUDE.md&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="s2"&gt;utf8&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;`Delta: &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;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;before&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use this to keep an eye on system-prompt bloat. When a prompt creeps up by a few thousand tokens, that is real money on every cached-miss request, and the diff makes it visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The tokenizer is part of the model. Borrowing another model's tokenizer to estimate counts is like measuring in the wrong units and hoping the error cancels. It does not cancel; it compounds. Use &lt;code&gt;countTokens&lt;/code&gt; against the exact model, never reuse a count across model versions, and remember output tokens are the unknown that dominates generation cost. It is one API call, it is free, and it is the difference between a budget projection you can trust and one that is off by a fifth.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Detecting Supply-Chain Malware Without Running the Code</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Sat, 27 Jun 2026 14:40:14 +0000</pubDate>
      <link>https://dev.to/pavelespitia/detecting-supply-chain-malware-without-running-the-code-d9g</link>
      <guid>https://dev.to/pavelespitia/detecting-supply-chain-malware-without-running-the-code-d9g</guid>
      <description>&lt;p&gt;After I got targeted by a fake-job-interview repo designed to steal my keys, I built a scanner that checks a repository for supply-chain attacks without cloning, installing, or running any of it. The whole point is to find the malicious code statically, before it ever executes, because by the time you run &lt;code&gt;npm install&lt;/code&gt; it is already too late. Here is how static detection of these attacks works and what it looks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why static, and why before install
&lt;/h2&gt;

&lt;p&gt;The dangerous moment in a supply-chain attack is install or build time. A &lt;code&gt;postinstall&lt;/code&gt; script, a malicious dependency, a build step that runs arbitrary code. Once you run &lt;code&gt;npm install&lt;/code&gt;, that code has already executed with your shell's environment, including any secrets it can reach.&lt;/p&gt;

&lt;p&gt;So a scanner that runs the code to analyze it has already lost. The analysis has to be static: read the files, parse them, reason about them, and never execute a line. That constraint shapes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scanner looks for
&lt;/h2&gt;

&lt;p&gt;Three categories cover most of what I have seen in real lures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Build-time code execution.&lt;/strong&gt; The first thing I check is anything that runs during install or build:&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;// package.json scripts that fire automatically&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dangerousScripts&lt;/span&gt; &lt;span class="o"&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;preinstall&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="s2"&gt;install&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="s2"&gt;postinstall&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="s2"&gt;prepare&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;postinstall&lt;/code&gt; that runs an obfuscated script, downloads and executes a remote payload, or shells out to &lt;code&gt;curl | sh&lt;/code&gt; is the single biggest red flag. Legitimate packages occasionally use these hooks, but a &lt;code&gt;postinstall&lt;/code&gt; that fetches and runs remote code is almost never benign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Dependencies missing from the lockfile.&lt;/strong&gt; This is the subtle one, and it is how the attack that targeted me worked. The &lt;code&gt;package.json&lt;/code&gt; declares a dependency, but it is not in the lockfile, or the lockfile points a known package name at a malicious tarball URL. The intent is that you trust the familiar name in &lt;code&gt;package.json&lt;/code&gt; and never check what the lockfile actually resolves it to.&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;// Flag dependencies in package.json that the lockfile resolves&lt;/span&gt;
&lt;span class="c1"&gt;// to an unexpected registry or a direct tarball URL&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkResolutions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PackageJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lockfile&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="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;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&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;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packages&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;resolved&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;resolved&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://registry.npmjs.org/&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;flag&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; resolves to a non-registry URL: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resolved&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A package named like a popular library but resolved from a random URL is a classic typosquat or hijack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Obfuscation.&lt;/strong&gt; Malicious payloads are usually obfuscated to hide what they do and slip past a casual reader. So I look for the fingerprints of obfuscation: long hex or base64 string literals, dense &lt;code&gt;\x&lt;/code&gt; escape sequences, &lt;code&gt;eval&lt;/code&gt; of a decoded string, arrays of character codes assembled at runtime.&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;function&lt;/span&gt; &lt;span class="nf"&gt;looksObfuscated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;boolean&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;longHexString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{120,}[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;evalOfDecoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/eval&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;atob|Buffer&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;from|decode&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;charCodeArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/String&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;fromCharCode&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(\s&lt;/span&gt;&lt;span class="sr"&gt;*,&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;){20,}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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;longHexString&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;evalOfDecoded&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;charCodeArray&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;None of these is proof of malice on its own. Together, on a file that also has a &lt;code&gt;postinstall&lt;/code&gt; hook, they are damning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The analyzer core is pure and testable
&lt;/h2&gt;

&lt;p&gt;The most important architectural decision was keeping the analysis logic pure: it takes file contents as input and returns findings, with no I/O of its own. Fetching the repo is a separate layer. That separation means I can unit-test the analyzer against known-malicious and known-clean fixtures without any network or filesystem:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flags postinstall that pipes curl to sh&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="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;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;package.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MALICIOUS_FIXTURE&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;findings&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContainEqual&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="nf"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remote-code-execution-on-install&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;A security tool that I cannot test thoroughly is a security tool I do not trust. Pure functions make the testing trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI fits, carefully
&lt;/h2&gt;

&lt;p&gt;I do use an LLM as one layer, but not as the gate. The deterministic rules above catch the known patterns reliably. The model is for the judgment call on suspicious-but-not-obviously-malicious code: "this script downloads a config file and parses it, is that benign or a staged payload?" The model reasons about intent in a way regex cannot.&lt;/p&gt;

&lt;p&gt;But the model never executes anything either, and I never let it be the sole reason to pass or fail a repo. Deterministic rules first, model for nuance, human for the final call. The model is an advisor, not an authority, because a model can be talked out of a finding and a regex cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;The attack that targeted me relied on me trusting a friendly repo and running its install step. The entire defense is to break that chain: analyze before you install, statically, and treat install-time code execution, lockfile mismatches, and obfuscation as the three things most likely to hurt you. You do not need to run hostile code to know it is hostile. You need to read it before it gets the chance to run.&lt;/p&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Chatting With Any EVM Contract: How Scry Resolves Proxies and Unverified Bytecode</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Fri, 26 Jun 2026 15:03:11 +0000</pubDate>
      <link>https://dev.to/pavelespitia/chatting-with-any-evm-contract-how-scry-resolves-proxies-and-unverified-bytecode-26fc</link>
      <guid>https://dev.to/pavelespitia/chatting-with-any-evm-contract-how-scry-resolves-proxies-and-unverified-bytecode-26fc</guid>
      <description>&lt;p&gt;Scry lets you talk to any EVM smart contract in plain English. Point it at an address on six chains and ask "what does this do?" or "can I withdraw my funds?" The hard part is not the chat. It is getting a usable interface for a contract when the ABI is hidden behind a proxy, or when there is no verified source at all. Here is how the resolution pipeline works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy case, and why it is rare
&lt;/h2&gt;

&lt;p&gt;If a contract is verified on a block explorer, you fetch its ABI and you are done. The ABI tells you every function, its inputs, and its outputs, and you can build a chat interface on top of it. With the unified Etherscan V2 API, one key covers all the chains Scry supports, which simplifies the fetch considerably.&lt;/p&gt;

&lt;p&gt;The trouble is that "verified with a clean ABI" is the minority case for the contracts people actually want to inspect. Two things break it constantly: proxies and unverified bytecode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: the proxy hides the real interface
&lt;/h2&gt;

&lt;p&gt;Most serious protocols use upgradeable proxies. You query the address, the explorer hands you the &lt;em&gt;proxy's&lt;/em&gt; ABI, and the proxy's ABI is almost empty: a fallback function and an upgrade mechanism. The functions you actually care about (transfer, withdraw, the protocol logic) live in the implementation contract, which sits at a different address.&lt;/p&gt;

&lt;p&gt;So step one of resolution is detecting that you are looking at a proxy and following it to the implementation. The implementation address lives at a known storage slot for standard proxy patterns. For EIP-1967 proxies, it is a specific, deterministic slot:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPublicClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;http&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="s2"&gt;viem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// EIP-1967 implementation slot&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMPL_SLOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc&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;resolveImplementation&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="nx"&gt;proxyAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&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="s2"&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;raw&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStorageAt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proxyAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IMPL_SLOT&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// The slot holds the implementation address, right-aligned in 32 bytes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;impl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="s2"&gt;`0x&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="s2"&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;impl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x0000000000000000000000000000000000000000&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;impl&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;If the slot is non-zero, that is the implementation, and &lt;em&gt;that&lt;/em&gt; is the address whose ABI you fetch. Scry follows the proxy automatically so the user does not have to know the contract is upgradeable. There are other proxy patterns (UUPS, beacon, transparent), so the resolver checks several known slots before giving up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: no verified source at all
&lt;/h2&gt;

&lt;p&gt;Sometimes there is no verified source anywhere: not on the proxy, not on the implementation. You have bytecode and nothing else. This is where most tools stop and where Scry uses bytecode reconstruction.&lt;/p&gt;

&lt;p&gt;Even without source, the bytecode contains the function selectors: the first four bytes of the keccak hash of each function signature, which the contract uses to dispatch calls. A library like &lt;code&gt;whatsabi&lt;/code&gt; scans the bytecode, extracts those selectors, and reconstructs a partial ABI:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;whatsabi&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="s2"&gt;@shazow/whatsabi&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;reconstructAbi&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="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&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="s2"&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;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;whatsabi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autoload&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provider&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="c1"&gt;// resolve selectors against a signature database to recover names&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abi&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 selectors are just four-byte hashes, so on their own they are opaque. But many of them are known: &lt;code&gt;0xa9059cbb&lt;/code&gt; is &lt;code&gt;transfer(address,uint256)&lt;/code&gt;. Resolving the selectors against a public signature database recovers human-readable names for the common ones, and the rest are presented as raw selectors the user can still call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layering the resolution
&lt;/h2&gt;

&lt;p&gt;Put together, the pipeline is a cascade, trying the richest source first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it verified? Use the ABI. Done.&lt;/li&gt;
&lt;li&gt;Is it a proxy? Resolve the implementation, then go back to step 1 for that address.&lt;/li&gt;
&lt;li&gt;No verified source? Reconstruct the ABI from bytecode selectors.&lt;/li&gt;
&lt;li&gt;Resolve selectors against a signature database for readable names.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step degrades gracefully into the next. The user gets the best interface available for that contract, and Scry never just throws up its hands because a contract is unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the LLM comes in
&lt;/h2&gt;

&lt;p&gt;The resolved ABI is the interface; the LLM is the translator. With a function list in hand, the model maps the user's plain-English question ("can I get my money out?") to the relevant function (&lt;code&gt;withdraw&lt;/code&gt;), explains what it does, and tells the user what they would need to call it. The ABI gives the model a precise, structured surface to reason about, which is far more reliable than asking it to guess about an address from raw bytecode.&lt;/p&gt;

&lt;p&gt;That is the architecture: deterministic resolution to build the most complete interface possible, then the model on top to make it conversational. The intelligence is in the chat. The hard engineering is in never giving up on a contract just because someone forgot to verify it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>blockchain</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Foundry Fuzzing vs Invariant Testing: When Each One Finds the Bug</title>
      <dc:creator>Pavel Espitia</dc:creator>
      <pubDate>Thu, 25 Jun 2026 15:07:46 +0000</pubDate>
      <link>https://dev.to/pavelespitia/foundry-fuzzing-vs-invariant-testing-when-each-one-finds-the-bug-4cbf</link>
      <guid>https://dev.to/pavelespitia/foundry-fuzzing-vs-invariant-testing-when-each-one-finds-the-bug-4cbf</guid>
      <description>&lt;p&gt;Foundry gives you two automated testing tools that sound similar and find very different bugs: property fuzzing and invariant testing. I see people reach for one when they need the other and conclude "fuzzing didn't find anything" when the bug was never in fuzzing's reach. Here is the difference, with the kind of bug each one actually catches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fuzzing: random inputs to one function
&lt;/h2&gt;

&lt;p&gt;A fuzz test takes parameters, and Foundry throws hundreds of random values at them, looking for an assertion that breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function testFuzz_depositThenWithdraw(uint256 amount) public {
    amount = bound(amount, 1, 1e24);     // keep inputs in a sane range
    vault.deposit{value: amount}();
    vault.withdraw(amount);
    assertEq(address(vault).balance, 0); // property: deposit+withdraw nets to zero
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Foundry runs this with many random &lt;code&gt;amount&lt;/code&gt; values. If some value breaks the property (an off-by-one in the math, an overflow at a boundary, a precision loss when &lt;code&gt;amount&lt;/code&gt; is tiny), it finds the counterexample and shrinks it to the minimal failing input.&lt;/p&gt;

&lt;p&gt;Fuzzing is excellent at finding edge cases in a &lt;em&gt;single operation&lt;/em&gt;. The boundary value, the zero, the max, the value that triggers a rounding error. It is testing "for all inputs to this function, does this property hold?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Where fuzzing misses
&lt;/h2&gt;

&lt;p&gt;Fuzzing tests one call (or a fixed sequence you wrote). It does not explore &lt;em&gt;sequences&lt;/em&gt; of operations in an order you did not anticipate. The bugs that live in "deposit, then someone else withdraws, then you transfer, then you withdraw again" are sequence bugs, and a single-function fuzz test will never stumble into that ordering.&lt;/p&gt;

&lt;p&gt;That is exactly the class of bug that drains protocols: not a bad input to one function, but a bad &lt;em&gt;interleaving&lt;/em&gt; of several functions across several actors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invariant testing: random sequences across the whole system
&lt;/h2&gt;

&lt;p&gt;Invariant testing flips the model. Instead of fuzzing inputs to one function, Foundry calls &lt;em&gt;many&lt;/em&gt; functions in &lt;em&gt;random order&lt;/em&gt; with &lt;em&gt;random inputs&lt;/em&gt;, across the whole contract, and after every call it checks that a system-wide invariant still holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract VaultInvariants is Test {
    Vault vault;

    function setUp() public {
        vault = new Vault();
        targetContract(address(vault)); // Foundry calls its functions randomly
    }

    // This must be true after ANY sequence of operations
    function invariant_solvency() public view {
        assertGe(address(vault).balance, vault.totalDeposits());
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;invariant_solvency&lt;/code&gt; says: the vault's actual balance must always cover what it owes depositors. Foundry now runs thousands of random call sequences (deposits, withdrawals, transfers, in every order it can think of) and checks solvency after each one. If any sequence breaks solvency, you have found a bug that no single-function test would surface.&lt;/p&gt;

&lt;p&gt;This is how you catch the "withdraw twice through a reentrant path" or "the accounting drifts after this specific interleaving" bugs. The invariant is the property that should survive any history, and Foundry attacks the history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the invariant is the hard part
&lt;/h2&gt;

&lt;p&gt;Writing the test is easy. Choosing the right invariant is the skill. Good invariants are global truths about the system that should never be violated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solvency: assets held &amp;gt;= liabilities owed.&lt;/li&gt;
&lt;li&gt;Conservation: total supply equals the sum of balances.&lt;/li&gt;
&lt;li&gt;Monotonicity: a nonce only increases, never resets.&lt;/li&gt;
&lt;li&gt;Access: only authorized roles ever changed a privileged variable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A weak invariant (one that is trivially true) passes forever and tests nothing. A strong invariant (solvency) is where the real bugs hide, because it constrains the whole system at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handler contracts: making the random calls realistic
&lt;/h2&gt;

&lt;p&gt;Out of the box, invariant testing calls functions with fully random arguments, which often just reverts (you cannot withdraw from an account with no balance). To get useful coverage you write a &lt;em&gt;handler&lt;/em&gt;: a wrapper that makes the random calls plausible, tracking ghost state so withdrawals target accounts that actually have balances. The handler is what turns invariant testing from "everything reverts" into "realistic sequences that actually exercise the logic."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract Handler is Test {
    Vault vault;
    uint256 public ghostDeposited; // track what we've put in

    function deposit(uint256 amount) public {
        amount = bound(amount, 1, 1e22);
        vault.deposit{value: amount}();
        ghostDeposited += amount;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You point the invariant suite at the handler, and now the random sequences are sequences of &lt;em&gt;valid-ish&lt;/em&gt; operations, which is where real protocols actually break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb
&lt;/h2&gt;

&lt;p&gt;Reach for fuzzing when the bug would be a bad input to one operation: an overflow, a boundary, a precision error. Reach for invariant testing when the bug would be a bad &lt;em&gt;interaction&lt;/em&gt; between operations: a drained vault after a specific call ordering, accounting that drifts over a history.&lt;/p&gt;

&lt;p&gt;Most serious protocol bugs are interaction bugs, which is why invariant testing earns its keep. But it costs more to set up (the handler, the right invariant), so fuzzing is the cheaper first pass. Run both. Fuzz the functions, then constrain the system, and let Foundry try to violate the truth you said could never be violated.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>solidity</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
