<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://0xsoftboi.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://0xsoftboi.github.io/" rel="alternate" type="text/html" /><updated>2026-06-09T07:13:07+00:00</updated><id>https://0xsoftboi.github.io/feed.xml</id><title type="html">Tsolmondorj Natsagdorj</title><subtitle>Tsolmondorj Natsagdorj — security &amp; systems engineer working on cross-chain infrastructure and smart-contract security, with merged contributions to alloy and uutils/coreutils. Writing, open source, and selected work.</subtitle><author><name>Tsolmondorj Natsagdorj</name></author><entry><title type="html">Who audits the auditor?</title><link href="https://0xsoftboi.github.io/blog/who-audits-the-auditor/" rel="alternate" type="text/html" title="Who audits the auditor?" /><published>2026-06-09T18:00:00+00:00</published><updated>2026-06-09T18:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/who-audits-the-auditor</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/who-audits-the-auditor/"><![CDATA[<p>I built a small Solidity static-analysis tool — <code class="language-plaintext highlighter-rouge">scaudit</code>, a dependency-free scanner that reads a contract and flags reentrancy, <code class="language-plaintext highlighter-rouge">tx.origin</code> auth, unchecked low-level calls, unprotected <code class="language-plaintext highlighter-rouge">selfdestruct</code>, and a dozen other things, plus some gas hints. Writing the detectors is the easy, satisfying part. It’s also the part that means almost nothing, because a security tool’s entire worth is <strong>whether you can trust its verdict</strong> — and the way that trust dies isn’t a crash, it’s a quiet lie.</p>

<p>A security scanner fails in two directions, and both are silent:</p>

<ul>
  <li><strong>False negative</strong> — a green light on vulnerable code. The worst outcome, because it manufactures confidence. You ran the tool, it said clean, you shipped the reentrancy.</li>
  <li><strong>False positive</strong> — a red flag on safe code. Less dangerous but corrosive: enough of them and people stop reading the output, which converts every real finding into a false negative by way of fatigue.</li>
</ul>

<p>So the interesting phase of building this wasn’t the detectors. It was handing the finished detectors to a <em>different, stronger</em> model — one that hadn’t written them and had no stake in them looking good — with a single instruction: <strong>construct the contracts these miss, and the safe ones they wrongly flag.</strong></p>

<figure class="chart">
<svg viewBox="0 0 620 320" role="img" aria-labelledby="wa-t">
<title id="wa-t">The two ways a detector lies: false negatives and false positives</title>
<text class="c-label-sm" x="150" y="40" text-anchor="middle">contract is VULNERABLE</text>
<text class="c-label-sm" x="430" y="40" text-anchor="middle">contract is SAFE</text>
<text class="c-label-sm" x="40" y="120" text-anchor="middle" transform="rotate(-90 40 120)">tool: FLAG</text>
<text class="c-label-sm" x="40" y="240" text-anchor="middle" transform="rotate(-90 40 240)">tool: clean</text>
<rect class="c-box" x="70" y="60" width="240" height="110" rx="8" />
<text class="c-label-sm" x="190" y="112" text-anchor="middle">true positive</text>
<text class="c-label-sm" x="190" y="132" text-anchor="middle">✓ caught it</text>
<rect class="c-box" x="320" y="60" width="240" height="110" rx="8" />
<text class="c-label-sm" x="440" y="106" text-anchor="middle" fill="var(--accent)">FALSE POSITIVE</text>
<text class="c-label-sm" x="440" y="126" text-anchor="middle">cries wolf →</text>
<text class="c-label-sm" x="440" y="146" text-anchor="middle">tx.origin in a log; uint x = 5</text>
<rect class="c-box-accent c-fill-soft" x="70" y="180" width="240" height="110" rx="8" />
<text class="c-label-sm" x="190" y="226" text-anchor="middle" fill="var(--accent)">FALSE NEGATIVE</text>
<text class="c-label-sm" x="190" y="246" text-anchor="middle">manufactures confidence</text>
<text class="c-label-sm" x="190" y="266" text-anchor="middle">selfdestruct in fallback; returns(bool)</text>
<rect class="c-box" x="320" y="180" width="240" height="110" rx="8" />
<text class="c-label-sm" x="440" y="232" text-anchor="middle">true negative</text>
<text class="c-label-sm" x="440" y="252" text-anchor="middle">✓ stayed quiet</text>
</svg>
<figcaption>The audit lived entirely in the two off-diagonal cells — the lies. Every finding it returned was a contract that landed in the wrong box.</figcaption>
</figure>

<h2 id="what-it-found">What it found</h2>

<p>The detectors were heuristics — regex and brace-counting over source, no compiler, no AST — and heuristics break on structure. The auditor found exactly where:</p>

<ul>
  <li>A function’s <code class="language-plaintext highlighter-rouge">returns (bool)</code> clause was being parsed into its <strong>modifier list</strong>. So a function that returned a value looked, to the access-control detector, like it had a guarding modifier — and its findings were suppressed. <strong>False negatives, by punctuation.</strong></li>
  <li><code class="language-plaintext highlighter-rouge">receive()</code> and <code class="language-plaintext highlighter-rouge">fallback()</code> weren’t being segmented as functions at all, so a <code class="language-plaintext highlighter-rouge">selfdestruct</code> sitting in a fallback was simply invisible.</li>
  <li>A <code class="language-plaintext highlighter-rouge">nonReentrant</code> modifier silenced <em>unrelated</em> checks, because the engine treated “has any modifier” as “is access-controlled.”</li>
  <li>On the other side: <code class="language-plaintext highlighter-rouge">tx.origin</code> passed to an event got flagged as phishable auth (it’s a log), and <code class="language-plaintext highlighter-rouge">uint x = 5</code> got flagged as a reentrancy state-write (it’s a declaration). <strong>Crying wolf.</strong></li>
</ul>

<p>Each one became a regression fixture — a tiny contract that <em>must</em> trip the detector, or <em>must not</em>. The suite went from 29 tests to 46. And then I did the part the fleet can’t do for me: re-ran them myself, and pointed the CLI at a vulnerable fixture (one HIGH finding, correct line) and a clean one (no findings) to watch recall and precision with my own eyes.</p>

<h2 id="the-honest-footer">The honest footer</h2>

<p><code class="language-plaintext highlighter-rouge">scaudit</code> is a first-pass triage aid. It has no control-flow graph, no taint analysis, no inheritance resolution; it reads one file at a time; its access-control detector is high-false-positive by nature. <a href="https://github.com/crytic/slither">Slither</a> compiles the contract and reasons over an IR; Mythril runs symbolic execution; real coverage is fuzzing and formal verification. I put all of that in the <a href="https://github.com/0xSoftBoi/smart-contract-audit-framework">README</a> in plain words, because a tool that overstates its reach is just a better-dressed false negative.</p>

