惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

DEV Community

Quick Tip: Benchmarking Multimodal APIs in Under 10 Minutes How I Slashed My AI API Bill by 92% in 2026 — A Cost Optimizer's Speed Benchmark Guide How I Slashed My AI API Bill by 95% — A Practical Guide for 2026 A Go outbox library that runs inside your own DB transaction How I Built a Credit Optimizer That Saves 30-75% on AI Agent Costs (Open Architecture) The Moment the Config Parser Became the Bottleneck Churn Tool Stack by Revenue Stage ($5K to $50K+) What I Learned Exploring AI-Generated 3D: A Hands-On Tour of Meshy, Tripo, and Three.js Day 15 - Software Composition Analysis(SCA) Contributing Upstream Instead of Forking: My grape-swagger-rails Story Behind The Badge: How We Built 2,000 Hackable Badges For Temporal Replay Access Control Doesn't Scale Linearly -- Part 3 33x faster than Rust: Why I stopped waiting for my compiler and built my own. I Built My First Production AWS Project as a Career Changer Why Detecting PII Matters More Than Ever JSON Schema in 10 Minutes — Validation, Types & Real Examples Python Tasks How I Started My Cybersecurity Journey as an SQA Engineer 🔐 Why "fancy fonts" in Discord and Instagram bios turn into boxes ☁️ GKE private cluster setup — common mistakes and how to avoid them I Thought a Username Didn’t Matter… Until I Saw How Much People Care About It Claude for Small Business: 382K Day-One Buyer's Guide I Built a Diagnostic Toolkit for PyTorch Because I Was Tired of Guessing Why Models Fail How I Built an AI-Powered Incident RCA Platform with LangGraph and RAG The Paywall Was a Painted Door Sonnet hallucinated. My agent stored it as fact. How React-Style Time-Slicing Keeps UIs Responsive 这个 Princeton 开源项目让 AI 自己修 Bug,19K Stars 但 90% 的人只用了 1% 功能 🔥 SWE-agent's 5 Hidden Uses Nobody Told You About 🔥 Decompiling Serial Number U-36: Python TERCOM Reconstruction, Cryptographic Logistical Forensics, and Swarm Consensus Fault Tolerance Microservices Patterns You Cannot Outrun a Wave I Fired My Entire Node.js Stack — Rust Rebuilt It in 3 Weeks (The Ugly Truth) BoxAgnts Introduction (2) — AI Agent Toolbox Cursor 3 ships parallel AI agents. Here is the multi-agent workflow that actually works. Prisma-7 A Complete Beginners Guide (With Free Cloud Database!) Akses HDD Rumah dari Laptop Kantor Pakai Tailscale + SMB (Tanpa VPN Ribet) Content Pipeline in MonoGame: Why I Don't Use It Debug Log #1 — The Pipeline That Looked Broken Data Structures in JavaScript: When to Use What (2026) BGP Route Flap Damping: A Solution or a New Problem? First look at AWS DevOps Agent The Next Big “Cult App” Probably Isn’t Another Social Media Platform From Template to Production-Shaped: An AI-Native Dev Flow for Go Side Projects Idempotency Keys: The API Pattern That Saves You From Duplicate Payments and Phantom Records Everyone's Building Jarvis. Nobody's Even Close. The Moment the Jaeger Tracer Exhausted Itself and What We Switched To How to Fix Tool-Use Loops in Autonomous Coding Agents Months of self-testing: Citations shine, other features remain unproven. Claude Code for Canary Deployments: How I Ship to 1% of Users Before Breaking Everything Your recurring scraper is re-downloading data that didn't change. Here's the 15-line fix (conditional GET) 20 Years of GPUs in Numbers: How FLOPS & TDP Grew, and Who Led the NVIDIA vs AMD Race (open dataset, 13.5k GPUs) Espressif Reveals CoreBoard and Korvo Dev Kits for ESP32-S31 Composable Abstraction Layer: o pattern que faltava entre Pinia e seus componentes Vue Your GitHub Actions Logs Are Leaking LLM Keys and Your SIEM Isn't Catching It Solving Complex Logic with Claude and Research Papers Building TheEpicBook: A Deep Dive into a Node.js Monolithic Web Application Haber yazilimi, haber scripti, haber sistemi: ayni urun, uc ayri arama niyeti Predicting Blood Glucose Fluctuations: Building a Transformer-based CGM Forecaster with PyTorch & InfluxDB Pre-task hooks: the one-line wire-up that gives your Hono agent shared memory Concurrent writes to a shared agent memory: what we shipped, what we punted on Building a Production Serverless URL Shortener on AWS — 21 Articles, Every Test Run for Real My CKA Cheat Sheet: Commands, Aliases, and Documentation Tricks I Used During the Exam Frontend Engineering Beyond Pixels: The Architecture of Digital Accessibility VLA or IL? A Controlled Dataset for Testing Whether Finetuning Turns Your VLA into a Fancy Imitation Learner Fabric AI Functions Turn GenAI Into a Data Pipeline Step Proximate vs Ultimate: The Bug Is Never Just the Bug The Treasure Hunt Engine That Broke Before the Traffic Did Reset Windows Update: The Definitive MSP Guide to RWU Your Resume Was Never Built for This AI Writes 46% of Code Now: What Snap's Layoffs Mean for Developers in 2026 From Chatbot to Agent — Tool Calling with NVIDIA NIM Fatigue and Fracture Mechanics: Why Parts Break Below Their Yield Strength I built a token-level debugger for comparing two LLMs VCP-Virtual Private Cloud Embedding sing-box in an iOS messenger to bypass Russian DPI (no VPN) Microsoft Copilot just exfiltrated a company's files. The attack was one email. Here's the mechanism. RAG 시스템 실전 구축 (v42) copilot cloud agent is becoming an automation api Cx Dev Log — 2026-04-23 Why Tesla Is Becoming the AI Enterprise Case Study Every Leader Should Understand ORA-00214 오류 원인과 해결 방법 완벽 가이드 SpecAgnt v2.0: The Agent Lifecycle Framework for AI-Native Engineering Optimizing Signal Latency and Weight Allocations in Algorithmic Pipelines SSH Under the Hood: Protocols, Mechanisms, and the Full Technical Story دليل بوابات الدفع للتاجر العربي في 2026 (وكيف تختار المناسبة لمتجرك) Cómo Mi Configuración de Docker Me Salvó de un Ataque de Supply Chain (Y Por Qué la Tuya Debería Hacerlo También) How My Docker Setup Saved Me From a Supply Chain Attack (And Why Yours Should Too) Astro: The epitome of SEO Technical Update I Gave My AI Agent the Ability to Research Before It Writes — Here’s What Changed Kubernetes sem Cloud Provider (Parte 2): Criando Operators em Go para automação e self-service de plataforma AI Memory Needs an Authority Policy, Not Just More Context You've done tutorial after tutorial. Your GitHub is still empty. (Free 1‑page PDF, no signup) TypeScript 7.0: The Go Compiler That Makes TS 10x Faster Connecting Wallets the Right Way: wagmi v2 and EIP-6963 The 5-Layer Architecture Every Production Multi-Agent System Needs (And Why Most Skip Layers 4 and 5) CSS Scroll-Driven Animations: No JavaScript Required Vite 8 + Rolldown: Rust-Powered Builds That Are 10–30x Faster Core Architectural Components of Azure
The Missing POP: How I Ported a Yul Contract to Huff by Reading Every Opcode
Andrei Mashu · 2026-05-26 · via DEV Community

A war story about hand-managing the EVM stack, two words of litter left behind a CALL, and the debug trace that finally made the drift visible.


I had a contract that worked. Then I rewrote it in a second language so it would behave the same way, faster — and it didn't. The bug wasn't a crash. It was worse: the contract ran to completion, returned status 1, and the value it produced was quietly wrong.

This is the story of that bug, the tool that caught it, and what porting an opcode dispatcher from Yul to Huff taught me about the one thing Yul had been doing for me the whole time without ever saying so.

Two implementations of the same thing

The contract is an opcode dispatcher — it reads a packed byte-stream of commands and routes funds through multi-hop swap paths across seven DEX families. The whole thing is open source (adaptive-mev-router on GitHub), so every snippet below can be read in full context. The detail that matters for this story is that it ships twice: once in Yul as the reference implementation (MEV_V2.yul), and once in Huff as a hand-optimised port (MEV_V2.huff) with an O(1) jump-table dispatch.

Why two? Yul is readable and gives me a reference I can trust. Huff lets me shave the contract down opcode by opcode and control the dispatch path exactly. The deal is that both must behave identically — and the test suite enforces it. The harness loads both builds as two variants and runs every scenario against each:

function loadVariants() {
  const MEVJson = require("../artifacts/contracts/MEV_V2.yul/MEV_V2.json");
  const huffRuntime = fs.readFileSync(huffBinPath, "utf8").trim();
  return [
    { name: "Yul",  abi: MEVJson.abi, bytecode: MEVJson.bytecode },
    { name: "Huff", abi: MEVJson.abi, bytecode: wrapRuntimeBytecode(huffRuntime) },
  ];
}