<p>But the structure is the point worth keeping. The same <a href="/blog/a-social-good-protocol-built-by-an-agent-fleet/">agent workflow</a> that built <code class="language-plaintext highlighter-rouge">scaudit</code> is, itself, this idea: a builder model and a separate, sharper auditor model that exists only to disbelieve it. The most useful thing you can do with a second model isn’t more output — it’s adversarial doubt aimed at the first one. Who audits the auditor? A different auditor, who was told to assume it’s lying.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="security" /><category term="static-analysis" /><category term="agents" /><category term="solidity" /><category term="tooling" /><summary type="html"><![CDATA[I built a Solidity static-analysis tool with an agent fleet. The phase that mattered wasn't writing the detectors — it was pointing a separate model at them and saying: lie detector, prove you lie.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/who-audits-the-auditor.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/who-audits-the-auditor.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">An arbitrage bot with no slippage is a sandwich</title><link href="https://0xsoftboi.github.io/blog/an-arb-bot-with-no-slippage-is-a-sandwich/" rel="alternate" type="text/html" title="An arbitrage bot with no slippage is a sandwich" /><published>2026-06-09T17:00:00+00:00</published><updated>2026-06-09T17:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/an-arb-bot-with-no-slippage-is-a-sandwich</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/an-arb-bot-with-no-slippage-is-a-sandwich/"><![CDATA[<p>A flash-loan arbitrage contract is a neat trick: borrow a pile of money you don’t have, use it to buy an asset cheap on one DEX and sell it dear on another, pay the loan back in the same transaction, and keep the difference. If the trade isn’t profitable, the whole thing reverts and you’re out nothing but gas. I had a 2021 hackathon skeleton of exactly this — an Aave <strong>v1</strong> contract in Solidity 0.5 with the borrowing wired up and no actual arb — and rebuilt it into a real Aave <strong>v3</strong> strategy.</p>

<p>The contract gets the genuinely hard part right. The part it left open is the one an arbitrage bot, of all things, should know about.</p>

<h2 id="the-hard-part-it-got-right">The hard part it got right</h2>

<p>The dangerous surface of a flash-loan receiver is the callback. Aave hands control to your <code class="language-plaintext highlighter-rouge">executeOperation</code> <em>in the middle of its own transaction</em>, holding your borrowed funds. Two things have to be true or you’re drained:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span><span class="p">(</span><span class="n">msg</span><span class="p">.</span><span class="n">sender</span> <span class="o">==</span> <span class="kt">address</span><span class="p">(</span><span class="n">POOL_CONTRACT</span><span class="p">),</span> <span class="s">"caller not pool"</span><span class="p">);</span>
<span class="nb">require</span><span class="p">(</span><span class="n">initiator</span> <span class="o">==</span> <span class="kt">address</span><span class="p">(</span><span class="nb">this</span><span class="p">),</span>          <span class="s">"bad initiator"</span><span class="p">);</span>
</code></pre></div></div>

<p>The first stops anyone but Aave from calling the callback directly. The second is subtler and more important: without it, <em>I</em> could call <code class="language-plaintext highlighter-rouge">flashLoanSimple</code> and name <strong>your</strong> contract as the receiver, and Aave would dutifully invoke your <code class="language-plaintext highlighter-rouge">executeOperation</code> with my parameters — running your logic with attacker-chosen routers. Binding <code class="language-plaintext highlighter-rouge">initiator == address(this)</code> means the loan had to originate from this contract’s own <code class="language-plaintext highlighter-rouge">executeArb</code>. Both guards are there, and the repayment is enforced before any profit is swept. That’s the stuff that usually gets these contracts emptied, and it was correct.</p>

<h2 id="the-part-it-didnt">The part it didn’t</h2>

<p>The swaps had no slippage protection. Each leg just took whatever the router gave:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">router</span><span class="p">.</span><span class="n">swapExactTokensForTokens</span><span class="p">(</span><span class="n">amountIn</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="p">...);</span>  <span class="c1">// amountOutMin = 0
</span></code></pre></div></div>

<p>Here’s why that’s the bug, and why it’s almost funny that it’s <em>this</em> contract: an arbitrage transaction sits in the public mempool announcing “I am about to do two profitable swaps.” A searcher reads it, front-runs to move the pool against you, lets your swaps execute at the worse price, and back-runs to collect. The contract built to harvest a price spread becomes the perfect victim of one — it broadcasts its intent and then accepts any fill. The arb bot is the sandwich filling.</p>

<p>And the <code class="language-plaintext highlighter-rouge">minProfit</code> check at the end doesn’t save you, which is the trap. Profit is measured <em>after both swaps</em>, so a sandwich that degrades each leg can still leave you above a loose <code class="language-plaintext highlighter-rouge">minProfit</code> while a searcher pockets the difference — or it pushes you to a tiny revert with nothing gained. You need a floor on <strong>each swap</strong> (<code class="language-plaintext highlighter-rouge">amountOutMin</code>) plus a <code class="language-plaintext highlighter-rouge">deadline</code>, not just a check on the final tally.</p>

<figure class="chart">
<svg viewBox="0 0 680 300" role="img" aria-labelledby="arb-t">
<title id="arb-t">A flash-loan arb cycle, and where an unprotected swap gets sandwiched</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<rect class="c-box" x="20" y="60" width="120" height="44" rx="6" />
<text class="c-label-sm" x="80" y="86" text-anchor="middle">borrow (flash)</text>
<line class="c-arrow" x1="142" y1="82" x2="174" y2="82" />
<rect class="c-box-accent c-fill-soft" x="176" y="60" width="140" height="44" rx="8" />
<text class="c-label-sm" x="246" y="80" text-anchor="middle">swap A (buy)</text>
<text class="c-label-sm" x="246" y="96" text-anchor="middle">amountOutMin = 0</text>
<line class="c-arrow" x1="318" y1="82" x2="350" y2="82" />
<rect class="c-box-accent c-fill-soft" x="352" y="60" width="140" height="44" rx="8" />
<text class="c-label-sm" x="422" y="80" text-anchor="middle">swap B (sell)</text>
<text class="c-label-sm" x="422" y="96" text-anchor="middle">amountOutMin = 0</text>
<line class="c-arrow" x1="494" y1="82" x2="526" y2="82" />
<rect class="c-box" x="528" y="60" width="130" height="44" rx="6" />
<text class="c-label-sm" x="593" y="82" text-anchor="middle">repay + profit?</text>
<rect class="c-box" x="176" y="150" width="100" height="36" rx="6" />
<text class="c-label-sm" x="226" y="172" text-anchor="middle" fill="var(--accent)">front-run</text>
<rect class="c-box" x="392" y="150" width="100" height="36" rx="6" />
<text class="c-label-sm" x="442" y="172" text-anchor="middle" fill="var(--accent)">back-run</text>
<line class="c-arrow" x1="226" y1="150" x2="240" y2="106" />
<line class="c-arrow" x1="428" y1="106" x2="442" y2="150" />
<text class="c-label-sm" x="334" y="172" text-anchor="middle">searcher's sandwich →</text>
<text class="c-label-sm" x="340" y="232" text-anchor="middle">With amountOutMin = 0, each public swap accepts any fill — the searcher moves the price,</text>
<text class="c-label-sm" x="340" y="254" text-anchor="middle">your swaps execute worse, and the final minProfit check can still pass. Bound each leg.</text>
</svg>
<figcaption>The callback guards (only-pool, initiator-bound) were correct; the unprotected swaps weren't. A per-leg <code>amountOutMin</code> + <code>deadline</code> is the fix — a trailing <code>minProfit</code> isn't enough.</figcaption>
</figure>

<h2 id="who-found-it">Who found it</h2>

<p>I didn’t, on the first pass. The rebuild came out of a <a href="/blog/a-social-good-protocol-built-by-an-agent-fleet/">model-tiered agent workflow</a> — one model writes the contract, a stronger one audits it before anything ships. The builder got the callback guards right and left the slippage at zero; the auditor flagged it medium-severity (“an arb with no <code class="language-plaintext highlighter-rouge">amountOutMin</code> is a free sandwich”), and the fix — per-leg <code class="language-plaintext highlighter-rouge">minOut1</code>/<code class="language-plaintext highlighter-rouge">minOut2</code> and a <code class="language-plaintext highlighter-rouge">deadline</code> — went in with a regression test. Then I re-ran all 17 tests myself and read the callback by hand, because a green check from the fleet is a claim, not a result.</p>

<p>The <a href="https://github.com/0xSoftBoi/marketmakehackathon">contract, the mocks, and the tests</a> are public. It’s tested against mock Aave and mock DEXes, not live ones — so it’s a faithful model of the strategy, not a deployed bot. But the lesson generalizes past flash loans: the protections you’re sure you don’t need are the ones for the attack your own code is <em>designed around</em>. An arb contract is a price-spread predator that forgot it trades in public.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="defi" /><category term="mev" /><category term="flashloan" /><category term="solidity" /><category term="security" /><summary type="html"><![CDATA[I rebuilt a 2021 flash-loan hackathon contract into a real Aave v3 arbitrage. The contract got the hard security right — and left the door open on the thing arb bots exist to exploit.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/an-arb-bot-with-no-slippage-is-a-sandwich.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/an-arb-bot-with-no-slippage-is-a-sandwich.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Running an OP Stack L2 with reth</title><link href="https://0xsoftboi.github.io/blog/running-an-op-stack-l2-with-reth/" rel="alternate" type="text/html" title="Running an OP Stack L2 with reth" /><published>2026-06-09T16:00:00+00:00</published><updated>2026-06-09T16:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/running-an-op-stack-l2-with-reth</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/running-an-op-stack-l2-with-reth/"><![CDATA[<p>An OP Stack rollup looks intimidating until you see that it’s four long-running processes wired to an L1, and one shared secret holding two of them together. I had a half-finished deployment of one — <code class="language-plaintext highlighter-rouge">op-stack-reth</code>, a docker-compose setup using <strong>reth</strong> as the execution client instead of the usual op-geth — and I finished it. The interesting parts were what “finishing” meant.</p>

<h2 id="the-four-processes-and-the-handshake">The four processes and the handshake</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>op-reth     execution layer — runs the EVM, holds L2 state, serves eth_* RPC
op-node     consensus layer — derives the L2 chain from L1, drives block production
op-batcher  posts L2 transaction batches down to L1 (as blobs)
op-proposer posts L2 state roots to L1 (so withdrawals can be proven)
</code></pre></div></div>

<p>The handshake is the part worth internalizing. <code class="language-plaintext highlighter-rouge">op-node</code> doesn’t execute transactions; <code class="language-plaintext highlighter-rouge">op-reth</code> doesn’t decide what the chain is. They talk over the <strong>Engine API</strong> — the same <code class="language-plaintext highlighter-rouge">engine_forkchoiceUpdated</code> / <code class="language-plaintext highlighter-rouge">engine_getPayload</code> interface Ethereum L1 uses between its consensus and execution clients — and that interface is authenticated with a <strong>JWT secret</strong> the two share. <code class="language-plaintext highlighter-rouge">op-node</code> says “build a block on top of this head”; <code class="language-plaintext highlighter-rouge">op-reth</code> builds and executes it; <code class="language-plaintext highlighter-rouge">op-node</code> says “this is now canonical.” That’s the whole dance. Everything else — the batcher, the proposer — is about getting that L2 chain <em>onto</em> L1 so it inherits Ethereum’s security.</p>

<figure class="chart">
<svg viewBox="0 0 680 350" role="img" aria-labelledby="op-t">
<title id="op-t">OP Stack topology: op-reth and op-node over the Engine API, batcher and proposer to L1</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-title" x="40" y="28">L2 — your rollup</text>
<rect class="c-box" x="40" y="44" width="180" height="64" rx="8" />
<text class="c-label-sm" x="130" y="70" text-anchor="middle">op-node</text>
<text class="c-label-sm" x="130" y="88" text-anchor="middle">consensus / derivation</text>
<rect class="c-box-accent c-fill-soft" x="300" y="44" width="180" height="64" rx="8" />
<text class="c-title" x="390" y="70" text-anchor="middle">op-reth</text>
<text class="c-label-sm" x="390" y="90" text-anchor="middle">execution (EVM, state)</text>
<line class="c-arrow" x1="222" y1="68" x2="298" y2="68" />
<line class="c-arrow" x1="298" y1="86" x2="222" y2="86" />
<text class="c-label-sm" x="260" y="36" text-anchor="middle" fill="var(--accent)">Engine API + JWT</text>
<rect class="c-box" x="40" y="150" width="180" height="44" rx="6" />
<text class="c-label-sm" x="130" y="176" text-anchor="middle">op-batcher → L1 (blobs)</text>
<rect class="c-box" x="300" y="150" width="180" height="44" rx="6" />
<text class="c-label-sm" x="390" y="176" text-anchor="middle">op-proposer → L1 (roots)</text>
<line class="c-arrow" x1="130" y1="108" x2="130" y2="150" />
<line class="c-arrow" x1="390" y1="108" x2="390" y2="150" />
<line class="c-grid" x1="40" y1="232" x2="640" y2="232" />
<text class="c-title" x="40" y="262">L1 — Ethereum (Sepolia / mainnet)</text>
<rect class="c-box" x="40" y="278" width="440" height="44" rx="6" />
<text class="c-label-sm" x="260" y="304" text-anchor="middle">batch inbox · output oracle / dispute game · bridge</text>
<line class="c-arrow" x1="130" y1="194" x2="130" y2="278" />
<line class="c-arrow" x1="390" y1="194" x2="390" y2="278" />
<line class="c-arrow" x1="560" y1="278" x2="560" y2="120" />
<text class="c-label-sm" x="560" y="200" text-anchor="middle">L1 →</text>
<text class="c-label-sm" x="560" y="218" text-anchor="middle">derivation</text>
<text class="c-label-sm" x="582" y="110" text-anchor="middle">op-node</text>
</svg>
<figcaption>op-node and op-reth share the Engine API (authenticated by a JWT); op-node also derives the chain from L1, while the batcher and proposer push data and state roots back down to L1.</figcaption>
</figure>

<h2 id="what-finishing-it-actually-meant">What “finishing it” actually meant</h2>

<p>The deployment mostly existed. What it lacked was everything that makes infra <em>trustworthy</em> rather than just present:</p>

<ul>
  <li><strong>Every image was <code class="language-plaintext highlighter-rouge">:latest</code>.</strong> That’s a time bomb — <code class="language-plaintext highlighter-rouge">docker compose pull</code> six months apart gives you two different rollups, and one of them won’t boot. I pinned all six (op-reth, op-node, batcher, proposer, prometheus, grafana) to released, env-overridable tags tracking <code class="language-plaintext highlighter-rouge">op-contracts v4.0.0</code>.</li>
  <li><strong>Genesis was a manual chore.</strong> The README said “generate <code class="language-plaintext highlighter-rouge">genesis.json</code> and <code class="language-plaintext highlighter-rouge">rollup.json</code> using the Optimism monorepo” — a clone-and-pray step. The modern answer is <strong>op-deployer</strong>: one tool that deploys the L1 contracts and emits both files. I wired it into a <code class="language-plaintext highlighter-rouge">make config</code> so the path from nothing to a configured chain is a single command.</li>
  <li><strong>A real config bug.</strong> In replica mode, reth’s <code class="language-plaintext highlighter-rouge">--rollup.sequencer-http</code> defaulted to the node’s <em>own</em> op-node. A replica is supposed to forward transactions to the <em>external</em> sequencer; pointing it at itself is a quiet footgun. Fixed.</li>
</ul>

<h2 id="the-part-that-caught-bugs-was-ci-not-me">The part that caught bugs was CI, not me</h2>

<p>Here’s the honest bit. I can’t run Docker in my environment, so I can’t boot the chain to prove it works — and I said so in the README, in a “what’s verified vs what needs Docker” section, rather than implying a green I didn’t earn. What I <em>could</em> do is make the repo verify itself: a <code class="language-plaintext highlighter-rouge">make validate</code> (script syntax, YAML, JSON) that runs anywhere, plus a CI workflow that adds <code class="language-plaintext highlighter-rouge">shellcheck</code>, <code class="language-plaintext highlighter-rouge">yamllint</code>, and crucially <code class="language-plaintext highlighter-rouge">docker compose config</code> on GitHub’s runners.</p>

<p>And the first thing CI did was fail — on <strong>my own scripts</strong>. <code class="language-plaintext highlighter-rouge">shellcheck</code> flagged the validation script I’d just written: an unguarded <code class="language-plaintext highlighter-rouge">cd</code>, a loop that could only run once, an unchecked <code class="language-plaintext highlighter-rouge">source</code>. <code class="language-plaintext highlighter-rouge">bash -n</code> had passed them happily; shellcheck didn’t. I fixed the three, pushed, and watched <code class="language-plaintext highlighter-rouge">docker compose config</code> validate the pinned compose file on a machine that actually had Docker — the one check I couldn’t run myself, now green.</p>

<p>That’s the whole reason to wire up CI on an infra repo you can’t fully exercise locally: it runs the checks your laptop can’t, and it has no investment in your code being correct. The <a href="https://github.com/0xSoftBoi/op-stack-reth">finalized deployment</a> boots a reth-powered OP Stack L2 in replica or sequencer mode; the green check next to it is the part I didn’t have to take on faith.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="optimism" /><category term="rollup" /><category term="reth" /><category term="ethereum" /><category term="devops" /><summary type="html"><![CDATA[An OP Stack rollup is four processes and a shared secret. I finalized an old deployment of mine into something reproducible — and the part that actually caught bugs wasn't me, it was CI.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/running-an-op-stack-l2-with-reth.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/running-an-op-stack-l2-with-reth.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rebuilding a perps DEX from its docs</title><link href="https://0xsoftboi.github.io/blog/rebuilding-a-perps-dex-from-its-docs/" rel="alternate" type="text/html" title="Rebuilding a perps DEX from its docs" /><published>2026-06-09T15:00:00+00:00</published><updated>2026-06-09T15:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/rebuilding-a-perps-dex-from-its-docs</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/rebuilding-a-perps-dex-from-its-docs/"><![CDATA[<p>I had a repo, <code class="language-plaintext highlighter-rouge">blastperps</code>, that was a perpetual-futures DEX. Except all that survived was a zipped Next.js frontend — a trade page, an earn page — sitting on top of a private SDK that no longer exists. The entire on-chain protocol was gone, and I didn’t have the source anymore.</p>

<p>The thing is, it shipped. It got rebranded to <a href="https://app.artura.finance">Artura Finance</a> and it’s live. So the protocol still exists <em>as documentation</em>, even though my copy of the code doesn’t. That’s enough to rebuild from — if you read the docs for the one thing that actually defines a perps DEX: <strong>who is the counterparty?</strong></p>

<h2 id="the-docs-tell-you-who-the-house-is">The docs tell you who the house is</h2>

<p>Artura’s marketing says “inspired by Hyperliquid.” Its <em>mechanics</em> say something much more specific. The liquidation rule is “you’re liquidated once you’ve lost 90% of your collateral.” The fees mention a “borrowing fee that goes into the vault.” LPs “earn from trader losses.” There’s a single vault, and traders trade against it.</p>

<p>That’s not an orderbook and it’s not an AMM. That’s the <strong><a href="https://gains.trade">Gains Network / gTrade</a> pool model</strong>: a single stablecoin vault is the <em>sole counterparty to every trade</em>. There’s no other trader on the other side of your long — there’s the LP pool, and it takes the opposite side of everything. When you win, the pool pays you. When you lose, the pool keeps it. The “earn” page in my old frontend wasn’t a staking gimmick; it was people <strong>funding the house</strong>.</p>

<p>Once you know that, the whole protocol falls out of it.</p>

<figure class="chart">
<svg viewBox="0 0 680 320" role="img" aria-labelledby="pp-t">
<title id="pp-t">The pool-perps money model: the LP vault is the counterparty to every trade</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<rect class="c-box-accent c-fill-soft" x="270" y="130" width="140" height="60" rx="10" />
<text class="c-title" x="340" y="156" text-anchor="middle">LP vault</text>
<text class="c-label-sm" x="340" y="176" text-anchor="middle">the counterparty</text>
<text class="c-title" x="60" y="40">Trader wins</text>
<rect class="c-box" x="40" y="54" width="150" height="40" rx="6" />
<text class="c-label-sm" x="115" y="78" text-anchor="middle">profit paid out</text>
<line class="c-arrow" x1="266" y1="140" x2="192" y2="84" />
<text class="c-label-sm" x="225" y="108" text-anchor="middle" fill="var(--accent)">LP NAV ↓</text>
<text class="c-title" x="510" y="40">Trader loses</text>
<rect class="c-box" x="490" y="54" width="150" height="40" rx="6" />
<text class="c-label-sm" x="565" y="78" text-anchor="middle">loss + fees kept</text>
<line class="c-arrow" x1="490" y1="84" x2="414" y2="140" />
<text class="c-label-sm" x="455" y="108" text-anchor="middle">LP NAV ↑</text>
<rect class="c-box" x="270" y="240" width="140" height="44" rx="6" />
<text class="c-label-sm" x="340" y="262" text-anchor="middle">borrowing fee +</text>
<text class="c-label-sm" x="340" y="277" text-anchor="middle">open/close fees</text>
<line class="c-arrow" x1="340" y1="240" x2="340" y2="192" />
<text class="c-label-sm" x="340" y="306" text-anchor="middle">LPs are the house: their NAV is the inverse of net trader PnL, plus the fees.</text>
</svg>
<figcaption>No orderbook, no AMM curve. Every long and short is taken by the vault; LP share price = vault assets ÷ shares, which moves opposite to traders and up with fees.</figcaption>
</figure>

<h2 id="the-rebuild">The rebuild</h2>

<p>Three contracts, in Foundry:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">ArturaVault</code></strong> — the LP vault, an ERC-20 share token (the “gToken”). Deposit the stablecoin, get pro-rata shares. Trader <strong>profit is paid out of here</strong>; trader <strong>losses and fees flow in</strong>. Share price is just <code class="language-plaintext highlighter-rouge">totalAssets / supply</code>, so it tracks net trader PnL exactly — and the vault can never pay out more than it holds (in the real protocol a backstop token mints to recapitalize; I left that as a documented seam).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">ArturaPerps</code></strong> — open/close leveraged longs and shorts against the vault. PnL is <code class="language-plaintext highlighter-rouge">collateral · leverage · (mark − entry) / entry</code>, priced off an oracle. A borrowing fee accrues over time to the vault. <strong>Liquidation is permissionless once equity drops to 10% of collateral</strong> — the gTrade signature.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">PriceFeed</code></strong> — keeper-pushed mark prices with a staleness guard. Artura’s docs don’t actually specify an on-chain staleness bound; I added one, because settling a trade on a stale price is how these get drained.</li>
</ul>

<p>The one number I could check against reality: Artura’s docs give a worked liquidation example — a 100× BTC long at $20,000 with $50 collateral liquidates at $19,824. My <code class="language-plaintext highlighter-rouge">liquidationPrice</code> view, with the same inputs and zero fees, returns $19,820; the $4 is their $1 borrowing-fee term. <a href="https://github.com/0xSoftBoi/blastperps/blob/main/test/Artura.t.sol">That’s a test</a>. Nine of them pass: long and short PnL settled against the vault (LP NAV moving the right way each time), the liquidation threshold, the borrowing fee, the caps, the stale-oracle revert, and the solvency floor.</p>

<h2 id="what-the-docs-dont-make-you-honest-about">What the docs don’t make you honest about</h2>

<p>This is the <em>core</em> money model, faithfully rebuilt and tested — not the whole of Artura. The backstop token that recapitalizes the vault, limit/stop orders, the OI-skew borrowing curve, referrals: all documented seams, none built. I wrote that down in the <a href="https://github.com/0xSoftBoi/blastperps">README</a> rather than letting “rebuilt the perps DEX” imply more than nine tests’ worth of protocol.</p>

<p>But the lesson that made the rebuild possible is the same one from the <a href="/blog/the-index-fund-that-held-the-wrong-asset/">index fund that held the wrong asset</a> and the <a href="/blog/the-bridge-that-paid-twice/">bridge that paid twice</a>: in DeFi, the design <em>is</em> the answer to one question about where the value sits. For a perps DEX that question is “who’s the counterparty,” and once the docs answered it — the LPs are — there was nothing left to guess.</p>

<p>Contracts, tests, and the recovered frontend: <a href="https://github.com/0xSoftBoi/blastperps">github.com/0xSoftBoi/blastperps</a>.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="defi" /><category term="perps" /><category term="solidity" /><category term="foundry" /><category term="security" /><summary type="html"><![CDATA[An old repo of mine had a perps-DEX frontend and nothing else — the protocol was gone. So I rebuilt it from the documentation of what it became. The interesting part is what the docs gave away: who the house is.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/rebuilding-a-perps-dex-from-its-docs.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/rebuilding-a-perps-dex-from-its-docs.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A social-good protocol, built by an agent fleet</title><link href="https://0xsoftboi.github.io/blog/a-social-good-protocol-built-by-an-agent-fleet/" rel="alternate" type="text/html" title="A social-good protocol, built by an agent fleet" /><published>2026-06-09T14:00:00+00:00</published><updated>2026-06-09T14:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/a-social-good-protocol-built-by-an-agent-fleet</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/a-social-good-protocol-built-by-an-agent-fleet/"><![CDATA[<p>One of my repos, <code class="language-plaintext highlighter-rouge">socialstack</code>, was empty: a 2021 license file and a README that said, in full, “socialstack frontend.” The reference I had was <a href="https://www.values.network">values.network</a> — a values-based social network where you go on <strong>missions</strong> (real action on causes: health, nature, local community, nonprofits) and earn a community reward token. Socialstack’s whole DNA is social tokens, so the reward is on-chain.</p>

<p>So I knew the <em>what</em>. The question was how to build it well, and I wanted to try something: instead of writing it all myself on one model, run a fleet — and be deliberate about which Claude model does which job.</p>

<h2 id="the-seating-chart">The seating chart</h2>

<p>The models aren’t interchangeable, and the price difference is real (Opus output is 5× Sonnet, which is 5× Haiku). The skill is matching the model to the <em>kind</em> of thinking the task needs:</p>

<ul>
  <li><strong>Opus 4.8</strong> sits in the seats where being wrong is expensive and being thorough pays: <strong>research, architecture, and the security audit.</strong> Opus is meaningfully better at finding bugs — higher recall <em>and</em> precision — so it’s wasted on boilerplate and indispensable on review.</li>
  <li><strong>Sonnet 4.6</strong> is the builder. <strong>Implementation and fixes</strong> from a clear spec — strong, fast, and the right call when the design decisions are already made.</li>
  <li><strong>Haiku 4.5</strong> does the <strong>scaffolding</strong> — the Foundry project, the Next.js skeleton, the config. Mechanical, high-volume, no judgment required. Cheap on purpose.</li>
</ul>

<figure class="chart">
<svg viewBox="0 0 700 300" role="img" aria-labelledby="af-t">
<title id="af-t">A six-phase workflow with each phase on the right model tier</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-label-sm" x="20" y="30">Opus 4.8 — judgment</text>
<text class="c-label-sm" x="250" y="30">Sonnet 4.6 — build</text>
<text class="c-label-sm" x="470" y="30">Haiku 4.5 — scaffold</text>
<circle cx="14" cy="26" r="5" fill="var(--accent)" />
<circle cx="244" cy="26" r="5" class="c-box" />
<circle cx="464" cy="26" r="5" class="c-grid" fill="currentColor" opacity="0.4" />

<rect class="c-box-accent c-fill-soft" x="20" y="70" width="120" height="46" rx="8" />
<text class="c-label-sm" x="80" y="90" text-anchor="middle">Research</text>
<text class="c-label-sm" x="80" y="106" text-anchor="middle">Opus</text>
<line class="c-arrow" x1="142" y1="93" x2="172" y2="93" />

<rect class="c-box-accent c-fill-soft" x="174" y="70" width="120" height="46" rx="8" />
<text class="c-label-sm" x="234" y="90" text-anchor="middle">Architecture</text>
<text class="c-label-sm" x="234" y="106" text-anchor="middle">Opus</text>
<line class="c-arrow" x1="296" y1="93" x2="326" y2="93" />

<rect class="c-box" x="328" y="70" width="120" height="46" rx="6" />
<text class="c-label-sm" x="388" y="90" text-anchor="middle">Scaffold</text>
<text class="c-label-sm" x="388" y="106" text-anchor="middle">Haiku</text>
<line class="c-arrow" x1="450" y1="93" x2="480" y2="93" />

<rect class="c-box" x="482" y="58" width="150" height="32" rx="6" />
<text class="c-label-sm" x="557" y="78" text-anchor="middle">Build: contracts — Sonnet</text>
<rect class="c-box" x="482" y="96" width="150" height="32" rx="6" />
<text class="c-label-sm" x="557" y="116" text-anchor="middle">Build: frontend — Sonnet</text>

<line class="c-arrow" x1="557" y1="128" x2="557" y2="170" />
<rect class="c-box-accent c-fill-soft" x="482" y="172" width="150" height="46" rx="8" />
<text class="c-label-sm" x="557" y="192" text-anchor="middle">Audit (adversarial)</text>
<text class="c-label-sm" x="557" y="208" text-anchor="middle">Opus</text>
<line class="c-arrow" x1="480" y1="195" x2="326" y2="195" />

<rect class="c-box" x="174" y="172" width="150" height="46" rx="6" />
<text class="c-label-sm" x="249" y="192" text-anchor="middle">Finalize: fix + tests</text>
<text class="c-label-sm" x="249" y="208" text-anchor="middle">Sonnet</text>

<text class="c-label-sm" x="350" y="262" text-anchor="middle">The auditor reads the builder's code and tries to break it — a different model in a different seat.</text>
<text class="c-label-sm" x="350" y="284" text-anchor="middle">Its findings become the fixer's worklist. Then I re-ran the tests myself.</text>
</svg>
<figcaption>Six phases, each on the tier the work needs. The expensive model is reserved for research, design, and the adversarial audit; the cheap one for scaffolding.</figcaption>
</figure>

<h2 id="what-got-built">What got built</h2>

<p>A real missions/rewards layer, not a toy:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">RewardToken</code></strong> — IMPACT, the ERC-20 community reward. Minting is owner-only and the Missions contract holds no minter rights, so a bug in the protocol can’t inflate the supply.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Missions</code></strong> — the escrow. A creator funds a mission with a <em>capped</em> reward pool. A user claims exactly once, and only with an <strong>EIP-712 attestation</strong> signed by a trusted off-chain verifier (you did the mission). The claim is replay-proof per <code class="language-plaintext highlighter-rouge">(mission, user)</code>, the pool can’t be over-claimed or siphoned across missions, it’s <code class="language-plaintext highlighter-rouge">nonReentrant</code> with checks-effects-interactions, and there’s an attestation epoch so the admin can revoke outstanding signatures.</li>
</ul>

<h2 id="the-bug-the-auditor-caught-the-builder-writing">The bug the auditor caught the builder writing</h2>

<p>Here’s the part that justifies the seating chart. Sonnet built the contracts and 26 passing tests. Then Opus audited them, with one instruction that matters: <em>report everything, including low-severity and uncertain — don’t filter.</em> (Tell Opus to “only report high-severity issues” and it obeys you literally, and recall drops.)</p>

<p>It found four real things the builder hadn’t:</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">missionId</code> squatting</strong> — creators chose their own mission IDs, so one creator could grief another by pre-claiming an ID. Fix: a cancel cooldown plus <code class="language-plaintext highlighter-rouge">delete</code> so IDs free up correctly.</li>
  <li><strong>Single-step <code class="language-plaintext highlighter-rouge">Ownable</code></strong> on the attestor key — the key that authorizes <em>every</em> payout. A fat-fingered <code class="language-plaintext highlighter-rouge">transferOwnership</code> loses it forever. Fix: <code class="language-plaintext highlighter-rouge">Ownable2Step</code>.</li>
  <li><strong>Pre-signable, never-expiring attestations</strong> — an attestor could sign for a mission before it existed, with an unbounded deadline. Fix: the epoch mechanism.</li>
  <li>A missing zero-address guard in <code class="language-plaintext highlighter-rouge">claim</code>.</li>
</ol>

<p>It also did the opposite of finding bugs: a careful, affirmative proof that double-claim, signature forgery, malleability (OZ’s ECDSA reverts on high-s), cross-chain and cross-mission replay, pool drain, and reentrancy were <em>actually closed</em> — citing the lines. That’s the half of an audit people skip: saying what you checked and why it holds.</p>

<p>Sonnet then applied every fix with a regression test apiece. 26 tests became 34.</p>

<h2 id="i-re-ran-them-anyway">I re-ran them anyway</h2>

<p>A workflow telling me “34/34 green” is a claim, not a result. So before any of it shipped I re-ran <code class="language-plaintext highlighter-rouge">forge test</code> myself (34/34, confirmed), read the <code class="language-plaintext highlighter-rouge">claim</code> path line by line to check the safety properties were real rather than asserted, and secret-scanned the repo before flipping it public. The fleet does the work; the verification is still mine to own.</p>

<p>The contracts, the 34 tests, and the frontend scaffold are at <a href="https://github.com/0xSoftBoi/socialstack">github.com/0xSoftBoi/socialstack</a>. The whole thing — research to audited, tested protocol — was one workflow. The trick wasn’t the agents. It was putting the careful model in the careful seat.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="agents" /><category term="claude" /><category term="workflow" /><category term="solidity" /><category term="security" /><summary type="html"><![CDATA[I had an empty repo and pointed a multi-agent workflow at it. The result was a tested rewards protocol — but the part worth writing down is which model sat in which seat, and the bug the auditor caught that the builder wrote.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/a-social-good-protocol-built-by-an-agent-fleet.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/a-social-good-protocol-built-by-an-agent-fleet.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The index fund that held the wrong asset</title><link href="https://0xsoftboi.github.io/blog/the-index-fund-that-held-the-wrong-asset/" rel="alternate" type="text/html" title="The index fund that held the wrong asset" /><published>2026-06-09T13:00:00+00:00</published><updated>2026-06-09T13:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/the-index-fund-that-held-the-wrong-asset</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/the-index-fund-that-held-the-wrong-asset/"><![CDATA[<p>I found a Sui Move contract called <code class="language-plaintext highlighter-rouge">crypto_index_fund</code>. You deposit SUI, it mints you an <code class="language-plaintext highlighter-rouge">IndexFundToken</code> recording an equal-weighted basket — BTC, ETH, XRP, ADA, MATIC, priced through the Supra oracle — and when you withdraw it pays you out in SUI at the basket’s current value. A one-click crypto index fund on-chain. Slick.</p>

<p>It is also insolvent the moment any price moves, and the reason is the first thing you should check in any fund: <strong>does it actually hold what it says it holds?</strong></p>

<h2 id="it-holds-sui-it-pays-a-basket">It holds SUI. It pays a basket.</h2>

<p>Here’s the withdraw, trimmed:</p>

<pre><code class="language-move">// value the token's NOTIONAL basket at current oracle prices...
let total_usd_value = btc_usd + eth_usd + xrp_usd + ada_usd + matic_usd;
let total_sui = ((total_usd_value / adjusted_sui_usd_price) as u64);
// ...and pay that many SUI out of the shared pool
let index_token_balance = balance::split(&amp;mut index_fund.balance, total_sui);
</code></pre>

<p>Deposit only ever added SUI to <code class="language-plaintext highlighter-rouge">index_fund.balance</code>. The contract <strong>never bought a single satoshi of BTC</strong> — the basket is a number in a struct. So when BTC goes up, your token is “worth” more SUI, and <code class="language-plaintext highlighter-rouge">withdraw</code> pays it to you out of the common pool, which is just everyone else’s SUI.</p>

<p>Play it forward with two depositors. Both put in 1,000 SUI; the pool holds 2,000. BTC rallies. Alice withdraws first: her token now values at, say, 1,400 SUI, so <code class="language-plaintext highlighter-rouge">balance::split</code> hands her 1,400 and leaves 600. Bob withdraws: his token also values at 1,400, <code class="language-plaintext highlighter-rouge">balance::split(&amp;mut pool, 1400)</code> against a 600 balance — <strong>abort</strong>. Bob’s money is stuck. It’s a bank run, except the run is triggered by a green candle and the loser is whoever clicks second. (There’s tangled decimal math and no oracle-staleness check on top, but the insolvency is the one that empties wallets.)</p>

<figure class="chart">
<svg viewBox="0 0 680 300" role="img" aria-labelledby="if-t">
<title id="if-t">A fund that pays a basket it doesn't hold vs a solvent pro-rata fund</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-title" x="20" y="24">Original: holds SUI, owes a basket</text>
<rect class="c-box" x="20" y="46" width="150" height="40" rx="6" />
<text class="c-label-sm" x="95" y="70" text-anchor="middle">deposit SUI</text>
<line class="c-arrow" x1="172" y1="66" x2="214" y2="66" />
<rect class="c-box" x="216" y="46" width="200" height="40" rx="6" />
<text class="c-label-sm" x="316" y="70" text-anchor="middle">record notional basket</text>
<line class="c-arrow" x1="418" y1="66" x2="460" y2="66" />
<rect class="c-box-accent c-fill-soft" x="462" y="46" width="198" height="40" rx="8" />
<text class="c-label-sm" x="561" y="63" text-anchor="middle">withdraw basket value in SUI</text>
<text class="c-label-sm" x="561" y="79" text-anchor="middle">→ drain pool, 2nd aborts</text>
<line class="c-grid" x1="20" y1="108" x2="660" y2="108" />
<text class="c-title" x="20" y="136">Fixed: pro-rata shares of what's actually held</text>
<rect class="c-box" x="20" y="158" width="150" height="40" rx="6" />
<text class="c-label-sm" x="95" y="182" text-anchor="middle">deposit SUI</text>
<line class="c-arrow" x1="172" y1="178" x2="214" y2="178" />
<rect class="c-box" x="216" y="158" width="200" height="40" rx="6" />
<text class="c-label-sm" x="316" y="182" text-anchor="middle">mint pro-rata shares</text>
<line class="c-arrow" x1="418" y1="178" x2="460" y2="178" />
<rect class="c-box-accent c-fill-soft" x="462" y="158" width="198" height="40" rx="8" />
<text class="c-label-sm" x="561" y="175" text-anchor="middle">withdraw pool·shares/total</text>
<text class="c-label-sm" x="561" y="191" text-anchor="middle">→ always ≤ pool, never aborts</text>
<text class="c-label-sm" x="340" y="234" text-anchor="middle">You can only ever be paid your slice of what the fund actually holds.</text>
<text class="c-label-sm" x="340" y="258" text-anchor="middle">The oracle becomes informational NAV — with a staleness guard — not the money path.</text>
</svg>
<figcaption>The original pays a basket's value out of a SUI-only pool; the fix pays each holder their pro-rata share of the pool that actually exists.</figcaption>
</figure>

<h2 id="fix-it-by-only-ever-paying-what-you-hold">Fix it by only ever paying what you hold</h2>

<p>The <a href="https://github.com/0xSoftBoi/01/tree/main/move/index-fund">solvent rebuild</a> is a pro-rata <strong>share</strong> fund. Deposit mints shares proportional to the pool; withdraw returns <code class="language-plaintext highlighter-rouge">pool * shares / total_shares</code> SUI. That quantity is <em>always</em> ≤ the pool — so <code class="language-plaintext highlighter-rouge">balance::split</code> can never abort, and the fund can never owe SUI it doesn’t have. The oracle drops out of the money path entirely and becomes an <em>informational</em> USD NAV view (now with the staleness check the original skipped). The <a href="https://github.com/0xSoftBoi/01/blob/main/move/index-fund/tests/index_fund_tests.move">headline test</a> is just: two depositors, withdraw both — the second one <em>succeeds</em> instead of aborting. Five Move tests, green.</p>

<p>And here’s the honest part I put in the README: this solvent thing <strong>isn’t really an index fund anymore</strong>. It’s a SUI pool with a NAV readout. A <em>real</em> on-chain index fund has to actually <strong>custody the assets</strong> — swap the deposited SUI into wrapped BTC/ETH/… through a DEX so the basket exists on-chain. That’s the part the original skipped, and skipping it is the whole bug. You can’t pay out exposure you never bought.</p>

<p>It’s the same lesson as the <a href="/blog/anatomy-of-a-fake-dice-game/">fake dice game</a> and the <a href="/blog/the-bridge-that-paid-twice/">bridge that paid twice</a>: the contract isn’t where these break. They break on a promise the contract makes about value it doesn’t actually hold — a roll it never reads, a release it never recorded, a basket it never bought. Read what the pool contains before you read what it claims to owe.</p>

<p>Audit, fix, and the five passing tests are at <a href="https://github.com/0xSoftBoi/01/tree/main/move/index-fund">github.com/0xSoftBoi/01</a>.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="sui" /><category term="move" /><category term="defi" /><category term="oracle" /><category term="security" /><summary type="html"><![CDATA[A Sui Move 'crypto index fund' lets you deposit SUI for exposure to a BTC/ETH/XRP/ADA/MATIC basket. The problem: it never buys any of them. It holds SUI and pays out basket gains it doesn't have — insolvent by construction, a bank run waiting for a green candle.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/the-index-fund-that-held-the-wrong-asset.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/the-index-fund-that-held-the-wrong-asset.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anatomy of a memecoin honeypot</title><link href="https://0xsoftboi.github.io/blog/anatomy-of-a-memecoin-honeypot/" rel="alternate" type="text/html" title="Anatomy of a memecoin honeypot" /><published>2026-06-09T12:00:00+00:00</published><updated>2026-06-09T12:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/anatomy-of-a-memecoin-honeypot</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/anatomy-of-a-memecoin-honeypot/"><![CDATA[<p>I found a memecoin contract in an old repo — <code class="language-plaintext highlighter-rouge">DarkPepe</code> (<code class="language-plaintext highlighter-rouge">DEPE</code>), the copy-pasted “token with a blacklist” template that’s been deployed thousands of times. It compiles, it trades, it has a cute name. It also hands the deployer a button that freezes your tokens after you buy. Here is that button, in full:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">mapping</span><span class="p">(</span><span class="kt">address</span> <span class="o">=&gt;</span> <span class="kt">bool</span><span class="p">)</span> <span class="k">public</span> <span class="n">blacklists</span><span class="p">;</span>

<span class="k">function</span> <span class="n">blacklist</span><span class="p">(</span><span class="kt">address</span> <span class="n">_address</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">_isBlacklisting</span><span class="p">)</span> <span class="k">external</span> <span class="n">onlyOwner</span> <span class="p">{</span>
    <span class="n">blacklists</span><span class="p">[</span><span class="n">_address</span><span class="p">]</span> <span class="o">=</span> <span class="n">_isBlacklisting</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">function</span> <span class="n">_beforeTokenTransfer</span><span class="p">(</span><span class="kt">address</span> <span class="n">from</span><span class="p">,</span> <span class="kt">address</span> <span class="n">to</span><span class="p">,</span> <span class="kt">uint256</span> <span class="n">amount</span><span class="p">)</span> <span class="k">override</span> <span class="k">internal</span> <span class="p">{</span>
    <span class="nb">require</span><span class="p">(</span><span class="o">!</span><span class="n">blacklists</span><span class="p">[</span><span class="n">to</span><span class="p">]</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">blacklists</span><span class="p">[</span><span class="n">from</span><span class="p">],</span> <span class="s">"Blacklisted"</span><span class="p">);</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every transfer runs <code class="language-plaintext highlighter-rouge">_beforeTokenTransfer</code> first. So the moment the owner calls <code class="language-plaintext highlighter-rouge">blacklist(you, true)</code>, every transfer touching your address reverts. You can’t sell. You can’t move it to another wallet. You can’t even <em>receive</em> more. Your bag is frozen, on-chain, at the deployer’s discretion. You buy at the top, they flip the switch, and the exit is gone. That’s a honeypot — and it’s not hidden in assembly or a proxy, it’s eleven lines of plain Solidity.</p>

<p>There are two more levers in the same contract:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// in _beforeTokenTransfer:
</span><span class="k">if</span> <span class="p">(</span><span class="n">uniswapV2Pair</span> <span class="o">==</span> <span class="kt">address</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span> <span class="p">{</span>
    <span class="nb">require</span><span class="p">(</span><span class="n">from</span> <span class="o">==</span> <span class="n">owner</span><span class="p">()</span> <span class="o">||</span> <span class="n">to</span> <span class="o">==</span> <span class="n">owner</span><span class="p">(),</span> <span class="s">"trading is not started"</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">limited</span> <span class="o">&amp;&amp;</span> <span class="n">from</span> <span class="o">==</span> <span class="n">uniswapV2Pair</span><span class="p">)</span> <span class="p">{</span>
    <span class="nb">require</span><span class="p">(</span><span class="n">balanceOf</span><span class="p">(</span><span class="n">to</span><span class="p">)</span> <span class="o">+</span> <span class="n">amount</span> <span class="o">&lt;=</span> <span class="n">maxHoldingAmount</span> <span class="o">&amp;&amp;</span> <span class="p">...</span> <span class="o">&gt;=</span> <span class="n">minHoldingAmount</span><span class="p">,</span> <span class="s">"Forbid"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The first means that <strong>until the owner sets the pair, only the owner can move tokens</strong> — they decide if and when trading ever opens, and can simply never open it. The second lets <code class="language-plaintext highlighter-rouge">setRule(...)</code> cap how much anyone can buy, down to zero. Mint 100% of supply to yourself, retain ownership, and you hold every lever.</p>

<figure class="chart">
<svg viewBox="0 0 680 290" role="img" aria-labelledby="hp-t">
<title id="hp-t">A honeypot token freezes the buyer; a trap-free token can't</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-title" x="20" y="24">DarkPepe: the deployer keeps the keys</text>
<rect class="c-box" x="20" y="46" width="120" height="40" rx="6" />
<text class="c-label-sm" x="80" y="70" text-anchor="middle">you buy</text>
<line class="c-arrow" x1="142" y1="66" x2="184" y2="66" />
<rect class="c-box" x="186" y="46" width="210" height="40" rx="6" />
<text class="c-label-sm" x="291" y="70" text-anchor="middle">owner calls blacklist(you, true)</text>
<line class="c-arrow" x1="398" y1="66" x2="440" y2="66" />
<rect class="c-box-accent c-fill-soft" x="442" y="46" width="218" height="40" rx="8" />
<text class="c-label-sm" x="551" y="70" text-anchor="middle">every transfer reverts — frozen</text>
<text class="c-label-sm" x="20" y="116">Three owner levers: freeze any holder · gate trading until "started" · cap/forbid buys.</text>
<line class="c-grid" x1="20" y1="138" x2="660" y2="138" />
<text class="c-title" x="20" y="166">SafeToken: nothing privileged to call</text>
<rect class="c-box" x="20" y="188" width="120" height="40" rx="6" />
<text class="c-label-sm" x="80" y="212" text-anchor="middle">you buy</text>
<line class="c-arrow" x1="142" y1="208" x2="184" y2="208" />
<rect class="c-box" x="186" y="188" width="210" height="40" rx="6" />
<text class="c-label-sm" x="291" y="208" text-anchor="middle">no owner / blacklist / gate exists</text>
<line class="c-arrow" x1="398" y1="208" x2="440" y2="208" />
<rect class="c-box-accent c-fill-soft" x="442" y="188" width="218" height="40" rx="8" />
<text class="c-label-sm" x="551" y="208" text-anchor="middle">you can always sell</text>
<text class="c-label-sm" x="340" y="262" text-anchor="middle">Read _beforeTokenTransfer (or _update) and every onlyOwner function before you ape.</text>
</svg>
<figcaption>The honeypot isn't subtle — it's a <code>blacklist</code> mapping checked on every transfer. A token nobody can rug simply has no privileged function to call.</figcaption>
</figure>

<h2 id="proving-it-and-the-antidote">Proving it, and the antidote</h2>

<p>I didn’t want to just assert this, so I <a href="https://github.com/0xSoftBoi/01/blob/main/audit/test/Honeypot.t.sol">wrote three Foundry tests</a> against the <em>real</em> <code class="language-plaintext highlighter-rouge">DarkPepe</code> bytecode. One buys in as a holder, has the owner blacklist them, and asserts the next sell reverts with <code class="language-plaintext highlighter-rouge">"Blacklisted"</code> — and that they can no longer receive either. One shows that before trading is “started,” a non-owner transfer reverts. They pass. The honeypot is exactly as advertised.</p>

<p>Then the antidote, also tested: <code class="language-plaintext highlighter-rouge">SafeToken</code> — a deliberately un-ruggable ERC-20. Fixed supply minted once, <strong>no owner, no blacklist, no transfer gate, no mint, no holding caps.</strong> There is no function any party can call to freeze or seize a balance, so the test that tries to trap a holder has nothing to call. That’s the whole point: safety here isn’t a feature you add, it’s privilege you <em>remove</em>.</p>

<h2 id="the-takeaway">The takeaway</h2>

<p>This is the most actionable post I’ll write, so here it is plainly: <strong>before you ape a token, read two things</strong> — its <code class="language-plaintext highlighter-rouge">_beforeTokenTransfer</code> / <code class="language-plaintext highlighter-rouge">_update</code> hook, and every <code class="language-plaintext highlighter-rouge">onlyOwner</code> function. A <code class="language-plaintext highlighter-rouge">blacklists</code> mapping, a <code class="language-plaintext highlighter-rouge">tradingEnabled</code> flag, a <code class="language-plaintext highlighter-rouge">setRule</code> that gates the pair, a <code class="language-plaintext highlighter-rouge">_mint</code> the owner can still call — any one of them means the deployer can stop you selling or dilute you at will. The contract will look friendly and trade fine right up until it doesn’t. The exit being open today doesn’t mean it’s open tomorrow if someone else holds the key.</p>

<p>The audit, the proofs, and the trap-free reference are at <a href="https://github.com/0xSoftBoi/01/blob/main/AUDIT.md">github.com/0xSoftBoi/01</a>.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="solidity" /><category term="security" /><category term="memecoin" /><category term="rug" /><summary type="html"><![CDATA[A token contract I found trades perfectly and looks like every other ERC-20. It also lets the deployer freeze your bag with one call. Here's the line that does it, proven against the real contract — and what an un-ruggable token looks like instead.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/anatomy-of-a-memecoin-honeypot.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/anatomy-of-a-memecoin-honeypot.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The bridge that paid twice</title><link href="https://0xsoftboi.github.io/blog/the-bridge-that-paid-twice/" rel="alternate" type="text/html" title="The bridge that paid twice" /><published>2026-06-09T11:00:00+00:00</published><updated>2026-06-09T11:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/the-bridge-that-paid-twice</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/the-bridge-that-paid-twice/"><![CDATA[<p>Here’s a bridge relayer, more or less as I found it:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">sepoliaBridge</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">Deposit</span><span class="dl">"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">depositor</span><span class="p">,</span> <span class="nx">amount</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">mumbaiBridge</span><span class="p">.</span><span class="nx">release</span><span class="p">(</span><span class="nx">depositor</span><span class="p">,</span> <span class="nx">amount</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Lock tokens on Sepolia, the bot sees the <code class="language-plaintext highlighter-rouge">Deposit</code> event, releases the same amount on Mumbai. And the destination <code class="language-plaintext highlighter-rouge">release</code> is exactly what you’d expect:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">release</span><span class="p">(</span><span class="kt">address</span> <span class="n">_to</span><span class="p">,</span> <span class="kt">uint256</span> <span class="n">_amount</span><span class="p">)</span> <span class="k">public</span> <span class="n">onlyOwner</span> <span class="p">{</span>
    <span class="n">IERC20</span><span class="p">(</span><span class="n">token</span><span class="p">).</span><span class="nb">transfer</span><span class="p">(</span><span class="n">_to</span><span class="p">,</span> <span class="n">_amount</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It works in the demo. It is also a contract that will pay you twice.</p>

<h2 id="events-are-not-exactly-once">Events are not exactly-once</h2>

<p>The bug is the unspoken assumption that each <code class="language-plaintext highlighter-rouge">Deposit</code> fires once and is handled once. Neither is guaranteed:</p>

<ul>
  <li>The relayer <strong>restarts</strong> and replays recent blocks — same event again.</li>
  <li>The websocket <strong>reconnects</strong> and re-emits buffered logs — same event again.</li>
  <li>The source chain <strong>reorgs</strong>: the block with your <code class="language-plaintext highlighter-rouge">Deposit</code> gets re-mined, the subscription re-delivers it — same event again. (And in a reorg the <em>original</em> deposit may no longer exist at all.)</li>
</ul>

<p>Each re-delivery calls <code class="language-plaintext highlighter-rouge">release</code> again, and the destination has <strong>no memory</strong> of what it already paid — no nonce, no record, nothing. So it transfers again. And again. The reserve drains one duplicate at a time, and there’s not even a <code class="language-plaintext highlighter-rouge">Released</code> event to notice it happening. This isn’t exotic; “the off-chain component double-submitted” is one of the most common ways real bridges have lost money.</p>

<figure class="chart">
<svg viewBox="0 0 680 300" role="img" aria-labelledby="b-t">
<title id="b-t">An event re-delivered double-pays; an idempotent release pays once</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-title" x="20" y="24">Original: release() on every event</text>
<rect class="c-box" x="20" y="46" width="210" height="40" rx="6" />
<text class="c-label-sm" x="125" y="70" text-anchor="middle">Deposit event (re-delivered ×2)</text>
<line class="c-arrow" x1="232" y1="66" x2="274" y2="66" />
<rect class="c-box" x="276" y="46" width="150" height="40" rx="6" />
<text class="c-label-sm" x="351" y="70" text-anchor="middle">release(to, amount)</text>
<line class="c-arrow" x1="428" y1="66" x2="470" y2="66" />
<rect class="c-box" x="472" y="46" width="188" height="40" rx="6" />
<text class="c-label-sm" x="566" y="70" text-anchor="middle">paid TWICE → drain</text>
<line class="c-grid" x1="20" y1="108" x2="660" y2="108" />
<text class="c-title" x="20" y="136">Hardened: a transferId paid at most once</text>
<rect class="c-box" x="20" y="156" width="210" height="46" rx="6" />
<text class="c-label-sm" x="125" y="176" text-anchor="middle">Locked(nonce) on source</text>
<text class="c-label-sm" x="125" y="192" text-anchor="middle">+ validator signs transferId</text>
<line class="c-arrow" x1="232" y1="179" x2="274" y2="179" />
<rect class="c-box-accent c-fill-soft" x="276" y="156" width="200" height="46" rx="8" />
<text class="c-label-sm" x="376" y="176" text-anchor="middle">release: check sig +</text>
<text class="c-label-sm" x="376" y="192" text-anchor="middle">processed[transferId]?</text>
<line class="c-arrow" x1="478" y1="179" x2="520" y2="179" />
<rect class="c-box" x="522" y="156" width="138" height="46" rx="6" />
<text class="c-label-sm" x="591" y="176" text-anchor="middle">1st: pay once</text>
<text class="c-label-sm" x="591" y="192" text-anchor="middle">2nd: AlreadyReleased</text>
<text class="c-label-sm" x="340" y="232" text-anchor="middle">The transferId binds the source lock + both chain ids + this contract — unique, unforgeable, paid once.</text>
<text class="c-label-sm" x="340" y="256" text-anchor="middle">And the relayer waits for source finality, so a reorg can't make it sign for a lock that vanished.</text>
</svg>
<figcaption>Idempotency keyed on a per-lock <code>transferId</code> turns "release on every event" into "release this lock at most once" — the re-delivered event reverts instead of paying again.</figcaption>
</figure>

<h2 id="making-a-payout-happen-once">Making a payout happen once</h2>

<p>The fix is to give every payout an identity and remember it:</p>

<ul>
  <li>Each destination release is keyed by a <strong><code class="language-plaintext highlighter-rouge">transferId</code></strong> derived from the <em>unique</em> source lock — <code class="language-plaintext highlighter-rouge">srcChainId, srcBridge, srcNonce, to, amount</code> — plus the destination chain id and this contract’s address. <code class="language-plaintext highlighter-rouge">mapping(bytes32 =&gt; bool) processed</code> means the second <code class="language-plaintext highlighter-rouge">release</code> for the same id reverts with <code class="language-plaintext highlighter-rouge">AlreadyReleased</code>. Re-deliver the event all you like; it pays once.</li>
  <li>The release carries a <strong>validator signature</strong> over that <code class="language-plaintext highlighter-rouge">transferId</code>, so authority to pay is explicit and verifiable, decoupled from the contract owner — and because the chain ids and both bridge addresses are baked into the id, a signature can’t be replayed onto another deployment.</li>
  <li><code class="language-plaintext highlighter-rouge">SafeERC20</code>, a <code class="language-plaintext highlighter-rouge">Released</code> event to reconcile against, <code class="language-plaintext highlighter-rouge">Pausable</code>, and a relayer that <strong>waits for source finality</strong> before signing. (Idempotency stops paying the same lock <em>twice</em>; finality stops paying for a lock a reorg <em>erased</em> — two different failures.)</li>
</ul>

<p><a href="https://github.com/0xSoftBoi/cross-evm-bridge">Six Foundry tests</a>, and the one that matters most just re-calls <code class="language-plaintext highlighter-rouge">release</code> with the same signature and asserts the second call reverts and the balance moved once.</p>

<h2 id="what-it-still-isnt">What it still isn’t</h2>

<p>I want to be precise about what this does and doesn’t buy, because “bridge” oversells. The hardening removes the <em>mechanical</em> ways to lose money — double-pays, unsafe transfers, unauthorized releases. It does <strong>not</strong> remove the <em>trust</em>: this is still a <strong>custodial</strong> bridge, and a compromised validator can sign a payout for a lock that never happened. The trust-minimized version replaces the signature with a <strong>light-client or Merkle proof</strong> of the source <code class="language-plaintext highlighter-rouge">Locked</code> event, so the destination <em>verifies</em> the lock instead of <em>trusting</em> a signer — that’s the real frontier, and it’s the line between this and a bridge you’d actually trust with size.</p>

<p>It’s the same shape as the <a href="https://github.com/0xSoftBoi/lock-mint-bridge-lab">other bridge I annotated</a>: the cryptography and the Solidity are rarely where these break. The break is an off-chain assumption — here, “events happen once” — that the chain never promised to keep.</p>

<p>The audit and the fix are at <a href="https://github.com/0xSoftBoi/cross-evm-bridge">github.com/0xSoftBoi/cross-evm-bridge</a>.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="solidity" /><category term="bridges" /><category term="security" /><category term="reorg" /><summary type="html"><![CDATA[A token bridge whose relayer calls release() on every Deposit event. That sounds fine until you remember event delivery isn't exactly-once — and a reorg, a reconnect, or a restart makes the destination pay the same lock again. Here's the fix that makes a payout happen exactly once.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/the-bridge-that-paid-twice.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/the-bridge-that-paid-twice.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anatomy of a fake dice game</title><link href="https://0xsoftboi.github.io/blog/anatomy-of-a-fake-dice-game/" rel="alternate" type="text/html" title="Anatomy of a fake dice game" /><published>2026-06-09T10:00:00+00:00</published><updated>2026-06-09T10:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/anatomy-of-a-fake-dice-game</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/anatomy-of-a-fake-dice-game/"><![CDATA[<p>I went spelunking through some old repos and found a dice game from 2018 — <code class="language-plaintext highlighter-rouge">YungBet</code>, the on-chain half of a gambling project whose parent pitch deck claims it <em>“sold for $4.5m.”</em> I can’t verify that number, and after reading the contract I’d suggest not taking the deck at face value. It’s 48 lines, and it is two bugs wearing a trenchcoat.</p>

<h2 id="bug-1-the-randomness-is-always-zero">Bug 1: the randomness is always zero</h2>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">RandomNumber</span><span class="p">()</span> <span class="k">public</span> <span class="k">returns</span><span class="p">(</span><span class="kt">uint</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">total_bets</span><span class="p">[</span><span class="n">msg</span><span class="p">.</span><span class="n">sender</span><span class="p">]</span><span class="o">++</span><span class="p">;</span>
    <span class="kt">uint</span> <span class="n">random_number</span> <span class="o">=</span> <span class="kt">uint</span><span class="p">(</span><span class="nb">keccak256</span><span class="p">(</span><span class="n">abi</span><span class="p">.</span><span class="n">encodePacked</span><span class="p">(</span>
        <span class="nb">blockhash</span><span class="p">(</span><span class="n">block</span><span class="p">.</span><span class="n">number</span><span class="p">),</span>
        <span class="n">total_bets</span><span class="p">[</span><span class="n">msg</span><span class="p">.</span><span class="n">sender</span><span class="p">]</span>
    <span class="p">)));</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">blockhash(block.number)</code> — the hash of the block currently executing — is <strong>always <code class="language-plaintext highlighter-rouge">0</code></strong>. The EVM only exposes the previous 256 block hashes; the current block isn’t mined yet, so it has no hash. So the “random” number collapses to <code class="language-plaintext highlighter-rouge">keccak256(0, total_bets[msg.sender])</code> — a pure function of <em>your own bet counter</em>. You can compute every future roll you’ll ever get, off-chain, before you bet. It’s the same family as the <a href="/blog/the-on-chain-randomness-landscape/">SmartBillions</a> lottery, which used a block hash that resolved to zero and got drained for ~400 ETH.</p>

<h2 id="bug-2-it-never-uses-the-roll-anyway">Bug 2: it never uses the roll anyway</h2>

<p>That bug almost doesn’t matter, because of the second one:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">makeBet</span><span class="p">()</span> <span class="k">public</span> <span class="k">payable</span> <span class="p">{</span>
    <span class="kt">uint</span> <span class="n">bet_roll</span> <span class="o">=</span> <span class="n">RandomNumber</span><span class="p">();</span>          <span class="c1">// computed...
</span>    <span class="kt">uint</span> <span class="n">bet_payout</span> <span class="o">=</span> <span class="n">bet_amount</span><span class="p">.</span><span class="n">div</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>     <span class="c1">// ...and ignored
</span>    <span class="nb">require</span><span class="p">(</span><span class="n">bet_payout</span> <span class="o">&lt;</span> <span class="kt">address</span><span class="p">(</span><span class="nb">this</span><span class="p">).</span><span class="nb">balance</span><span class="p">,</span> <span class="s">"..."</span><span class="p">);</span>
    <span class="n">user</span><span class="p">.</span><span class="nb">transfer</span><span class="p">(</span><span class="n">bet_payout</span><span class="p">);</span>
    <span class="k">emit</span> <span class="n">betPlaced</span><span class="p">(</span><span class="n">bet_amount</span><span class="p">,</span> <span class="n">bet_payout</span><span class="p">,</span> <span class="n">bet_roll</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">bet_roll</code> is computed, emitted in an event for flavour, and <strong>never read</strong>. The payout is unconditionally half your stake. There is no win condition, no target, no comparison — you send <code class="language-plaintext highlighter-rouge">x</code>, you get <code class="language-plaintext highlighter-rouge">x/2</code> back, every time. It’s not a dice game with bad randomness; it’s a guaranteed 50% drain with a random-number generator bolted on for decoration. The broken RNG is a red herring sitting on top of a contract that was never a game.</p>

<h2 id="building-the-one-it-pretended-to-be">Building the one it pretended to be</h2>

<p>So I built the real thing — <a href="https://github.com/0xSoftBoi/dice/blob/main/src/Dice.sol"><code class="language-plaintext highlighter-rouge">Dice.sol</code></a>. The starting constraint is the lesson of every randomness post I’ve written: <strong>there is no safe single-transaction randomness on the EVM.</strong> Anything you can read in the bet transaction, the bettor can read too. So you need a value that’s fixed <em>before</em> the bet and unknown <em>until after</em> it — commit-reveal.</p>

<figure class="chart">
<svg viewBox="0 0 680 300" role="img" aria-labelledby="d-t">
<title id="d-t">A fake dice flow versus a provably-fair commit-reveal flow</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-title" x="20" y="24">YungBet: a roll nobody rolls</text>
<rect class="c-box" x="20" y="46" width="150" height="40" rx="6" />
<text class="c-label-sm" x="95" y="70" text-anchor="middle">bet</text>
<line class="c-arrow" x1="172" y1="66" x2="214" y2="66" />
<rect class="c-box" x="216" y="46" width="200" height="40" rx="6" />
<text class="c-label-sm" x="316" y="66" text-anchor="middle">blockhash(now) = 0 -&gt; roll</text>
<text class="c-label-sm" x="316" y="80" text-anchor="middle">(predictable, and unused)</text>
<line class="c-arrow" x1="418" y1="66" x2="460" y2="66" />
<rect class="c-box" x="462" y="46" width="198" height="40" rx="6" />
<text class="c-label-sm" x="561" y="70" text-anchor="middle">always pay stake / 2</text>
<line class="c-grid" x1="20" y1="108" x2="660" y2="108" />
<text class="c-title" x="20" y="136">Dice: house pre-commit + player seed + reveal</text>
<rect class="c-box-accent c-fill-soft" x="20" y="156" width="158" height="46" rx="8" />
<text class="c-label-sm" x="99" y="176" text-anchor="middle">house commits</text>
<text class="c-label-sm" x="99" y="192" text-anchor="middle">keccak(serverSeed)</text>
<line class="c-arrow" x1="180" y1="179" x2="218" y2="179" />
<rect class="c-box" x="220" y="156" width="170" height="46" rx="6" />
<text class="c-label-sm" x="305" y="176" text-anchor="middle">bet + target + clientSeed</text>
<text class="c-label-sm" x="305" y="192" text-anchor="middle">(your entropy)</text>
<line class="c-arrow" x1="392" y1="179" x2="430" y2="179" />
<rect class="c-box-accent c-fill-soft" x="432" y="156" width="228" height="46" rx="8" />
<text class="c-label-sm" x="546" y="176" text-anchor="middle">reveal -&gt; roll = keccak(server,</text>
<text class="c-label-sm" x="546" y="192" text-anchor="middle">client, id) % 100 decides; edge applied</text>
<text class="c-label-sm" x="340" y="232" text-anchor="middle">House can't pick the seed late; player can't see it early; the roll actually pays.</text>
<text class="c-label-sm" x="340" y="256" text-anchor="middle">No reveal by the deadline? Every bet pays as a WIN — withholding can't help the house.</text>
</svg>
<figcaption>The fake game computes an unused, predictable number and pays half. The real one binds the outcome to a seed the house committed before the bet and revealed after — and pays the house edge on a roll that decides.</figcaption>
</figure>

<p>The shape:</p>

<ul>
  <li>The <strong>house commits</strong> <code class="language-plaintext highlighter-rouge">keccak256(serverSeed)</code> <em>before</em> any bet references it, so it can’t choose the seed after seeing the action.</li>
  <li>Each <strong>bet</strong> carries a player-chosen <code class="language-plaintext highlighter-rouge">clientSeed</code>. The roll is <code class="language-plaintext highlighter-rouge">keccak256(serverSeed, clientSeed, betId) % 100</code> — the house can’t predict it without knowing the player’s seed, the player can’t predict it without the still-secret server seed, and neither can grind it.</li>
  <li>The <strong>roll decides</strong>: bet that it lands under a target, paid with a real <strong>house edge</strong> — a 50/50 bet pays <code class="language-plaintext highlighter-rouge">1.96×</code>, not <code class="language-plaintext highlighter-rouge">2×</code>. (The original couldn’t even charge an edge; it just kept half.)</li>
</ul>

<p>The one residual is the <em>house</em> withholding a losing reveal — the <a href="/blog/verifiable-isnt-trustless-onchain-randomness/">last-revealer problem</a> that haunts every commit-reveal scheme. <code class="language-plaintext highlighter-rouge">claimRevealTimeout</code> closes it: miss the deadline and <strong>every bet in the round pays as a win</strong>, so withholding is strictly worse for the house than just revealing. Plus the boring-but-load-bearing parts the original lacked entirely: a bankroll that reserves each open bet’s maximum loss (unbacked bets revert), pull-payment withdrawals, reentrancy guards. It’s <a href="https://github.com/0xSoftBoi/dice">seven Foundry tests</a>, including one that confirms the outcome actually tracks the roll and one that pays the player when the house goes quiet.</p>

<h2 id="the-lesson">The lesson</h2>

<p>The funny thing about <code class="language-plaintext highlighter-rouge">YungBet</code> is that the headline bug — the always-zero randomness — is the <em>less</em> serious one. You could fix the RNG perfectly and the contract would still just take half your money, because it never looked at the roll. It’s the purest example of a pattern I keep running into: the cryptography is rarely where these things actually break. The break is in whether the outcome people are betting on is <em>bound to</em> the cryptography at all — the <a href="/blog/the-on-chain-randomness-landscape/">randomness</a>, the <a href="/blog/what-a-zk-proof-proves/">proof</a>, the roll. Here it wasn’t bound to anything. It was decoration on a coin-flip you always lose.</p>

<p>The audit and the rebuild are at <a href="https://github.com/0xSoftBoi/dice">github.com/0xSoftBoi/dice</a>.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="solidity" /><category term="randomness" /><category term="security" /><category term="gambling" /><summary type="html"><![CDATA[I dug up a dice-game contract from 2018. It's 48 lines, and it's two bugs in a trenchcoat: its randomness is always zero, and it never uses the roll anyway — every bet just loses half. Here's the autopsy, and the provably-fair version it was pretending to be.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/anatomy-of-a-fake-dice-game.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/anatomy-of-a-fake-dice-game.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How CoW Protocol settles a trade (and what my TWAP router got wrong)</title><link href="https://0xsoftboi.github.io/blog/how-cow-protocol-settles/" rel="alternate" type="text/html" title="How CoW Protocol settles a trade (and what my TWAP router got wrong)" /><published>2026-06-08T00:00:00+00:00</published><updated>2026-06-08T00:00:00+00:00</updated><id>https://0xsoftboi.github.io/blog/how-cow-protocol-settles</id><content type="html" xml:base="https://0xsoftboi.github.io/blog/how-cow-protocol-settles/"><![CDATA[<p>I had a TWAP order-router for CoW Protocol — split a big sell into time-sliced parts to cut price impact. It built, it had seventeen passing tests, and its core did this:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ICowVaultRelayer</span><span class="p">(</span><span class="n">cowRelayer</span><span class="p">).</span><span class="n">deposit</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">owner</span><span class="p">,</span> <span class="n">amount</span><span class="p">);</span>
<span class="n">ICowSettler</span><span class="p">(</span><span class="n">cowSettler</span><span class="p">).</span><span class="n">settle</span><span class="p">(</span><span class="n">orderUid</span><span class="p">);</span>
</code></pre></div></div>

<p>Neither of those functions exists on mainnet. The contract was integrating with a CoW Protocol I’d imagined — and the tests passed because they mocked the imaginary interface. That’s the trap with on-chain integrations: “compiles + green against mocks” tells you nothing about whether you’re calling a real protocol. So here’s how CoW actually settles a trade, and the three things I had backwards.</p>

<h2 id="orders-are-intents-not-swaps">Orders are intents, not swaps</h2>

<p>On an AMM you submit a transaction that <em>executes</em> a swap against a pool. On CoW you sign an <strong>intent</strong>: “sell at most X of token A for at least Y of token B before time T.” You never execute anything. A signed order is just the <a href="https://docs.cow.fi/cow-protocol/reference/contracts/core/settlement"><code class="language-plaintext highlighter-rouge">GPv2Order.Data</code></a> struct — tokens, amounts, limits, <code class="language-plaintext highlighter-rouge">validTo</code>, kind — and its EIP-712 digest is the order’s id.</p>

<p>Orders are collected over a short window into a <strong>batch</strong>. Then off-chain <strong>solvers</strong> compete for the right to settle the batch, each proposing a solution that routes the orders (matching them directly against each other where possible — “coincidence of wants” — and only hitting external AMMs for the remainder). The winning solver is the one that returns the most surplus to users, and only that solver calls:</p>

<div class="language-solidity highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">settle</span><span class="p">(</span><span class="n">IERC20</span><span class="p">[]</span> <span class="n">tokens</span><span class="p">,</span> <span class="kt">uint256</span><span class="p">[]</span> <span class="n">clearingPrices</span><span class="p">,</span>
                <span class="n">GPv2Trade</span><span class="p">.</span><span class="n">Data</span><span class="p">[]</span> <span class="n">trades</span><span class="p">,</span> <span class="n">GPv2Interaction</span><span class="p">.</span><span class="n">Data</span><span class="p">[][</span><span class="mi">3</span><span class="p">]</span> <span class="n">interactions</span><span class="p">)</span>
    <span class="k">external</span> <span class="n">nonReentrant</span> <span class="n">onlySolver</span><span class="p">;</span>
</code></pre></div></div>

<p>Two things in that signature mattered for my bug. It’s <strong><code class="language-plaintext highlighter-rouge">onlySolver</code></strong> — an allow-listed, bonded set; a random contract cannot call <code class="language-plaintext highlighter-rouge">settle</code>. And it takes a <strong><code class="language-plaintext highlighter-rouge">clearingPrices</code></strong> array with one price per token: every order in the batch touching a given token settles at the <em>same</em> price. That uniform clearing price is the actual MEV-protection mechanism — within a batch, transaction ordering carries no value, so there’s no sandwich to run. (<a href="https://docs.cow.fi/cow-protocol/reference/contracts/core/settlement">CoW settlement docs</a>)</p>

<p>So my <code class="language-plaintext highlighter-rouge">settle(orderUid)</code> was wrong twice over: the real <code class="language-plaintext highlighter-rouge">settle</code> has a completely different shape, and my contract isn’t a solver and could never call it.</p>

<h2 id="you-approve-the-relayer-not-the-settlement-contract">You approve the relayer, not the settlement contract</h2>

<p>Here’s the subtle one. To trade on CoW you grant your ERC-20 allowance to the <strong><code class="language-plaintext highlighter-rouge">GPv2VaultRelayer</code></strong> (<code class="language-plaintext highlighter-rouge">0xC92E…0110</code>) — <em>not</em> to <code class="language-plaintext highlighter-rouge">GPv2Settlement</code>. Why a second contract? Because <code class="language-plaintext highlighter-rouge">settle</code> executes arbitrary <code class="language-plaintext highlighter-rouge">interactions</code> (calls into external liquidity). If your allowance pointed at the settlement contract, a malicious solver could craft an “interaction” that just calls <code class="language-plaintext highlighter-rouge">transferFrom</code> on your tokens. So CoW puts the allowance on a minimal relayer that <strong>only <code class="language-plaintext highlighter-rouge">GPv2Settlement</code> may call</strong>, and interactions <em>to the relayer are forbidden</em>. Funds can move only as part of a settlement that respects your signed order. (<a href="https://docs.cow.fi/cow-protocol/reference/contracts/core/vault-relayer">vault relayer docs</a>)</p>

<p>My router approved the relayer and then <em>reset the approval to zero in the same transaction</em>, right after a synchronous <code class="language-plaintext highlighter-rouge">deposit</code>. But settlement is <strong>asynchronous</strong> — a solver fills your order minutes later. Resetting the allowance immediately would leave nothing for the relayer to pull when the fill actually happens. The approval has to persist. CoW’s own guidance is a single standing relayer approval.</p>

<figure class="chart">
<svg viewBox="0 0 680 300" role="img" aria-labelledby="cow-t">
<title id="cow-t">How an order goes from intent to settlement on CoW Protocol</title>
<defs>
<marker id="c-arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--accent)" /></marker>
</defs>
<text class="c-title" x="20" y="24">Intent → batch → solver competition → settlement</text>
<rect class="c-box" x="20" y="50" width="172" height="56" rx="6" />
<text class="c-val" x="106" y="72" text-anchor="middle">you authorize</text>
<text class="c-label-sm" x="106" y="90" text-anchor="middle">sign / presign an intent</text>
<line class="c-arrow" x1="194" y1="78" x2="250" y2="78" />
<rect class="c-box" x="252" y="50" width="150" height="56" rx="6" />
<text class="c-val" x="327" y="72" text-anchor="middle">batch</text>
<text class="c-label-sm" x="327" y="90" text-anchor="middle">orders collected</text>
<line class="c-arrow" x1="404" y1="78" x2="460" y2="78" />
<rect class="c-box-accent c-fill-soft" x="462" y="48" width="198" height="60" rx="8" />
<text class="c-val" x="561" y="72" text-anchor="middle">solvers compete</text>
<text class="c-label-sm" x="561" y="90" text-anchor="middle">most surplus wins</text>
<line class="c-arrow" x1="561" y1="110" x2="561" y2="150" />
<rect class="c-box" x="372" y="152" width="288" height="48" rx="6" />
<text class="c-label-sm" x="516" y="180" text-anchor="middle">winner settles batch — one uniform price per token</text>
<line class="c-arrow" x1="372" y1="176" x2="316" y2="176" />
<rect class="c-box" x="20" y="152" width="296" height="48" rx="6" />
<text class="c-label-sm" x="168" y="173" text-anchor="middle">relayer pulls your tokens (only now,</text>
<text class="c-label-sm" x="168" y="189" text-anchor="middle">only via this settlement)</text>
<line class="c-grid" x1="20" y1="224" x2="660" y2="224" />
<text class="c-label-sm" x="20" y="250">You approve the RELAYER, never settlement — so a solver's arbitrary calls can't touch your funds.</text>
<text class="c-label-sm" x="20" y="272">No on-chain swap to front-run: within a batch, ordering carries no value.</text>
</svg>
<figcaption>The user authorizes an intent; solvers compete; the winner settles the whole batch at uniform clearing prices, and only then does the relayer pull funds — bounded to that settlement.</figcaption>
</figure>

<h2 id="a-contract-presigns-it-doesnt-settle">A contract presigns; it doesn’t settle</h2>

<p>If you can’t call <code class="language-plaintext highlighter-rouge">settle</code>, how does a <em>contract</em> place an order? CoW’s <code class="language-plaintext highlighter-rouge">GPv2Signing</code> mixin allows four schemes: an EOA’s <strong>EIP-712</strong> or <strong>EthSign</strong> signature, a smart contract’s <strong>ERC-1271</strong> <code class="language-plaintext highlighter-rouge">isValidSignature</code>, or <strong>PreSign</strong> — calling <code class="language-plaintext highlighter-rouge">setPreSignature(orderUid, true)</code> on-chain to flag an order as authorized. (<a href="https://github.com/cowprotocol/contracts/blob/main/src/contracts/mixins/GPv2Signing.sol">GPv2Signing source</a>)</p>

<p>That’s the hook my executor needed. Each slice is its own order (its own <code class="language-plaintext highlighter-rouge">validTo</code> window); when a slice comes due, the contract <strong>presigns</strong> that slice’s <code class="language-plaintext highlighter-rouge">orderUid</code> and leaves the rest to solvers. The whole fix was: approve the real relayer once, presign per slice, and <em>delete the <code class="language-plaintext highlighter-rouge">settle</code> call entirely</em>. The contract authorizes; the network settles. I also added a <code class="language-plaintext highlighter-rouge">revokePresignature</code> so cancelling can kill a presigned-but-unfilled slice — the honest caveat being that an in-flight slice stays fillable until its <code class="language-plaintext highlighter-rouge">validTo</code> otherwise.</p>

<h2 id="two-ways-to-twap-and-when-to-roll-your-own">Two ways to TWAP, and when to roll your own</h2>

<p>The above is a <em>self-hosted</em> TWAP: your own keeper presigns slices on a timer. CoW also ships an official TWAP, and it’s worth knowing why it exists. It’s not a bespoke contract — it’s a <strong>conditional order</strong> on <a href="https://docs.cow.fi/cow-protocol/reference/contracts/periphery/composable-cow">ComposableCoW</a>. You register one order with a handler implementing <code class="language-plaintext highlighter-rouge">getTradeableOrder(...)</code>; CoW’s <strong>watchtower</strong> calls that each block to get the part valid <em>now</em> and posts it, and the settlement contract calls the handler’s <code class="language-plaintext highlighter-rouge">verify(...)</code> through ERC-1271 so a solver can only ever fill the part the handler currently authorizes. That’s <strong>validated discretization</strong> — the chain itself enforces the schedule — plus on-chain cancellation and a keeper you don’t have to run.</p>

<p>A naive splitter gives all of that up: it trusts your off-chain service to presign the right thing, and any already-presigned part stays fillable until it expires. So I built both — a fixed <code class="language-plaintext highlighter-rouge">CowTwapExecutor</code> <em>and</em> a faithful ComposableCoW <code class="language-plaintext highlighter-rouge">IConditionalOrder</code> TWAP handler (part scheduling, <code class="language-plaintext highlighter-rouge">span</code> windows, the watchtower’s poll/abort signals, the validation guards) — to have the self-hosted path <em>and</em> the framework path side by side. The honest default is the framework; you roll your own only when you need logic the handler can’t express, and even then the right move is a new handler, not an off-chain loop.</p>

<h2 id="the-lesson">The lesson</h2>

<p>The contract compiled and its tests were green the whole time it was calling functions that don’t exist. Mocks test your code against <em>your model</em> of the protocol; they can’t tell you the model is fiction. The fix wasn’t really Solidity — it was reading <a href="https://github.com/cowprotocol/contracts">the actual contracts</a> until the shape of a real settlement (intent, relayer, presign, solver) replaced the shape I’d assumed. It’s the same theme as <a href="/blog/the-on-chain-randomness-landscape/">getting randomness right</a> and <a href="/blog/what-a-zk-proof-proves/">binding a ZK proof</a>: the hard part isn’t the code you write, it’s checking it against the system it actually has to live in.</p>

<p>The fix, the handler, and the 26 tests are in <a href="https://github.com/0xSoftBoi/cowswaprouter">the repo</a>.</p>]]></content><author><name>Tsolmondorj Natsagdorj</name></author><category term="cow-protocol" /><category term="defi" /><category term="solidity" /><category term="mev" /><category term="intents" /><summary type="html"><![CDATA[My TWAP router for CoW Protocol compiled, passed its tests, and called two functions that don't exist on mainnet. Fixing it meant actually learning how CoW settles: intents not swaps, why you approve the relayer and not the settlement contract, and why a smart contract presigns its orders instead of settling them.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://0xsoftboi.github.io/assets/og/how-cow-protocol-settles.png" /><media:content medium="image" url="https://0xsoftboi.github.io/assets/og/how-cow-protocol-settles.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>