loadVariants().forEach((variant) => {
  it(`MEV_V2 [${variant.name}]: ...`, async function () {
    // every scenario runs once for Yul, once for Huff
  });
});

Enter fullscreen mode Exit fullscreen mode

If Yul and Huff ever disagree, CI goes red. That harness is the hero of this story. It turned a vague "something feels off" into a precise, reproducible failure. But before it could do that, I had to write the Huff version. And the Huff version is where I met the stack.

What Yul never told me

Here is the thing I didn't fully appreciate until I left it behind: Yul manages the stack for you.

Look at one swap handler in the Yul reference. This is the entire V2 adaptive swap, zero-for-one:

function swap_v2_adaptive_zfo(cursor) {
    let sig      := shr(224, calldataload(cursor))
    let feeBps   := and(shr(240, calldataload(add(cursor, 4))), 0xFFFF)
    let pair     := shr(96,  calldataload(add(cursor, 6)))
    let tokenIn  := shr(96,  calldataload(add(cursor, 26)))
    let amountIn := shr(144, calldataload(add(cursor, 46)))

    amountIn := resolve_amount(amountIn, tokenIn)            // 0 -> balanceOf(this, tokenIn)
    let amountOut := v2_compute_amount_out(pair, amountIn, feeBps, 1)

    transfer_token(tokenIn, pair, amountIn)
    swap_v2(sig, pair, 0, amountOut, address())
}

Enter fullscreen mode Exit fullscreen mode

Five named values. When I write swap_v2(sig, pair, 0, amountOut, address()), the compiler decides where sig, pair, amountOut live, in what order they get pushed, which DUP retrieves each one, and when they get cleaned up. I think in named values. The compiler thinks in stack slots. I never have to know the translation.

Huff removes that layer. In Huff you are the compiler's stack allocator. There are no names — there is a column of 32-byte words, and you address them by how far down they sit right now. Here is the same swap in the Huff port — and notice the comments running down the right side, the stack diagram after every single instruction:

// SWAP_V2 expects [sig, pair, amount0, amount1, to]
// zfo: amount0=0, amount1=amountOut
address                                  // [to, cursor, limit]
0x620 mload                              // [amountOut, to, cursor, limit]
0x00                                     // [0, amountOut, to, cursor, limit]
dup4 0x06 add calldataload 0x60 shr      // [pair, 0, amountOut, to, cursor, limit]
dup5 calldataload 0xe0 shr               // [sig, pair, 0, amountOut, to, cursor, limit]
SWAP_V2()                                // [cursor, limit]

Enter fullscreen mode Exit fullscreen mode

Those comments are not decoration. In Huff the stack layout is the program state, and dup4 only fetches the right value if pair is genuinely four slots down at that exact instruction. There is one more tell in that snippet, and it's the heart of this whole story: 0x620 mload.

When the stack isn't enough — and why that's the warning sign

In Yul, amountIn and amountOut are just locals; the compiler keeps them alive across the whole function for free. In the Huff port I couldn't do that. The V2 swap has to call getReserves() on the pair halfway through to compute amountOut — and that staticcall writes its result into scratch memory, and the reserve math needs a deep working stack of its own. Trying to also balance amountIn and amountOut on top of all that, reachable by dup, across dozens of intervening opcodes, is exactly the kind of bookkeeping that breaks.

So in Huff I spilled them to memory on purpose:

dup1 0x600 mstore     // save amountIn  — getReserves() is about to clobber scratch memory
...
dup1 0x620 mstore     // save amountOut — survive until the transfer + swap at the end

Enter fullscreen mode Exit fullscreen mode

That decision — this value lives too long and travels too deep, put it in memory — is one Yul made silently for me every time. In Huff it's a conscious call, and getting it wrong is a real bug. Which brings me to the bug.

The macro that leaves litter on the stack

Most of the swap macros in the contract are clean: they take their inputs, push them into the right memory slots, and consume everything. SWAP_CURVE_EXEC is the textbook case — five inputs in, every one of them spent, nothing left behind.

Then there's the native-ETH variant. A Curve swap that sends ETH has to pass the amount twice: once written into memory as a call argument, and once as the actual msg.value of the call. Which means, unlike every other swap macro, it cannot just consume pool and amount — it has to keep them alive on the stack until the call itself. Here is the real macro:

#define macro SWAP_CURVE_ETH_EXEC() = takes(5) returns(0) {
    0xe0 shl 0x00 mstore   // sig<<224 at mem[0]. stack: [pool, sellId, buyId, amount]
    swap1 0x04 mstore      // sellId at mem[4]. stack: [pool, buyId, amount]
    swap1 0x24 mstore      // buyId at mem[36]. stack: [pool, amount]
    dup2 0x44 mstore       // amount at mem[68], keep both on stack. stack: [pool, amount]
    0x00 0x64 mstore       // minOut = 0. stack: [pool, amount]
    // call(gas, pool, amount, 0, 132, 0, 32) — amount as msg.value
    0x20 0x00 0x84 0x00    // stack: [0x00, 0x84, 0x00, 0x20, pool, amount]
    dup6 dup6              // stack: [pool, amount, 0x00, 0x84, 0x00, 0x20, pool, amount]
    gas call
    iszero err jumpi
    pop pop                // clean up leftover pool and amount from dup6 dup6
}

Enter fullscreen mode Exit fullscreen mode

Read the last four lines slowly, because they are the whole problem in miniature.

dup6 dup6 reaches deep down the stack and copies pool and amount to the top, because call needs them there as arguments. call consumes its seven inputs and pushes one result. But the originals — the pool and amount that dup6 dup6 copied from — are still sitting down there. The call didn't touch them. They are litter. And the macro is declared returns(0): it promises to leave the stack exactly as deep as it found it. So the macro has to end with pop pop to sweep that litter away by hand.

That pop pop is not optional and it is not obvious. It exists only because of a dup that happened nine instructions earlier. Forget it, and the macro returns two words heavier than it claims to. Nothing reverts. The next opcode in the dispatcher just finds a stack two slots deeper than its comments assume — and every dup and swap it does from that point on reaches for the wrong neighbour.

The bug that didn't crash

That is exactly the bug I shipped.

Not in SWAP_CURVE_ETH_EXEC itself — that one I'd already gotten right, and the pop pop comment is me having learned the lesson once. The bug was in a different macro, one I wrote later, where I did the same dup-deep-then-call pattern and simply did not realise it had left two originals stranded. I'd internalised "call consumes its arguments" and stopped there. But call consumes the copies dup puts on top. It has nothing to say about the originals dup copied from. Those are mine to clean up, and that time I didn't.

Here is what made it vicious: nothing reverted.

The EVM doesn't know a leftover pool from a meaningful value from any other 32-byte word. It's all just words. The macro returned two words heavier than its returns(0) signature claimed. The dispatcher continued, every stack comment from that point on now describing a stack two slots shallower than reality, and the next dup fetched a word two places off from the one I wanted — a different address entirely. The swap was issued with a wrong argument, the transaction ran to the end, and it returned status 1. Success.

The Yul variant of the same scenario returned the correct result. The Huff variant returned a different one. The forEach harness caught the divergence and turned CI red — but all it could tell me was that the two disagreed, not where. I had a contract producing a wrong answer with no revert, no error, no line number.

Reading every opcode

You cannot reason your way out of this from the source. The whole problem is that your reasoning about the stack is what's broken — re-reading the macro just reproduces the same wrong mental model. You need ground truth.

Ground truth is the execution trace. I ran the failing Huff scenario under a debug tracer and dumped the step-by-step opcode log: every instruction executed, and crucially, the stack contents after each one.

Then I did something tedious and completely worth it. I walked the trace one opcode at a time, and beside each line I wrote what I expected the stack to be. Two columns: what the trace said, what I thought.

For a while the columns matched — PUSH, PUSH, CALLDATALOAD, fine. Then I reached the CALL inside the offending macro. On the line after it, the trace still carried two words my map had already discarded. The columns diverged by exactly two slots, and they never re-converged — every subsequent line was off by the same two.

That was the whole bug, sitting in the diff between two columns: a missing pop pop. Two characters. The fix took seconds. Finding it took the trace.

The habit that was already half there

Here's the part I'm slightly embarrassed by: the fix wasn't a new technique. It was doing the thing I was already half-doing, properly.

My Huff already had stack comments. Some of them were even notes-to-self mid-calculation — at one point in the V2 amountOut math I'd literally written, inline:

0x2710 sub   // stack was [feeBps, ...], push 0x2710 -> [0x2710, feeBps, ...],
             // sub -> 0x2710-feeBps. Correct!

Enter fullscreen mode Exit fullscreen mode

That comment is me reverse-engineering my own stack in real time and verifying it. I had the discipline in places. What I lacked was the discipline everywhere — and a missing pop pop is precisely what a lapse looks like. In the macro that bit me, I'd written the stack diagram down the right margin, then changed the instructions during a later edit and didn't re-derive the diagram beneath the call. The comment said one thing; the opcodes did another.

So the lesson wasn't "start commenting the stack." It was: the stack comment is code. It is the source of truth your dup/swap indices are read from, and it has to be updated with the same discipline as the instructions themselves. An out-of-date stack comment is exactly as dangerous as an out-of-date mental model — because it is one, just written down.

Concretely, the rules I now hold myself to:

  • Every line that touches the stack updates the diagram on that line. Not the top of the macro — every line. Top-of-stack on the left.
  • When you need a value, read its depth off the current comment and count. Never count from memory. The comment is authoritative; the dup index just obeys it.
  • dup copies; it does not move. Every dup-deep-then-call pattern leaves the originals stranded below — call only consumes the copies on top. If you dup to reach call arguments, you almost certainly owe a matching pop afterwards. SWAP_CURVE_ETH_EXEC's trailing pop pop is that debt, paid.
  • A macro's takes/returns signature is a contract — verify it. returns(0) means the stack must be exactly as deep on exit as on entry. Walk the macro and prove it. A macro that secretly returns two words heavy corrupts every caller downstream.
  • When a value lives long or travels deep, spill it to memory — like 0x600/0x620 for amountIn/amountOut. If keeping it on the stack feels fragile, that feeling is correct; that's the Yul compiler's job knocking, and in Huff the job is yours.
  • A wrong answer with clean execution? Suspect the stack first. A revert usually means a bad jump or a failed call. A wrong result with status 1 is the signature of a stack that drifted — leftover litter, or a dup/swap that grabbed the wrong neighbour.

Why the trace beat everything else

The bug was an invisible disagreement between my model of the stack and the EVM's actual stack. Source review can't fix that — the review is done by the same broken model that wrote the bug. A debugger that shows only values doesn't help much either, because every value is a 32-byte word and they all look alike; a wrong address is indistinguishable from a right one until you know which slot it should have come from.

What the opcode-level trace gives you is the shape of the stack at every step, independent of your assumptions. It's the one artifact in the toolchain that doesn't share your mental model. Line it up against your expectations and the divergence point is the bug — not "near the bug," the bug, the exact instruction where reality and intention split.

What I'd tell anyone starting with Huff

Huff is wonderful for what it is: total control, no compiler between you and the bytecode, every opcode chosen by you. But "no compiler between you and the bytecode" means the compiler's stack allocator is now a job on your desk, and it is a real job with a real failure mode.

So:

  1. Respect what the high-level language was doing for you. Yul's stack management isn't a convenience — it's an entire correctness layer. Take it over deliberately.
  2. Maintain the stack diagram as code. Inline, every line, updated as rigorously as the instructions. Your dup/swap indices are reads from that diagram.
  3. When behaviour diverges and nothing crashes, go straight to the opcode trace. Don't re-read the source. Walk the trace beside your expectations and find the slot where they part.
  4. Keep a reference implementation and test against it relentlessly. The Yul-vs-Huff forEach harness didn't find the bug for me, but it's the reason I knew there was one. An executable specification you can't argue with beats any amount of careful reading.

The payoff: Yul vs Huff, measured

Once the two implementations agreed byte-for-byte, the harness handed me something else for free. Every scenario runs against both variants and logs receipt.gasUsed, so I got a direct, apples-to-apples gas comparison — same test, same calldata, two compilers.

Huff's hand-built O(1) jump table wins consistently on dispatcher-heavy opcodes; on I/O-dominated swaps the two land within a handful of gas. A selection of measured numbers (Solidity 0.8.28, EVM Cancun, viaIR, optimizer runs=200):

Operation Yul Huff Δ (Huff − Yul)
0x02 V3 swap zfo (amount=0) 56 851 56 800 −51
0x04 Balancer V2 zfo (amount=0) 81 794 81 644 −150
0x08 wrap_weth (adaptive) 37 907 37 782 −125
0x0A unwrap_weth (adaptive) 34 097 33 921 −176
0x0B transfer_eth 31 454 31 290 −164
0x0C transfer_erc20 50 249 50 009 −240
0x0D balance_check 27 756 27 504 −252
0x0E sweep 33 775 33 458 −317
0x19 Balancer V1 zfo (amount=0) 82 730 82 257 −473
0x1A Balancer V1 ofz 82 880 82 313 −567
0x1B Fluid zfo (amount=0) 72 234 71 624 −610
0x1C Fluid ofz (amount=0) 55 171 54 535 −636
0x1D DODO zfo (amount=0) 86 338 85 696 −642
0x1E DODO ofz (amount=0) 69 298 68 640 −658
V2 flash 3-hop chain 146 486 145 781 −705
Sandwich backrun (resolve + check + sweep) 89 950 89 152 −798

Negative Δ means Huff is cheaper. The gap widens exactly where you'd expect: the more dispatching a scenario does relative to actual I/O, the more the hand-built jump table pulls ahead. The 3-hop flash chain and the sandwich backrun — the dispatcher-heaviest scenarios — show the biggest savings, around 700–800 gas.

But notice what every row in that table depends on. The numbers are only meaningful because the behaviour column is identical first. A faster implementation that returns a different answer isn't an optimisation — it's the bug I spent this whole article describing. The gas win is real, but it's a footnote to the actual achievement: two independent implementations, in two languages, that a test suite cannot tell apart.

The fix to my bug was two characters: a pop pop that should have been there and wasn't. I think about that a lot. The cost of the mistake and the cost of the fix were wildly mismatched, and the only thing that closed the gap between them was being willing to read every single opcode until the stack told me the truth.

In Huff, the stack always tells you the truth. You just have to be looking at it instead of at your idea of it.


The full contract — both the Yul reference and the Huff port, the forEach parity harness, the fork tests, and the gas-diff CI — is on GitHub: github.com/AndreyMashukov/adaptive-mev-router. The SWAP_CURVE_ETH_EXEC macro in this article lives in contracts/MEV_V2.huff; its Yul counterpart is in contracts/MEV_V2.yul. Stars and issues welcome — and if you spot a stack comment that's drifted, you now know exactly what to look for.