<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Cethology</title>
        <link>https://paragraph.com/@cethology</link>
        <description>Technical deep dives for those navigating the turbulent waters of Ethereum.</description>
        <lastBuildDate>Tue, 10 Nov 2026 00:02:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Cethology</title>
            <url>https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/2dc92e0c3c780c2af39d7f39b02e0b982c7af8bffd1edd83680d67f65a2019c5.jpg</url>
            <link>https://paragraph.com/@cethology</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Account Abstraction: The Story So Far]]></title>
            <link>https://paragraph.com/@cethology/account-abstraction-the-story-so-far</link>
            <guid>5e9M26rZilatMZEek6um</guid>
            <pubDate>Tue, 09 Jun 2026 15:10:46 GMT</pubDate>
            <description><![CDATA[In April of 2016, issue #86 was opened in Ethereum's EIPs repository. Entitled Proposed initial abstraction changes for Metropolis, this was the first major proposal trying to solve the problem of account abstraction (AA). Ten years and more than eight thousand EIPs later, EIP-8141 (Frame Transactions) was proposed as the headliner for the Hegota upgrade. This article tells the story of how we went from EIP-86 to EIP-8141 and what it took to get there.]]></description>
            <content:encoded><![CDATA[<div data-type="x402Embed"></div><p>In April of 2016, issue #86 was opened in Ethereum's EIPs repository. Entitled <em>Proposed initial abstraction changes for Metropolis</em>, this was the first major proposal trying to solve the problem of account abstraction (AA). Ten years and more than eight thousand EIPs later, EIP-8141 (Frame Transactions) was proposed as the headliner for the Hegota upgrade. This article tells the story of how we went from EIP-86 to EIP-8141 and what it took to get there.</p><p>My goal here isn't to enumerate and briefly explain each proposal. Instead, I want to give a better sense of the problem itself: what trade-offs are involved, how the thinking evolved over time, and why we are still talking about it. While I'm not assuming any familiarity with AA, I do expect the reader to have some basic Ethereum knowledge, such as the difference between EOAs and contracts, the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://paragraph.com/@cethology/deterministic-deployments-part-2-create2-based-approaches">CREATE2</a> opcode, and how nonces work. There are other concepts we'll need, but we'll cover them before we start.</p><div data-type="subscribeButton" class="center-contents"><a class="email-subscribe-button" href="https://paragraph.com/@cethology/subscribe">Subscribe</a></div><h1 id="h-basics" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Basics</h1><p>This section introduces some basic concepts that are important to understand the rest of the article. We'll also start building a mental framework that will help us think about the different account abstraction proposals we'll explore.</p><h2 id="h-whats-not-abstract-about-accounts" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">What's not abstract about accounts?</h2><p>Let's start with a simple example to explain how Ethereum accounts work and what their limitations are. We'll come back to this example many times throughout the article to explain different AA mechanisms.</p><p>Alice wants to send 1 ETH to Bob, who has never interacted with Ethereum before. Bob wants to receive that ETH and then transfer 0.1 of it to Charlie. Assuming Bob wants to use an EOA (the default path for most new users), this is what will happen next:</p><ol><li><p>Bob generates an ECDSA pair and derives a public address from it. He shares the address with Alice, and she sends the ETH.</p></li><li><p>Once Bob receives the ETH from Alice, he builds a transaction that looks like this: <code>{ to: charlie, value: 0.1 ETH, nonce: 0, gasLimit: 21000, gasPrice: X }</code>. He signs that transaction with the private key from the first step, and then sends the signed transaction to some node. The transaction is propagated through the network.</p></li><li><p>Nodes that see Bob's transaction perform some checks to verify that it's valid. For example, they:</p><ul><li><p>Check that the signature is valid and use it to recover the address of the sender (Bob, in this example).</p></li><li><p>Check that Bob's balance is at least <code>gasLimit * gasPrice</code>, since that's the maximum amount of ETH he could end up spending from the transaction.</p></li><li><p>Check that the nonce of the transaction matches the nonce of Bob's account.</p></li></ul><p>If any of these checks fail, the transaction is dropped. Otherwise, the nodes add it to their mempool.<sup id="fnref:1"><a class="footnote-ref" data-id="1d1d7e11-0392-42ee-8e54-53453735531f" href="#fn:1" data-reference-number="1">1</a></sup></p></li><li><p>Eventually, a block builder creates a block that includes Bob's transaction. Once that happens, Bob and Charlie can see that the 0.1 ETH was transferred.</p></li></ol><p>There are many things in this example that are "hardcoded" at the protocol level:</p><ul><li><p>An EOA is managed by a single, unchanging ECDSA key.</p></li><li><p>Access is all-or-nothing: if you have the private key, you can do anything you want with the account; if you don't have it, you can do nothing at all.</p></li><li><p>The account sending a transaction and the account paying for its gas are always the same.</p></li><li><p>The block builder is always paid in ETH.</p></li><li><p>Replay protection is handled by a single, monotonically increasing nonce.</p></li></ul><p>These aspects rule out several use cases: you can't rotate the key of an EOA, allow other people to use it within some limits, or have some social recovery mechanism in case you lose your key. Applications can't sponsor the gas cost of transactions for users. UX mechanisms like batching are not possible. The list goes on.</p><p>There's also another big problem: it's widely assumed that ECDSA will be broken by quantum computers at some point in the future. This means that all EOAs, as they exist today, will eventually be compromised. This is an existential risk that needs to be fixed sooner or later (and sooner would be nice).</p><h2 id="h-what-about-smart-wallets" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">What about smart wallets?</h2><p>Instead of an EOA, it's always been possible to use a contract that acts as a wallet. These "smart wallets" fix every problem we mentioned above: you can have multisig schemes, use complex authorization mechanisms, rely on cryptographic primitives other than ECDSA, etc.</p><p>The problem is... who calls the smart wallet? That call, wherever it comes from, is ultimately part of a transaction, and transactions can only be initiated by EOAs.</p><p>Smart wallets <em>are</em> the best solution, because the most powerful way to abstract something is to let it be arbitrary code. But they aren't a complete solution as long as they need EOAs. In that sense, the problem of account abstraction can be reframed as: <strong>how do we initiate actions from smart wallets without using EOAs?</strong></p><p>Now that we have the basics in place, let's start with our story, which takes us back to the beginning of Ethereum itself.</p><h1 id="h-part-1-early-attempts-2015-2020" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Part 1: Early attempts (2015–2020)</h1><blockquote><p>One of Ethereum's goals from the start, and arguably its entire raison d'être, is the high degree of abstraction that the platform offers.</p><p><strong>— Vitalik Buterin, July 2015</strong></p></blockquote><p>The fact that EOAs were too rigid and quantum-unsafe was known even before Ethereum's first block was mined. At the time, though, the separation between EOAs and contracts was deemed a reasonable trade-off to ship the chain sooner. But less than a year after the launch, there was already one proposal addressing the problem.</p><h2 id="h-eip-86-unsigned-transactions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-86: Unsigned transactions</h2><p>In the <em>Basics</em> section, we said that smart wallets fix every account abstraction issue, since they can be used as accounts and implemented in any desired way. The problem would end here if it weren't for the fact that smart wallets still need those pesky EOAs to call them.</p><p>With this in mind, the solution proposed by EIP-86 is laughably simple: just allow unsigned transactions to be mined. The EIP adds a new kind of transaction that doesn't include a signature (and therefore doesn't have a sender), and that builders can include in new blocks. To use a smart wallet, you simply call it with one of these EIP-86 transactions. The data includes everything the wallet needs to verify it's a valid call and to execute the intended action.</p><p>The main question is: why would block builders include these transactions? If they don't have a sender, then there's no one to pay for their inclusion. EIP-86's answer is that wallets themselves can pay the block builder after verifying that the transaction is valid. This is possible because the address of the block builder can be obtained through the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.evm.codes/?fork=osaka#41">COINBASE</a> opcode.</p><p>The following pseudocode shows what an EIP-86 smart wallet would look like. For simplicity, the wallet in this example uses a single ECDSA key, so its functionality is not really different from that of an EOA. But since this is code, the validation could do anything else, like requiring 2 out of 3 signatures from a list of approved addresses, or allowing key rotation.</p><pre data-type="codeBlock" language="python" text="# get the destination, value, gas price, and signature from the transaction data
to = tx.data[0:32]
value = tx.data[32:64]
gasPrice = tx.data[64:96]
sig = tx.data[96:128]

# check the signature is correct
verify(tx.data[0:96], sig)

# figure out how much to pay the block builder and transfer it
gasPayment = tx.gasLimit * gasPrice
transfer(block.coinbase, gasPayment)

# do whatever you want in the rest of the execution; here
# we just make an ETH transfer to some destination
transfer(to, value)"><code><span class="hljs-comment"># get the destination, value, gas price, and signature from the transaction data</span>
to = tx.data[<span class="hljs-number">0</span>:<span class="hljs-number">32</span>]
value = tx.data[<span class="hljs-number">32</span>:<span class="hljs-number">64</span>]
gasPrice = tx.data[<span class="hljs-number">64</span>:<span class="hljs-number">96</span>]
sig = tx.data[<span class="hljs-number">96</span>:<span class="hljs-number">128</span>]

<span class="hljs-comment"># check the signature is correct</span>
verify(tx.data[<span class="hljs-number">0</span>:<span class="hljs-number">96</span>], sig)

<span class="hljs-comment"># figure out how much to pay the block builder and transfer it</span>
gasPayment = tx.gasLimit * gasPrice
transfer(block.coinbase, gasPayment)

<span class="hljs-comment"># do whatever you want in the rest of the execution; here</span>
<span class="hljs-comment"># we just make an ETH transfer to some destination</span>
transfer(to, value)</code></pre><p>To check if a given EIP-86 transaction is valid, a node simply executes it and checks at the end that there was a payment. If there wasn't, it drops the transaction.</p><h2 id="h-alice-bob-and-eip-86" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Alice, Bob, and EIP-86</h2><p>Let's go through our Alice and Bob example to understand how EIP-86 would work in practice. Bob starts by sharing the address of his EIP-86 wallet with Alice, who then sends it 1 ETH (she doesn't care whether Bob's address is an EOA or a contract). Once he receives the transfer, Bob builds a piece of transaction data that looks like this:</p><pre data-type="codeBlock" language="solidity" text="txData = [charlieAddress, 0.1 ETH, gasPrice, sign([charlieAddress, 0.1 ETH, gasPrice])]"><code>txData <span class="hljs-operator">=</span> [charlieAddress, <span class="hljs-number">0</span><span class="hljs-number">.1</span> ETH, gasPrice, sign([charlieAddress, <span class="hljs-number">0</span><span class="hljs-number">.1</span> ETH, gasPrice])]</code></pre><p>where the signature is computed over all the other fields. He then sends this data in an EIP-86 transaction, with his own wallet as the target and some reasonable gas limit:</p><pre data-type="codeBlock" language="json" text="{
  &quot;to&quot;: bobAddress,
  &quot;gasLimit&quot;: 100000,
  &quot;data&quot;: txData
}"><code><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"to"</span><span class="hljs-punctuation">:</span> bobAddress<span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"gasLimit"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">100000</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"data"</span><span class="hljs-punctuation">:</span> txData
<span class="hljs-punctuation">}</span></code></pre><p>When nodes receive Bob's transaction, they execute it to check if it's valid (that is, if it pays the coinbase address). The transaction then stays in the mempool until eventually a block builder includes it in a block.</p><h2 id="h-deployments-with-eip-86" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Deployments with EIP-86</h2><p>Did you notice how I cheated? When we introduced the Alice and Bob example, we explicitly said that Bob had never interacted with Ethereum before. But in the previous section I assumed that he already had a smart wallet whose address he could share with Alice. So let's fix that.</p><p>In principle we seem to have a circular problem: Bob wants to receive some ETH using an EIP-86 wallet but he can't deploy such a wallet if he doesn't have ETH. To make things harder, we are trying to accomplish this without involving an EOA; we want our AA solution to be complete. This seems unsolvable, but we'll actually see a few approaches that work.</p><p>In the case of EIP-86, one solution arises from the following fact about the EVM: when the initialization of a contract (its constructor, in Solidity terms) is executed, it can use whatever ETH is already present in the address where it will be deployed. This means that the wallet can pay the block builder for its own deployment, just as it does for normal transactions.</p><p>With that in mind, Bob can share with Alice the address where he knows his contract will <em>eventually</em> be deployed. Once the address has the ETH that Alice sent, Bob can use an EIP-86 transaction to deploy the wallet. Our problem reduces to how Bob knows where his smart wallet will be deployed.</p><p>Nowadays the answer is obvious: we use some <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://paragraph.com/@cethology/deterministic-deployments-part-2-create2-based-approaches">CREATE2 factory</a> to predict the deployment address. But the <code>CREATE2</code> opcode wouldn't be added to Ethereum until 2019, three years after EIP-86 was proposed. Instead, the EIP itself introduces a CREATE2-like behavior for deployment transactions. This also makes sense if you remember that in normal transactions the deployment address is derived from the sender and its nonce, but in an EIP-86 transaction we don't have either of those! Deriving the address from the init code and some salt is then a natural idea.</p><p>To recap: Bob builds an EIP-86 deployment transaction for his smart wallet but doesn't propagate it yet; he only uses it to know the address where it will eventually be deployed. He shares that address with Alice and, once she sends the ETH, Bob propagates the deployment transaction, whose init code at some point pays the block builder. After all this, Bob can use a normal EIP-86 transaction to send some ETH to Charlie from the just deployed wallet, as we explained in the previous section.</p><h2 id="h-eip-86-problems" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-86 problems</h2><p>Believe it or not, EIP-86 satisfies all of our AA requirements: you can deploy and interact with smart wallets without needing an EOA, and there's no constraint on what those wallets can do. Problem solved? Well, no, because EIP-86 introduces as many issues as it solves.</p><p>A major issue with EIP-86 is that the block builder must execute the whole transaction to figure out if they are going to get paid. One could suggest running it only until a transfer to the coinbase happens, but this wouldn't work: the transaction could fail right at the end and revert its state changes, including the payment. Compare this with a normal EOA transaction, where the validation has a fixed, upfront computational cost and the block builder is paid regardless of whether the transaction reverts.</p><p>Having a fixed, reasonable validation cost is important to prevent certain Denial of Service (DoS) attacks against the network. You can try to clog nodes today by sending a large number of invalid transactions, but the fact that the validation cost is constant means that the problem is manageable. Things change if you allow invalid transactions that can force the node to do unbounded work. With EIP-86 you could send thousands of unsigned transactions that use as much gas as possible and revert right at the end. The node would get overwhelmed by having to run them all. And even if it can take measures against the person doing the DoS, the balance of power shifts in favor of the attacker.</p><p>Another issue is related to refunds. If you send a normal transaction with a gas limit of 100,000 and the execution cost is less than that, the unused gas is "returned" to you at the end. With EIP-86 that's not really possible, because the block builder has no obligation to say "hey, you didn't need to pay that much, here's your change" and forcing this at the protocol level is difficult.</p><p>A third issue has to do with the uniqueness of transaction hashes. In normal transactions, the nonce is unique for each sender, and so the combination of nonce and signature is unique for each transaction. Since a transaction hash is computed over the whole signed transaction, the hashes themselves are therefore unique. That's not the case with EIP-86 because there are no nonces.</p><p>This is, in theory, a solvable problem, because there's nothing at the protocol-level that requires hashes to be unique. In practice, though, many things would break in the upper layers. To give just one example: an API that receives a transaction hash and returns a transaction object would have to be updated to return a list instead. And since that would be a breaking change in itself, every consumer of that API would have to be updated too.</p><p>The idea of breaking hash uniqueness was already considered disruptive in 2016, when Ethereum had just launched. In 2026, the idea of breaking absolutely everything for the niche benefits of nonce abstraction is unthinkable. For this and other reasons, nonce abstraction was eventually dropped as a goal. In the rest of the article, we'll ignore anything related to this, even if in reality many subsequent proposals kept attempting to abstract nonce management.</p><h2 id="h-eip-859-paygas" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-859: PAYGAS</h2><p>The two main problems of EIP-86 are that the cost of validating a transaction is not constant, and that you can't refund unused gas. EIP-859, proposed in 2018, solves both of those problems by adding a new opcode: <code>PAYGAS</code>. This opcode takes a gas price as its input, multiplies it by the transaction gas limit, and pays this value to the block builder from the contract's balance (halting if this balance is not enough).  In a sense, this is a shortcut to what we were already doing manually when we obtained the coinbase address and made a transfer to it. But having an opcode gives us two properties we can't get without changing the EVM:</p><ul><li><p><code>PAYGAS</code> works as a checkpoint. If the transaction fails after <code>PAYGAS</code> was used, the state is reverted to the point when the opcode was executed. The payment to the block builder and any state changes that happened before it are kept.</p></li><li><p>At the end of the transaction, any unused gas is refunded to the account using the same gas price that <code>PAYGAS</code> had as its input.</p></li></ul><p>The smart wallet we used for EIP-86 doesn't need to change that much to work with EIP-859:</p><pre data-type="codeBlock" language="python" text="# get the destination, value, and signature from the transaction data
to = tx.data[0:32]
value = tx.data[32:64]
gasPrice = tx.data[64:96]
sig = tx.data[96:128]

# check the signature is correct
verify(tx.data[0:96], sig)

# use the PAYGAS opcode to pay the block builder
paygas(gasPrice)

# do whatever you want in the rest of the execution; here
# we just make an ETH transfer to some target
transfer(to, value)"><code><span class="hljs-comment"># get the destination, value, and signature from the transaction data</span>
to = tx.data[<span class="hljs-number">0</span>:<span class="hljs-number">32</span>]
value = tx.data[<span class="hljs-number">32</span>:<span class="hljs-number">64</span>]
gasPrice = tx.data[<span class="hljs-number">64</span>:<span class="hljs-number">96</span>]
sig = tx.data[<span class="hljs-number">96</span>:<span class="hljs-number">128</span>]

<span class="hljs-comment"># check the signature is correct</span>
verify(tx.data[<span class="hljs-number">0</span>:<span class="hljs-number">96</span>], sig)

<span class="hljs-comment"># use the PAYGAS opcode to pay the block builder</span>
paygas(gasPrice)

<span class="hljs-comment"># do whatever you want in the rest of the execution; here</span>
<span class="hljs-comment"># we just make an ETH transfer to some target</span>
transfer(to, value)</code></pre><p>This opcode clearly solves our refunds problem. What about the validation time?</p><p>For EIP-86, we suggested that nodes could expect the payment to happen near the start of the execution, and reject transactions that don't pay "soon enough". The problem with this idea was that a transaction could indeed pay close to the beginning and revert later. If we have a checkpointing opcode like <code>PAYGAS</code>, though, that same idea works: the node can ignore anything that happens after it because the rules of the protocol dictate that the payment is irreversible.</p><p>In more concrete terms, nodes can agree on a gas threshold (say, 200,000 gas) within which they expect <code>PAYGAS</code> to happen. If the opcode is not executed within that limit, the transaction is considered invalid and dropped. Crucially, this is a rule about the mempool, not the protocol. This distinction matters in the context of AA, so let's dive deeper into it.</p><h2 id="h-mempool-rules" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Mempool rules</h2><p>It will soon become clear that issues arising from transaction validation and the mempool are the main source of complexity in account abstraction. To better understand why, let's see how the mempool works in practice.</p><p>If you've been in Ethereum long enough, you're likely familiar with the concept of bumping a transaction: you send something with a gas price that happens to be too low, your transaction takes too long to be included, and you send another transaction with the same fields but a higher gas price. The latter transaction "replaces" the previous one in the mempool and the new one is included in a block faster.<sup id="fnref:2"><a class="footnote-ref" data-id="352e2de2-5e0f-4123-841f-f0d2b9692a1a" href="#fn:2" data-reference-number="2">2</a></sup></p><p>Did you know, though, that some nodes will only accept the new transaction if the new gas price is at least 10% higher than the previous one? There is a reason for this<sup id="fnref:3"><a class="footnote-ref" data-id="c9eea395-a95e-43cd-abdf-65da5c7d424c" href="#fn:3" data-reference-number="3">3</a></sup>, but the important part for us is that there's nothing in the protocol rules that says things should work that way. To illustrate this idea, imagine that a node has a pending transaction T with gas price X and then it receives a newly built block with a transaction that replaces T and has gas price Y, where Y is only 1% greater than X. The node can't say "that gas bump was under 10%, rejected!" There's nothing invalid in the block.</p><p>The same distinction applies to EIP-859 and the gas threshold within which <code>PAYGAS</code> should be used. Each node can decide how to set that threshold, but that doesn't mean they would reject blocks that execute <code>PAYGAS</code> after it; this only matters when deciding which pending transactions are valid.</p><p>In summary, we have two kinds of rules: "hard" protocol rules, which all nodes <em>must</em> follow if they want to participate in the network, and "soft" mempool rules where each node can do what they want but where EIPs might suggest useful guidelines.<sup id="fnref:4"><a class="footnote-ref" data-id="30cf6f46-eae9-4804-9143-a1541e2d23ae" href="#fn:4" data-reference-number="4">4</a></sup></p><h2 id="h-the-multi-invalidation-problem" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The multi-invalidation problem</h2><p>EIP-859 seems like a keeper: it adds support for account abstraction and it doesn't seem to have DoS vectors related to transaction validation. But it has a huge DoS vector related to transaction <em>invalidation</em>, something it took the community a while to realize.</p><p>To understand this, imagine an EIP-859 wallet with a function that sends ETH to some address only if the balance of that address is zero:</p><pre data-type="codeBlock" language="solidity" text="function transferIfNoBalance(address to, uint value, uint gasPrice, bytes memory sig) public {
  // ...check that signature is valid...
 
  // check that the address's balance is zero
  require(to.balance == 0);

  // pay the block builder
  paygas(gasPrice);

  // make the transfer
  (bool success, ) = to.call{value: value}(&quot;&quot;);
  require(success);
}"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferIfNoBalance</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> value, <span class="hljs-keyword">uint</span> gasPrice, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> sig</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
  <span class="hljs-comment">// ...check that signature is valid...</span>
 
  <span class="hljs-comment">// check that the address's balance is zero</span>
  <span class="hljs-built_in">require</span>(to.<span class="hljs-built_in">balance</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);

  <span class="hljs-comment">// pay the block builder</span>
  paygas(gasPrice);

  <span class="hljs-comment">// make the transfer</span>
  (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> to.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: value}(<span class="hljs-string">""</span>);
  <span class="hljs-built_in">require</span>(success);
}</code></pre><p>There's nothing inherently wrong with this functionality, and one can even imagine a scenario where this could be useful. The problem is this: what happens if you build 100 transactions that use this functionality targeting the same address? If the balance of the address is 0, then all of those transactions will be valid on their own. But the moment one of them is included in a block, the other 99 become invalid and have to be dropped. The same idea applies to 200 or 1000 transactions. This means we can spam the mempool with as many of those transactions as we want but the cost of doing so (the cost of the only transaction that will be included) is always the same.</p><p>This problem, known as "the multi-invalidation problem", is a big headache for any account abstraction proposal, as we'll see many times in the rest of the article.</p><h2 id="h-eip-2938-paygas-revisited" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-2938: PAYGAS revisited</h2><p>We can think of EIP-859 as essentially being EIP-86 with a new opcode and a basic mempool guideline: don't use too much gas during the validation phase. EIP-2938, the last proposal we'll see in this first part, is in turn essentially the same as EIP-859 but with more mempool guidelines.<sup id="fnref:5"><a class="footnote-ref" data-id="e8dc0120-16e4-4b6a-abd9-315996878616" href="#fn:5" data-reference-number="5">5</a></sup> The reason for this is the multi-invalidation problem.</p><p>To understand why new mempool guidelines are needed, let's look at transaction invalidation from scratch. Suppose a node has a pending transaction from an EOA in its mempool, and a new block arrives. What can happen in this block that invalidates that pending transaction? There are only two possibilities: the nonce of the sender changes, or its balance drops below the upfront cost of the transaction. In both cases, the culprit has to be a transaction from the same sender.</p><p>This is a good property to have: when a transaction gets included in a block it can only invalidate up to one pending transaction in the mempool. As we saw with the multi-invalidation problem, EIP-859 doesn't have that property. And at the root of this issue is the fact that the validity of an EIP-859 transaction can depend on anything that is observable from the EVM: the balance or code of any account, storage slots, the block timestamp, etc.</p><p>EIP-2938 attempts to fix this by adding more mempool guidelines, specifying what can happen before <code>PAYGAS</code> is executed. Here's the relevant section of the EIP:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/816adf33c04f35f19d2c96cd531918f1ad1f4e550e78150e77c511481b291f99.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAALCAIAAACRcxhWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADgklEQVR4nGP4//+/v39AaGioj6+Pm5ubo6Ojv3+Ar6+vuYVFaGiovYODlZW1r69vfHyCoZFRHgjk5+XlJaempqWl5eXl19TWpqVl5OXlpaSmFZeUxMcn5OXlJ6ckJyYk1tXVf/v2jeH///91dXVpaRltYFAHAvUtrW01tXUN9fVgkfq2tvbikpL09Mzbt29/+/bt+bPnEPQWCaBxIeD///8gC/7//3/w4MGwsDBHR0c7e/ukxBQHE8eEmEQ3NzdnVxcvH29fX19/z0BdRd3m2paOro7ikuLikpJXr1//JwJALbh06VJfX198fIKBoaG5hYWpiamTk5OxsYkwt4goi7CrvZuStLK0kNTiuYuL0wpba1q7m7qayhvWLlvbXNU8a+JsEKOicdP6LTgt+Pnj5////1etWiPMIKypoGVja2tgaKCrr+dg4yDEKaiirKYmpc7CwKahpm1pbNnX3j+ha0JzZVN3U3dHQ8e0STMmdE2oKKxYuXjlo/uPTh05cfvGLXQLvn379v///927d1saW1kaW/n5BhpoGbq7eTrZOlla2BQWFTbXthRllEyZMqWuob67p3vK9Knz5iysK63bvHXrzNmzmlua+/snTOycNHfmouSwlJ7mXuw+2L17t6SAFA8Dt66mvhS/lJGOsSirKD+DQGFRYXtrZ3FaYVJw/Npla0vTS2K9I/taenuae0NdgipzyidOmFhXV9fR0FGQmN9Q23j23Fl0C96+fXv79u3NmzZrKWoriisZahpHRkeZmpnZ2NqYmplZmFhoqGgyMLCI8oibGpiJi0kqK6gyMbAHeIXMnT2vt3vC/PkL1q9fP3XK9NbmjlUrV61fv/7UqVNPnjxBWHD58hUfXx9zC4v4+IT58xd0dnROnDypo6ujrrGht7s/Jz4rNyk/PSajMq+yuaq5u6enuqa6uLBo2rRpy5cvnzZtWndPz/z58/v6+mpqa5cvX15XV9fW1n7q1CmUVGRjaaWipOrq4l5TWZOXl5+blB/jG1NSVFpXV9/c0jJx8qSeCf2d3d11jQ11dXV5efnTpk2rrqlua2ufOGEiJKP09fVBclJ3Tw/E+ShBlJWQlRCWnBqfGeARFOobkRaX5e8U6Onos3jR0idPnly7eu3a1Wu3weDGrVuXLl26dvXa5ctXbt++fe/u3du3b1+6dAkie/v27cuXr3z+8hliAQC9A1sXDIHNhwAAAABJRU5ErkJggg==" nextheight="425" nextwidth="1203" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">EIP-2938 mempool guidelines</figcaption></figure><p>As you can see, these guidelines are more arcane than EIP-859's simple "don't use too much gas before calling <code>PAYGAS</code>", because they have to make sure that the validity of a transaction can only be affected by a different transaction from the same wallet. Here I have to stress that these are not protocol rules. A block can have a transaction that uses any of these opcodes before <code>PAYGAS</code> is executed and it will be valid. It's just that most nodes would reject that transaction in the first place.</p><p>When EIP-2938 was introduced in 2020, it was considered a solid proposal, but it didn't end up being adopted. The reason was less about technical concerns than about engineering bandwidth: core devs were completely focused on the Merge. In that context, it was hard to prioritize an EIP that added a new transaction type, a new opcode with deep changes to the EVM, and a set of complex mempool rules. And so the goal of native account abstraction was put on hold, leaving the ecosystem to explore its own solutions.</p><div data-type="x402Embed"></div><div data-type="x402Embed"></div><div data-type="x402Embed"></div><h1 id="h-part-2-outside-the-protocol" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Part 2: Outside the protocol</h1><blockquote><p>The greatest victory is that which requires no battle.</p><p><strong>— Sun Tzu, as quoted by Yoav Weiss, January 2019</strong></p></blockquote><p>In Part 1 we explored a few proposals aiming for the moon: to get all the benefits of account abstraction from a single EIP. But not every AA-related issue is equally important. As Ethereum gained popularity, a frequent source of friction was the need for ETH to do almost anything. Try some app? You need ETH. Received some tokens and want to swap them? You need ETH.</p><p>From the beginning, this problem was recognized and people experimented with workarounds that didn't require waiting for protocol upgrades. The experience from these efforts, combined with the shelving of native AA efforts, eventually converged into ERC-4337, a project with the ambitious goal of getting as close to account abstraction as possible without changing the protocol.</p><h2 id="h-meta-transactions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Meta-transactions</h2><p>To send a transaction in Ethereum you use your private key to sign a transaction object, proving you own the account and want a specific action performed from it. When a block builder receives this object, it validates its signature and executes the corresponding action from the signer's account.</p><p>This idea—signing data to prove to someone that you want something done on your behalf—can be used at the contract level too. An ERC20 contract can have a special function that receives signed data to perform a transfer. If that function is called with the proper arguments, the contract can do something analogous to what block builders do: recover the address from the signature, verify it has enough balance, and move the specified amount of tokens to the target address. And crucially, anyone can call that function, even if they weren't the ones who signed the data.</p><p>This is in essence the idea of <strong>meta-transactions</strong>: separating the actor that signs from the one that wraps that signed data in a transaction, the so-called <strong>relayer</strong>. The question is, why would anyone act as a relayer? Whoever does it has to spend ETH to send the transaction, and therefore must have an incentive to do it. In the token example, the recipient might be relaying the meta-transaction because they benefit from the underlying action (the token transfer). An application might also decide to sponsor the first interactions from verified users, letting them use the app without having to pay gas; in this case, the incentive is user acquisition.</p><p>Meta-transactions work and have been used in production to let users interact with apps without needing ETH.<sup id="fnref:6"><a class="footnote-ref" data-id="525b4fbb-5200-4b07-b42a-f92143c9c33b" href="#fn:6" data-reference-number="6">6</a></sup> But they have several issues:</p><ul><li><p>Meta-transactions depend on a relayer wrapping your signed data and sending it in a transaction. But how do you find and use a relayer? Depending on the design, there is a risk of ending up with a centralized system where only a couple of relayers are realistically available.<sup id="fnref:7"><a class="footnote-ref" data-id="3c00028d-8418-460f-b977-9d6c111d6fe8" href="#fn:7" data-reference-number="7">7</a></sup></p></li><li><p>A meta-transaction has a considerable gas overhead compared with a normal transaction.</p></li><li><p>Meta-transactions are app-centric, meaning that every token or app needs to individually implement support for them.</p></li></ul><p>The last item is the worst one, because it means this approach simply won't scale. While some projects might do it if it makes sense for their goals, it's unreasonable to expect every single application out there to have support for meta-transactions.</p><h2 id="h-erc-4337" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">ERC-4337</h2><p>While the attempts to have native support for account abstraction were put on hold, the dream didn't really go away. Instead, most of the energy went to try to accomplish as much as possible without needing any protocol upgrades. These efforts resulted in ERC-4337<sup id="fnref:8"><a class="footnote-ref" data-id="56025468-5cdc-4b19-b3ac-25b61dbe30a0" href="#fn:8" data-reference-number="8">8</a></sup>, perhaps the most well-known of all AA proposals.</p><p>ERC-4337 is quite complex, so I won't go into detail on it (check the resources at the end for a great explainer). At the same time, it's impossible to properly understand the rest of the history of AA without at least some idea of how ERC-4337 works and some of the concepts it introduced, so let's do a quick overview.</p><p>The way I like to think about ERC-4337 is the following. We saw in the previous section that meta-transactions are cool, but they need every app to implement support for them. But what if smart wallets support meta-transactions? If you can sign data to execute arbitrary actions with your wallet, and if you can find a relayer that will wrap it in a transaction for you, then you get many of the benefits of account abstraction without having to change anything in the protocol.</p><p>This is, of course, easier said than done. There are several problems raised by this approach, and most of the complexity around ERC-4337 stems from those issues. For now, let's only mention two.</p><p>The first problem is that relayers need to know that the result of relaying an action will benefit them somehow. They can do this by auditing the code of every wallet they support, but this results in an "M x N" problem: each new relayer needs to support multiple wallets, and every new wallet generates work for every existing relayer. For this and other reasons, ERC-4337 uses a well-audited singleton contract called <code>EntryPoint</code> that coordinates relayers with wallets. This turns things into an "M + N" problem, because each new relayer or wallet only needs to implement compatibility with that contract.</p><p>The other problem is gas overhead. So far we've talked as if each meta-transaction corresponds to a single contract action, but it's more efficient to gather multiple actions and send them all together in a single transaction. That's what relayers do in ERC-4337, and that's the reason why they are called <strong>bundlers</strong>, which is the term we'll use from now on.</p><h2 id="h-alice-bob-and-erc-4337" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Alice, Bob, and ERC-4337</h2><p>What would our Alice and Bob example look like if Bob used ERC-4337?</p><p>As with the EIP-86 example, Bob doesn't have a smart wallet or any ETH to deploy one. But just like before, this is not a problem as long as he can predict the address where his wallet will be, and by this point <code>CREATE2</code> was already part of the protocol. We won't dive into how deployments work in ERC-4337, but the short version is that they can happen "on the fly" and are handled by the <code>EntryPoint</code> contract.</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/ff9a610b48fcbff0415dd30d120640b964ec98bbc2191f8a18865d6af610ea1d.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAPCAIAAAAK4lpAAAAACXBIWXMAAAsTAAALEwEAmpwYAAACK0lEQVR4nK2UzWsTQRjGB/ZPqNe9iLAHTwk9iNCrpIIUZGMPXqJgaGFDgwfZBmn8GhRECmvSSwtju8aDDpjrHLp4WJhNc0kWBsIK6WEq9joHe0ixI+60a7LGktr+2NO7L+/zfjy7gBBCKfV9n/4vQdDqhiFjbOxbwBiT58bd3DRNEyGUinPOAedcCOE4DqX0rHWFECymUChommbbdiphb+/b8QQAAAjhWQUYY5xz1alayGkCCCHG2OBwMGF1HqPyCSGMMUKIECItoFZkGAbn3PviuQ1X9ZVw8ONASqkKCSFUkFLa/NxcflrRr15e39qIepGaYHA4SDJHJkAIRV+jazeuz929TQhBCGGMKaXKY0lHhBCMsUqo12qvV9/cys+9rdcQQqZpYoyT2wBw3PofFx0d/Qzarf5uXwhBKYUQ6rre6XYwxm7DLT0sdbodJ0bXdcuyEELJQiCEAID5O/Mrz58UHhTW6mtpgeRcUkpv28Mxql/OebG8eCVjfGx+2v++n8Rt237/oXFv8T5y31FKlQ9XXlSLSwvetjc1dWlEwDAMXdfLS+XlaqVYWgjardMvLIRo7+y8XH2VmZl+VLUppepDG3NkJeA4jqZpUS+qPHucmZle39oYzhucnG6Y4aHJCf8UgBBms9kJzDkCY6y/25dSRr0oZYe0TWdnb+bz+SD4vRkRIyeDc84Yy+VyAADLssYIXMi/iBDiOA4h5G954Pt+Nwz9mG4YXvjzC/IMrMKQHF1qAAAAAElFTkSuQmCC" nextheight="988" nextwidth="2177" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Bob sends 0.1 ETH to Charlie using an ERC-4337 wallet</figcaption></figure><p>Bob figures out where his wallet will be deployed and shares this address with Alice. Once she transfers the ETH, Bob signs a user operation: a structured piece of data defined by ERC-4337 specifying the action that the wallet should perform. In our case, this user operation will indicate that 0.1 ETH should be transferred to Charlie, plus some fields specifying that the wallet should be deployed. This user operation is received by a bundler who sends it along with other user operations to the <code>EntryPoint</code> contract. When Bob's user operation is executed, the wallet is deployed and called, Charlie receives his 0.1 ETH, and the wallet pays the bundler.</p><h2 id="h-paymasters" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Paymasters</h2><p>There's one final concept introduced by ERC-4337 which deserves mention. In the previous example, we said that Bob's wallet pays the bundler for including his user operation in a transaction. If things always worked that way, then this aspect wouldn't be fundamentally different from how EOAs work: the entity initiating some action (the wallet in this case) is always the one that pays for its cost. ERC-4337 uncouples these two responsibilities with the concept of paymasters. A paymaster is an optional part of a user operation, and it represents an entity that is willing to pay for it.</p><p>To understand how a paymaster could be useful, imagine that Alice sends USDC to Bob instead of ETH. The wallet can't use it to pay the bundler for sending the user operation; only ETH can be used for that. But Bob can modify his user operation to include a paymaster that accepts tokens.</p><p>ERC-4337 also defines a "paymaster post op" callback, which is executed at the end to let paymasters perform final checks or do some cleanup work. The details don't matter, but the concept will be important later when we try to generalize the whole mechanism.</p><h2 id="h-erc-7562-standardizing-mempool-rules" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">ERC-7562: Standardizing mempool rules</h2><p>Everything we've explained about ERC-4337 shows that you can get many of the benefits of native account abstraction without having to change the protocol. The mechanism is in fact quite similar to the EIPs we explored in the first part. In those proposals, the smart wallet executes some validation logic and then pays the block builder; in ERC-4337, the wallet validates a user operation and pays a bundler. But this is the exact same setup that led to the multi-invalidation problem. Imagine the same example we saw before but in an ERC-4337 context: someone creates hundreds of user operations that transfer ETH to some account but that are only valid if the target's balance is zero. If a bundler includes multiple of those user operations, it will only be paid for the first of them to be executed.</p><p>ERC-4337 largely follows EIP-2938's approach here: add guidelines about which opcodes can be used during validation. These guidelines evolved over time, reflecting a more detailed understanding of the multi-invalidation problem and the trade-offs between preventing it and letting wallets do as much as they want during validation. In fact, the rules became so complex that they were moved to a different spec, ERC-7562.</p><p>But there is a big difference between mempool guidelines in a native AA proposal like EIP-2938 and an out-of-protocol one like ERC-4337. A node can know in detail what's happening in the EVM during the execution of a transaction, but a bundler is just a regular user for whom the execution is by default a black box. This is not an unsolvable problem: there are alternative JSON-RPC methods that can be used to simulate a transaction and get a detailed trace of its execution, which can then be used to check that guidelines are respected. But this adds to the overall complexity of the proposal.</p><h2 id="h-erc-4337-in-the-wild" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">ERC-4337 in the wild</h2><p>ERC-4337 was deployed to production in 2023, and it has been maintained and upgraded since then. To be honest, it's unclear to me how much adoption it has had. The <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.bundlebear.com/">BundleBear</a> website has some interesting usage stats, but it's hard to evaluate them because they are in absolute terms instead of, say, showing which percentage of the overall onchain activity goes through bundlers.</p><p>Still, what matters is that ERC-4337 worked and contributed to a much deeper understanding of the account abstraction problem. And its technical achievements are impressive: smart wallets that can be used with relative gas efficiency without needing an EOA, that can be deployed on the fly, and whose operations can be funded by third parties; all without needing changes to the protocol.</p><p>But ERC-4337 still has downsides:</p><ul><li><p>Since EOAs are still needed for bundling, it can't be a long-term solution (nor does it claim to be one).</p></li><li><p>While bundling helps with gas efficiency, there's some unavoidable overhead that can only be eliminated through a native AA mechanism.</p></li><li><p>Because user operations are not first-class citizens like transactions, they won't benefit from future protocol features. For example, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-7805">inclusion lists</a> wouldn't prevent user operations from being censored.</p></li></ul><p>For all these reasons, there continued to be a need for protocol changes, resulting in a second wave of native account abstraction proposals. We'll discuss them in the fourth and final part, but first we need to rewind once more and talk about a parallel attempt at dealing with the limitations of Ethereum accounts.</p><h1 id="h-part-3-empowering-eoas" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Part 3: Empowering EOAs</h1><blockquote><p>I am once again asking for your support of EIP-3074.</p><p><strong>— lightclient, September 2022</strong></p></blockquote><p>At the beginning of this article, we listed some pain points caused by the lack of account abstraction. And in the previous part, we saw how some of these pain points could be addressed<em> </em>with two out-of-protocol approaches: app-centric support for meta-transactions, or smart wallets that can be used through ERC-4337. In both cases we are avoiding a direct use of EOAs due to their limitations. But what if we tried to fix the worst of those shortcomings instead? Why not empower EOAs to make Ethereum better now, even if they are going to go away in the long term?</p><p>There have been many proposals aiming to make EOAs more powerful, but here we'll only focus on two of them: EIP-3074 and its successor, EIP-7702.</p><h2 id="h-eip-3074-the-proposal" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-3074: The proposal</h2><p>By 2020, Ethereum usage had grown significantly. New tokens and protocols were constantly emerging and the legendary DeFi summer was around the corner. This surge in interest only made it more frustrating to need ETH for everything.</p><p>At the core of this problem was the fact that one could only initiate an action from an EOA by sending a transaction from it, and this always requires ETH. The proposals we've seen so far tried to circumvent EOAs entirely and rely on smart wallets instead. But an alternative solution is to add some other mechanism to make calls from EOAs.</p><p>To understand the mechanism that EIP-3074 proposes, let's recap the relationship between transactions and calls. A transaction is made of at least one top-level call, the one that comes from the EOA sending the transaction (even a plain ETH transfer is considered a call in this context). If the recipient is a contract, then this contract in turn could make multiple calls, which in turn could execute other contracts that make more calls, and so on. But notice that there is one and only one call coming from an EOA: the one at the beginning.</p><p>The essence of EIP-3074 is in allowing a contract to "impersonate" an EOA and make a call from it. Of course, the EOA has to authorize this somehow, and the way to do it is not that different from how meta-transactions work: the user signs some structured data with their private key to let some contract make calls from their address.</p><p>To be more specific: EIP-3074 adds two new opcodes, <code>AUTH</code> and <code>AUTHCALL</code>. The first one verifies a signature over some data specific to the EIP. If that signature is correct, then for the rest of the execution the contract is allowed to use <code>AUTHCALL</code>, which works pretty much as a normal call but where the sender is the EOA that signed the data.</p><h2 id="h-alice-bob-and-eip-3074" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Alice, Bob, and EIP-3074</h2><p>EIP-3074 is about making EOAs more powerful, not about smart wallets, so we'll use a different scenario for our Alice and Bob example. Alice sends 100 USDC to an EOA that Bob generated, and he wants to send 10 of those USDC to Charlie. Bob's EOA is fresh and doesn't have any ETH. Normally this would mean that he can't do anything with it, but with EIP-3074 he can sign an authorization for a contract compatible with EIP-3074 (called an invoker in the EIP) that can transfer the tokens on his behalf.</p><p>As with meta-transactions, though, Bob still needs to find a relayer<sup id="fnref:9"><a class="footnote-ref" data-id="1bc19e31-e482-4e88-a2b7-b06f6e760639" href="#fn:9" data-reference-number="9">9</a></sup> that will call the invoker with the data he signed. And, as always, the relayer needs an incentive to perform this work. In this example, the invoker might be written so that it sends part of the USDC to the relayer in addition to the 10 USDC it sends to Charlie.</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/54f5fa9f1fc2e7a6b8ef097776313471b16802b368fa2d94325dcd1589d3e96f.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAWCAIAAAAuOwkTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAC5UlEQVR4nK1VTUwTQRSeg0dORnsD48FrLybibU82HEi4F2/FmJjsycN4om40EtagHEbRANmYqmSSBhQbDjSZW8kkgrZmuABZvLTN0C2B5N34GWMfrEsB26LfafLmvfnmvXnvG1IslXzfV0oJIfyTqFSrlWq1HWOTPVwopYhSq8YYxphlWQBgjqG1Rg/f90MjAEgpMTg0hlsAENRqQa0WnrO5+ZMopYwxlFJCCADoBgCAMZbL5TzP+zgzg3YAEEJQSqWUjDGtdT2o14O61lpKqZSSUrquyzlXDWAUWd/YMMa4rhuLxepBPTubffL8qVIqlUpxzlkDGKyUYoxRSoulouM4haXCxNTE42eOEIIxhmnF4/FUKmWMkVLiXY8IGGO4EWwHucUF9Pg0P885F0JgBYwxSqne3t5kMkkp3T84+JLPfcjO7OzuGGOwnnjrsLCVavWIgHPuOE60plrrK1djhBDP86IE3d3dPT3XKKVNDxBGRR/yD0EUe/t7uBhOpxOJhJQyuousnHP0DJ3PxAmCKPNfMDh4l5BLWLeWODuDps6Dk8RhR/0Hgn9H5TRBy6vpxvUxjw4IAACbHQcYF6cDACAej5MGmrquBUGxVMKBJITYtm2MEcfIvM9kZ7PZ2SwOFGOMN+B5Xst0/xDgdGASOLQoIWz69a07t+8/fNCfHChXymFyKBsdEITtEWoRBucWF9jUqzfe5Mj4aLAdBLVaYamAgiOl7IzgPGitM+8ya+trO7s7xVLp7fQkIcSyLBSG0O10Q7dLUCwVCSFjL8dQcFCu8cGiJ7quOzR07yIEAEApxScxxmAXdXV12bYthJj7PJfP56WUlmXFYrGmRu940LTWrutSSm3b9jxv5dtKf3KAOo+MMYlEoq+v7yIZmEh9tdbYPACw/HV5jL0gl8n1mzfKlbLjOMPp9BkE+GW2icPDA+xgVHwAGBkfXfnx/Tx//DJXQ1FrB02ZRTVxa+v3bxyF7/u/AFT3Kj6Hk5seAAAAAElFTkSuQmCC" nextheight="1200" nextwidth="1736" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Bob transfers USDC to Charlie using EIP-3074</figcaption></figure><h2 id="h-eip-3074-questions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-3074 questions</h2><p>While EIP-3074 is a conceptually simple proposal, there are details that need to be clarified that can have a substantial impact on the end result. For example:</p><ul><li><p>Can the authorization be used multiple times? One possible answer is to forbid it at the protocol level, for example by adding a nonce mechanism to authorizations. Alternatively, re-use could be allowed for maximal flexibility, in which case it is the invoker's responsibility to implement replay protection at the application layer.</p></li><li><p>Can authorizations be revoked? Suppose Bob signs and propagates an authorization but then regrets it. Can he invalidate it? How?</p></li><li><p>How can users know what the impact of signing a certain authorization will be? This is a UX problem for every scenario where users sign something, but it's especially important in EIP-3074 because a single bad signature could be used to completely drain an account.</p></li></ul><p>These and many other questions meant that EIP-3074 was proposed and delayed many times. But in 2024, the proposal was finally accepted for inclusion in the Pectra upgrade.</p><h2 id="h-pushback" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Pushback</h2><p>Maybe you remember there was some drama around EIP-3074. And as with any controversy, the account of what happened depends on who you ask. I'll briefly explain one version of the story (perhaps the most often repeated one) which I believe is broadly correct. But keep in mind that it might be missing a good deal of nuance.</p><p>After EIP-3074 was accepted as part of the Pectra upgrade, it started to receive some serious pushback, especially from the team working on ERC-4337. As we explained in Part 2, ERC-4337 went live in 2023 and there was already some talk of making it more useful by enshrining it into the protocol (of which more in the next part). A major complaint about EIP-3074 was that it wasn't aligned with that roadmap. For example:</p><ul><li><p>EIP-3074 needs relayers to send authorizations. The work on decentralized bundlers by the ERC-4337 team couldn't be reused for this, so a new relayer system would be needed.</p></li><li><p>As in any meta-transaction scheme, relayers need an incentive to do their job: the resulting on-chain action has to benefit them somehow. Here we have the same problem we've seen a couple of times: as a relayer, you can run a simulation to check that you'll be paid, but there's no guarantee that this is what will actually happen when the transaction is finally included in a block. ERC-4337 solved this with the <code>EntryPoint</code> contract but, again, this solution couldn't be leveraged by EIP-3074.</p></li><li><p>By making EOAs more powerful, the EIP was going against the long-term goal of users migrating to smart wallets.<sup id="fnref:10"><a class="footnote-ref" data-id="2742be3a-9145-4a4b-9a58-d4f47c0c26f3" href="#fn:10" data-reference-number="10">10</a></sup> And by adding opcodes that explicitly relied on ECDSA, the EIP was going against the long-term goal of deprecating non-quantum-resistant cryptography.</p></li></ul><p>But the UX problems EIP-3074 was trying to address were real, and waiting for full AA (migration to smart wallets included) seemed unrealistic to many. The bottom line is that there were two groups of people in the community honestly trying to make the protocol better, but with different philosophies about what the best next move was.</p><h2 id="h-eip-7702" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-7702</h2><p>Things seemed to be at a stalemate, with an EIP that had technically been approved for inclusion receiving pushback that couldn't be ignored. But then, right before a crucial meeting to discuss the topic, Vitalik famously drafted a new, alternative EIP in 22 minutes. This was EIP-7702, and it covered most of the use cases the EIP-3074 authors cared about while being compatible with the long-term vision of the ERC-4337 team.</p><p>To understand how EIP-7702 works, remember that EOAs can be the targets of calls from other EOAs or contracts, but the only thing these calls can do is transfer ETH. If they have some data, it is ignored.</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/4671e14dee61045ef9e5826f11c28cf42d053eddf7bbfcad38d3c2c21e756ca0.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAUCAIAAABj86gYAAAACXBIWXMAAAsTAAALEwEAmpwYAAACj0lEQVR4nLWVTUwTQRTHx3uPUA8ePNXEmPQkridb09CgQUgTv1LwALSWkJQPldQ0RENCgCwejAycIGM0xmzjwZCRA4eNqZAOJkYqWy6SBx7nhjLlQssYO3WBtSVRyz+bzezszvvN+8/bGcQYg9+ySoLaaSWbRYwxeWwCALS8/FE9cM5DoVBXV9dxAQzD8Pl8mqZxzo8FIIRACLlcLtWuPQAAXC6XrutSyqmpadM0q2FESX8N4JwjhBQAAGZmZ59NTmKMOzo7j46yW9iteDkBjLFAYyPG2B7JOSeEPBgcpJQenHI8Hq9YC/Y3+fxOBQAAnG9oSKVSjjUQQrydmxsbH0+lUhsbmwDg8Xg0TaOUSilHn47Fk/0dfdGeh/HIQMzfEojd72nrvhNP9kspv66v7wOOlhDCMIympitSSoSQ2+1W/enM4q1o+Pbd9o6+aHO49dK1y/6WwJkLZ+nCfAVAoVisFl2UcgIABQiHw+rVpy+f6cJ8OrO4uraazixaazlrLffqzesPbKls0Uo2q+xWUYQQqu2wFQAIIX6/XwFCoZDqf2G8jAzETp07fb3zRvDmVXTyRHO4NXqvOzkytA/Y2yt6vV6EkNfrVf4CgApdKBY555S+m5h4ogqXkOeJRKK3t9eyLMXY+r6VHBnCs9N4ZioyEHs0+thOvQwQQiiAksfjUVak36cJIT6fLxgMKp4Qoq7eXVfvRgi1tbXbNepI91CZWlYOAHRdBwC1s+q6TinFGBNCtrd/YIw17aLt28G7Y5FsHcrAsnKcc9M07WGmaTLGqv2r+fzOn4GqaT8DB0D5a09W/OvWVAZ829wcHh6mlBqGQSlNJBL2Asr/0y+AOnCqmVgDwFImU8Mz0iHG2E+boGrr4dAvpAAAAABJRU5ErkJggg==" nextheight="808" nextwidth="1278" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">tx/call to an EOA</figcaption></figure><p>EIP-7702 lets EOAs optionally change that behavior by delegating to a contract the handling of incoming calls. To do that, they have to sign an authorization specifying the address to which they want to delegate. These authorizations can be enabled on-chain by including them as a field of a new type of transaction.</p><h2 id="h-alice-bob-and-eip-7702" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Alice, Bob, and EIP-7702</h2><p>In the USDC version of our Alice and Bob example, Bob has received 100 USDC in his EOA but he can't do anything with them because he has no ETH. With EIP-7702 he has a way out: he can sign an authorization to delegate how the EOA handles incoming calls. The target of the delegation can be any contract, but the most logical choice is a smart wallet implementation.</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/dd7c8fe13d16f0b9510aa91dc1bd3d1d3c8c33f74b50800e7cbb0b23377e57eb.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAICAIAAAAX52r4AAAACXBIWXMAAAsTAAALEwEAmpwYAAABpklEQVR4nI2RMUhbURSG7+akoIOPV+lgNdSMSfsGIWTQQt8WvBS3gIMxpVCMr0NLJeoQ4UHsIm9wy5BCwoM6mcRBItwlFeSkQyVw1jyOpYIKD3SwcEtyQ3iNQ/pxl3M55/7n/j9rNn+0PW/oIaK252GAy8tfJbecy+/uHzi5/G7je0P1BEd+XlwwRJT/ASL6vt8vH/48SCk/7nxiY2zm5fORqdHG+Vmw3++CiB0BIhJCUBfoMvA6EaXTaQCo1Y4zGxufNze3travfl/VT+vR+ReTT7XU+7X+omqPZDJpmqaUkgGAEEJKaRgGY0zJFAoF13Vt2y6Vy06XeDyOiJxzxpimabr+xDRNznloNsQYe7WwmM1mAaD4tVipHAkhdF03DKMj0Gq1EBEAqtWq67pCCABQhjiOAwBEhIi2bfv/or4ViUYSicRcOHz47bB6Upt4pr213kkpY7EY5/zm9raXgRogIjU8YFHb89YzGSIauO+LqXLP+TI+Pbm49FpKuZpKWdaHXgZDEyYiy7IeCw9wfXP9ZmU5mHZHQBkyFGXUUO7v7oOdAPAX6P1ZxZsBcsMAAAAASUVORK5CYII=" nextheight="452" nextwidth="1755" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Bob sends USDC to Charlie using EIP-7702. Here the relayer could be the ERC-4337 entry point, if the wallet implementation is compatible with it.</figcaption></figure><p>Once this happens, Bob's EOA is practically indistinguishable from a smart wallet. He still needs someone to call it on his behalf, but that's exactly the kind of problem that ERC-4337 is meant to help with, and what any native AA scheme will try to solve. This is one of the main reasons why EIP-7702 is considered more aligned with the AA roadmap than EIP-3074 was.</p><p>EIP-7702 was good enough to satisfy both sides of the debate. It was included in the Pectra upgrade as a replacement for EIP-3074 and is now live on mainnet and several L2 chains.</p><h1 id="h-part-4-the-dream-of-native-account-abstraction" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Part 4: The dream of native account abstraction</h1><blockquote><p>Even enshrined in-protocol account abstraction is still a massive "de-enshrinement" compared to the status quo.</p><p><strong>— Vitalik Buterin, September 2023</strong></p></blockquote><p>As we discussed in Part 2, ERC-4337 has limitations that can only be solved by enshrining some of its features at the protocol level. In fact, in 2023, the very same year that ERC-4337 went live, a proposal was drafted with this goal. Since then, the conversation has evolved and culminated in EIP-8141 (Frame Transactions), a proposal that attempts to take all the lessons from this story and wrap them into a single AA proposal.</p><h2 id="h-enshrining-erc-4337" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Enshrining ERC-4337</h2><p>Remember how ERC-4337 works: a user signs a "user operation" which is later included in a transaction by a bundler and processed by the <code>EntryPoint</code>. This contract handles the logic related to checking that the operation is correct, executing the call, paying the bundler, etc. What we would like is a similar mechanism that's handled directly by the protocol. That is: user operations become transactions, the work of bundlers is done by block builders, and the logic coded in the <code>EntryPoint</code> contract becomes part of the rules of the chain.</p><p>The first two attempts at this were RIP-7560<sup id="fnref:11"><a class="footnote-ref" data-id="27703dda-8a53-461c-baee-8edb7aec7bee" href="#fn:11" data-reference-number="11">11</a></sup> (proposed in 2023) and EIP-7701 (proposed a year later and, yes, the EIP immediately before EIP-7702). There are many cosmetic differences between these two proposals, but they boil down to the same idea, so I'll just focus on EIP-7701 here.</p><p>EIP-7701 proposes adding a new transaction type with these fields:</p><pre data-type="codeBlock" language="json" text="[
  chain_id,
  nonce,
  sender, sender_validation_data,
  deployer, deployer_data,
  paymaster, paymaster_data,
  sender_execution_data,
  max_priority_fee_per_gas, max_fee_per_gas,
  sender_validation_gas, paymaster_validation_gas,
  sender_execution_gas, paymaster_post_op_gas,
  access_list,
  authorization_list
]"><code><span class="hljs-punctuation">[</span>
  chain_id<span class="hljs-punctuation">,</span>
  nonce<span class="hljs-punctuation">,</span>
  sender<span class="hljs-punctuation">,</span> sender_validation_data<span class="hljs-punctuation">,</span>
  deployer<span class="hljs-punctuation">,</span> deployer_data<span class="hljs-punctuation">,</span>
  paymaster<span class="hljs-punctuation">,</span> paymaster_data<span class="hljs-punctuation">,</span>
  sender_execution_data<span class="hljs-punctuation">,</span>
  max_priority_fee_per_gas<span class="hljs-punctuation">,</span> max_fee_per_gas<span class="hljs-punctuation">,</span>
  sender_validation_gas<span class="hljs-punctuation">,</span> paymaster_validation_gas<span class="hljs-punctuation">,</span>
  sender_execution_gas<span class="hljs-punctuation">,</span> paymaster_post_op_gas<span class="hljs-punctuation">,</span>
  access_list<span class="hljs-punctuation">,</span>
  authorization_list
<span class="hljs-punctuation">]</span></code></pre><p>As you can see, most of these concepts come directly from ERC-4337. The idea is that the protocol itself does the work of the <code>EntryPoint</code>: it calls <code>sender</code> (presumably a smart wallet) with <code>sender_validation_data</code> to check if they indeed want to make that call, it validates the paymaster if present using <code>paymaster_data</code>, and so on.</p><p>How does a wallet or paymaster communicate whether they accept the transaction? In ERC-4337, this is done by implementing a certain interface that is called by the <code>EntryPoint</code> contract, but in EIP-7701 they have to communicate with the protocol itself. For this reason, the EIP adds two new opcodes, <code>CURRENT_ROLE</code> and <code>ACCEPT_ROLE</code>, which let contracts say "yes, I want to send this call" or "no, I don't want to pay for this transaction."</p><p>To be able to decide how to respond, wallets and paymasters need visibility into the whole transaction. In ERC-4337 the whole user operation object is always passed around, but in EIP-7701 the data in each call is only what is relevant to that phase of execution. To make up for this, the proposal also adds a new group of <code>TXPARAM*</code> opcodes that let a contract inspect other fields of the transaction. For example, a paymaster can use these opcodes to check who the sender is and the data with which it's called.</p><p>EIP-7701 fixes the main issues with ERC-4337: it doesn't need EOAs to work, it's more gas-efficient, and it benefits from any improvements to transactions. What it doesn't do, nor does it try to, is to fix the problems inherent to account abstraction, like the multi-invalidation problem. Mempool guidelines are still needed and, while the draft doesn't really mention them (perhaps because the EIP didn't make that much progress), they are clearly necessary.</p><p>In summary, EIP-7701 takes ERC-4337 and transforms each of its parts into a native equivalent. I don't know about you, but this feels wrong to me. ERC-4337 had to deal with the constraint of not being able to modify the protocol, and this limitation certainly influenced its design. Using its ideas verbatim seems like taking the shortest path to enshrinement. A better way to approach the problem is to ask: what is the minimal set of native features that enable the capabilities we want?</p><h2 id="h-eip-8141-frame-transactions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-8141: Frame Transactions</h2><p>The core problem of account abstraction is how to allow users to interact with smart wallets without needing an EOA. What's involved in those interactions? Here are the key steps, according to what we've learned so far:</p><ol><li><p>A smart wallet is created on the fly if needed.</p></li><li><p>A smart wallet is asked if it wants to perform a certain action.</p></li><li><p>A paymaster is asked if it wants to pay for the interaction.</p></li><li><p>The action from the smart wallet is performed.</p></li><li><p>The paymaster is allowed to do some work at the end.</p></li></ol><p>These steps can be grouped into two categories. Items 2 and 3 are about querying a contract to check if it wants to play a certain role in the interaction (making a call in the case of the wallet, paying for it in the case of the paymaster). The rest of the items are about making a call: to a factory to deploy the wallet, to a paymaster for its post-op, and of course to the wallet itself to perform the main action.</p><p>But notice that this second category (making calls) can in turn be divided: when we perform the main action, we are interested in the wallet being the sender; when we deploy the contract or execute the paymaster post-op, on the other hand, we don't care who makes the call. Putting it all together, we can define three categories of steps in an AA interaction:</p><ul><li><p>Asking a contract if it's willing to play a certain role</p></li><li><p>Making a call from a specific address</p></li><li><p>Making a call without caring about the sender</p></li></ul><p>EIP-8141 proposes a new transaction type composed of frames, each belonging to one of those three categories. The EIP calls these categories "modes" and gives them the names <code>VERIFY</code>, <code>SENDER</code>, and <code>DEFAULT</code>.</p><p>And that's it, that's the core idea of frame transactions. There's a fair amount of extra detail, like new opcodes similar to the ones we saw for EIP-7701, but all that complexity is just a means to an end. And, of course, our old friend, the multi-invalidation problem, is alive and well. The answer here hasn't changed much: you need mempool guidelines, and EIP-8141 simply defers to the ERC-7562 rules originally defined by ERC-4337.</p><h2 id="h-alice-bob-and-eip-8141" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Alice, Bob, and EIP-8141</h2><p>Let's revisit our Alice and Bob example one final time. As always, Bob hasn't interacted with Ethereum before, so the first thing he'll do is predict where his smart wallet will be deployed. Alice transfers ETH to that address and then Bob builds an EIP-8141 transaction with three frames:</p><ul><li><p>One <code>DEFAULT</code> frame to deploy the wallet.</p></li><li><p>One <code>VERIFY</code> frame asking the newly created wallet to confirm whether it wants to perform the desired action (send ETH to Charlie) and pay for the whole transaction.</p></li><li><p>One <code>SENDER</code> frame corresponding to the actual call.</p></li></ul><p>The relationship between the <code>VERIFY</code> and <code>SENDER</code> frames might need some clarification. The <code>VERIFY</code> frame receives some data, and it can also use the new opcodes to inspect the rest of the transaction. A simple wallet that just checks a signature could get it from the <code>VERIFY</code> data and assert that it indeed corresponds to the <code>SENDER</code> data, but more complex schemes are of course possible.</p><p>When a node receives this transaction, it will check that it's valid. In the context of EIP-8141, that means executing the <code>VERIFY</code> frames and checking both that they succeed and that they don't use any opcodes forbidden by ERC-7562.</p><p>I'm glossing over many details here; check the EIP for the full picture. Hopefully knowing why the proposal looks the way it does will help you navigate the spec.</p><h2 id="h-issues-with-frame-transactions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Issues with frame transactions</h2><p>If you've read this far and I've done a good enough job, then it should be easy to sympathize with the idea that frame transactions are the "endgame" for AA, given how simple the core concepts are. And yet, EIP-8141 was rejected as the headliner for the Hegota upgrade. It now lives in a weird limbo: it's a headliner-sized EIP that is "considered for inclusion" as a non-headliner. Whatever that means.</p><p>There are many reasons for this rejection. Here are some:</p><ul><li><p><strong>"It adds a lot of complexity to the mempool."</strong> People against frame transactions would prefer something less abstract but more mempool-friendly, like a hardcoded set of transaction sub-types. EIP-8141 proponents favor maximum power combined with "soft" mempool guidelines that can evolve over time.</p></li><li><p><strong>"There's no need for so much generalization."</strong> Opponents say that there are 80/20 solutions that cover the most important use cases related to AA. Proponents argue that the protocol shouldn't be in charge of making these decisions and should let higher-level layers figure it out.</p></li><li><p><strong>"It's not really quantum safe."</strong> Opponents say that frame transactions don't really solve the quantum issue because more things are needed, like new precompiles. EIP-8141 proponents agree but argue it's a necessary first step in the right direction.</p></li></ul><p>In my opinion, some of these criticisms are more valid than others. The mempool complexity is real but, as we've seen time and again in this article, this problem seems inherent to having full account abstraction. Dealing with it by using hand-picked 80/20 solutions seems short-sighted to me and completely against Ethereum's philosophy. On the other hand, the fact that this is not a complete solution to the quantum threat is absolutely true. The problem here is that the EIP-8141 authors shot themselves in the foot by originally putting the post-quantum (PQ) motivation up front in the spec without any nuance (though this has been fixed since then).</p><p>That last point illustrates something I believe is not taken into account often enough. The AA or PQ problems don't need to be solved with the stroke of a single EIP, as much as that would help with <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://x.com/VitalikButerin/status/2010621884811845708">the walkaway test</a>. But, someone asks, what if Ethereum suddenly stops having upgrades after Hegota? Well... if that happens it's because we have much bigger issues than the lack of full account abstraction or quantum resistance. In this sense, I agree with the 80/20 proponents but from a different angle: we should add enough basic, neutral functionality (plus mempool guidelines) to help with the biggest current UX issues and pave the way for PQ, but nothing much beyond that. At the time of writing, though, the EIP is only growing bigger, adding new capabilities that feel like feature creep to me.</p><p>Still, at the end of the day these are details that can be worked out. There's no target date for the Hegota upgrade, but it will probably happen in early 2027, giving us plenty of time to iron out the spec. The debate today shouldn't be about those details; the question is whether the core of EIP-8141 is the right path to AA and whether it's urgent enough to be included as soon as possible. I think the answer to both questions is yes.</p><h1 id="h-epilogue-whats-next" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Epilogue: What's next?</h1><p>At the time of writing, EIP-8141 is "considered for inclusion" for Hegota, meaning that it might be part of the upgrade but it's not guaranteed. So what's in store for account abstraction? Short term, I think there are three possible scenarios:</p><ul><li><p>Frame transactions are included in Hegota in some form.</p></li><li><p>Frame transactions are not included, but some alternative AA mechanism is.</p></li><li><p>No AA features are added to Hegota.</p></li></ul><p>I'm fairly sure the second option won't happen. While EIP-8141 didn't have the consensus needed to become a headliner, it has more than enough support to block any alternative. Unless, that is, someone pulls an EIP-7702 at the last minute. But I think this is unlikely: the underlying problem here is way more complex than the one EIP-3074 was trying to solve.</p><p>This leaves us with two possibilities: frame transactions are scheduled for Hegota, or account abstraction is postponed to the next upgrade. Personally, I don't think that postponing AA six months would be catastrophic; after all, it has been postponed for ten years. The problem is less about the delay than about what it signals: if EIP-8141 can't get consensus due to core philosophical issues rather than workable details, will six months change anything? Does the problem's wickedness, combined with the different priorities and philosophies of the community, mean that the dream of native account abstraction cannot be realized? Let's hope that's not the case.</p><div data-type="subscribeButton" class="center-contents"><a class="email-subscribe-button" href="https://paragraph.com/@cethology/subscribe">Subscribe</a></div><h1 id="h-resources" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Resources</h1><ul><li><p>General</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.youtube.com/watch?v=iLf8qpOmxQc">A history of account abstraction</a>. A talk by Vitalik, from the beginning up until ERC-4337.</p></li><li><p><a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://hackmd.io/@matt/aa-link-tree">Account Abstraction Link Tree</a>. A “rough outline of the major contributions to the topic of Account Abstraction over the years”, maintained by <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://x.com/lightclients"><u>lightclient</u></a>. The best meta-resource if you want to go much deeper into the topic.</p></li></ul></li><li><p>Part 1: Early attempts</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-86">EIP-86: Abstraction of transaction origin and signature</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/issues/859">EIP-859: Account abstraction for main chain</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-2938">EIP-2938: Account Abstraction</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethresear.ch/t/tradeoffs-in-account-abstraction-proposals/263">Tradeoffs in Account Abstraction Proposals</a>. 2017 Vitalik post in Ethereum Magicians analyzing approaches with different trade-offs between complexity and capabilities.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethresear.ch/t/a-recap-of-where-we-are-at-on-account-abstraction/1721">A recap of where we are at on account abstraction</a>. 2018 Vitalik post in Ethereum Magicians, focused mostly on how to deal with invalidation issues.</p></li></ul></li><li><p>Part 2: Outside the protocol</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-4337">ERC-4337: Account Abstraction Using Alt Mempool</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.erc4337.io/">ERC-4337 website</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://web.archive.org/web/20260221021404/https://www.alchemy.com/blog/account-abstraction">You Could Have Invented Account Abstraction</a>. 4-part series on ERC-4337. If you are going to read only one thing from this list, choose this one. I’m linking a Wayback Machine version because the code snippets don’t show up in the original blog.</p></li></ul></li><li><p>Part 3: Empowering EOAs</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-3074">EIP-3074: AUTH and AUTHCALL opcodes</a></p></li><li><p><a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://eips.ethereum.org/EIPS/eip-7702">EIP-7702: Set Code for EOAs</a></p></li><li><p>EIP-3074 proposals history</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/pm/issues/260">London proposal</a> (Mar 2021)</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum-magicians.org/t/shanghai-candidate-eip-3074/10781">Shanghai proposal</a> (Sep 2022)</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum-magicians.org/t/pectra-network-upgrade-meta-thread/16809/21?u=fvictorio">Pectra proposal</a> (Dec 2023)</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/pull/8403">EIP-3074 included in Pectra</a> (Apr 2024)</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/pull/8591">EIP-3074 replaced by EIP-7702 in Pectra</a> (May 2024)</p></li></ul></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://notes.ethereum.org/@yoav/erc-4337-vs-eip-3074-false-dichotomy">ERC-4337 vs EIP-3074: False dichotomy</a>. There are dozens of texts arguing in favor or against EIP-3074. I'm only linking this one because it gives some good insights into the problem of AA</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://derekchiang.com/4337-3074-disagreements/">Why 4337 and 3074 authors are disagreeing, and who got it right</a>. Great summary by <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://x.com/decentrek">Derek Chiang</a> about the 3074/4337 debates. Derek also wrote a follow-up, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://derekchiang.com/governance-reflections/">Reflections on Ethereum Governance Following the 3074 Saga</a>, which is not exactly about account abstraction but it’s worth reading anyway.</p></li></ul></li><li><p>Part 4: The dream of native account abstraction</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7560.md">RIP-7560: Native Account Abstraction</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-7701">EIP-7701: Native Account Abstraction</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-8141">EIP-8141: Frame Transactions</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.fileverse.io/0xd961b83d3421bddec9d8966efabf13800617cfea/10#key=Z-XeQk6mE7uZX9Q2ZbqWkiniYi9IZ1oSbd_2vPFyt26S7Kf8gO_UHpLJ936--lp8">Full account abstraction</a>. An article by Vitalik analyzing the problem from first principles, written when EIP-7701 was the latest proposal.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://x.com/decentrek/status/2036697881512701997">The case for Frame Transactions</a>. An article in defense of EIP-8141 by Derek Chiang.</p></li></ul></li></ul><br><ol class="footnotes"><li id="fn:1" data-id="1d1d7e11-0392-42ee-8e54-53453735531f"><p>The mempool of a node is the set of transactions that could be mined at some point in the future. While each node technically has its own mempool, people often use “the mempool” to refer to the group of pending transactions in the network irrespective of a particular node.</p></li><li id="fn:2" data-id="352e2de2-5e0f-4123-841f-f0d2b9692a1a"><p>This is almost never necessary nowadays, thanks to EIP-1559.</p></li><li id="fn:3" data-id="c9eea395-a95e-43cd-abdf-65da5c7d424c"><p>Suppose a node accepts replacement transactions without a minimum gas price difference. An attacker can send a transaction with gas price X, then a bumped one with gas price 1.001*X, then another one with 1.002*X, and so on. This makes the node validate thousands of transactions when only one of them is finally mined. Enforcing a minimum bump of 10% means that the gas price grows exponentially, making this kind of denial of service extremely expensive. For example, if an attacker sent 100 bumped transactions, the last of those would have to use a gas price 13,780 times higher than the first one.</p><br></li><li id="fn:4" data-id="30cf6f46-eae9-4804-9143-a1541e2d23ae"><p>There is even more nuance about mempool guidelines. There are some rules at the network-level (not protocol-level) that are necessary to let nodes know which transactions they can propagate without being penalized in the p2p network. But explaining that in more detail would make this article even longer than it already is, so we’ll simplify and think about mempool rules as being up to each node.</p></li><li id="fn:5" data-id="e8dc0120-16e4-4b6a-abd9-315996878616"><p>There are several other differences between EIP-859 and EIP-2938, but they aren’t very relevant. For example, EIP-2938 uses a new EIP-2718 transaction type, while EIP-859 and EIP-86, as you can guess by their numbers, don’t.</p></li><li id="fn:6" data-id="525b4fbb-5200-4b07-b42a-f92143c9c33b"><p>A good example of how meta-transactions have been used is <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-2612">ERC-2612</a>, an extension to the ERC20 spec. It adds a <code>permit</code> function that receives some signed data and sets an allowance from the signing account to some address. This is different from my simplified example, which transferred tokens directly, but the core idea is the same.</p></li><li id="fn:7" data-id="3c00028d-8418-460f-b977-9d6c111d6fe8"><p>For a noble but ultimately abandoned effort to solve the problem of centralization in the context of meta-transactions, you can read about Gas Stations Network. <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://medium.com/tabookey/1-800-ethereum-gas-stations-network-for-toll-free-transactions-4bbfc03a0a56">This article</a> has a good explanation of how it worked.</p></li><li id="fn:8" data-id="56025468-5cdc-4b19-b3ac-25b61dbe30a0"><p>EIP means Ethereum Improvement Proposal. EIPs usually involve changes to the protocol itself. ERC means Ethereum Request for Comments and it's used for standards at the application layer, like the ERC20 token standard. Technically ERCs are a subset of EIPs, but I’m not sure anyone cares.</p></li><li id="fn:9" data-id="1bc19e31-e482-4e88-a2b7-b06f6e760639"><p>EIP-3074 uses the word “sponsor” instead of relayer. We’ll keep calling them relayers anyway, to avoid introducing even more terminology.</p></li><li id="fn:10" data-id="2742be3a-9145-4a4b-9a58-d4f47c0c26f3"><p>I strongly dislike the idea that EOAs shouldn't be improved just to protect the AA roadmap. Ethereum too often falls into the trap of only thinking long-term. As laudable as that might be, sometimes you need to balance it with short/medium term improvements.</p><br></li><li id="fn:11" data-id="27703dda-8a53-461c-baee-8edb7aec7bee"><p>RIP means Rollup Improvement Proposal. RIPs are like EIPs, but aimed at L2s instead of the main chain.</p></li></ol>]]></content:encoded>
            <author>cethology@newsletter.paragraph.com (Franco Victorio)</author>
            <category>ethereum</category>
            <enclosure url="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/043c006087c310137fb80a85d49c5d56c4608813ef0c0485fd573a13792ff635.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[EIP-8024; or, killing the "stack too deep" error]]></title>
            <link>https://paragraph.com/@cethology/eip-8024;-or-killing-the-stack-too-deep-error</link>
            <guid>dj8OGpTAxrbKm4cPiQEu</guid>
            <pubDate>Thu, 12 Mar 2026 10:43:21 GMT</pubDate>
            <description><![CDATA[If you've written a non-trivial amount of Solidity, you've almost certainly run into the "stack too deep" error. EIP-8024 is a proposal to modify the EVM and put an end to this error for good. In this article we'll go in depth into the "stack too deep" problem, how EIP-8024 addresses the underlying issue, and why the solution is not as simple as it appears at first.]]></description>
            <content:encoded><![CDATA[<p>If you've written a non-trivial amount of Solidity, you've almost certainly run into the "stack too deep" error. <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-8024">EIP-8024</a> is a proposal to modify the EVM and put an end to this error for good. The EIP is being considered for the upcoming Glamsterdam upgrade (expected mid-2026) and has a good chance of being included.</p><p>In this article we'll go in depth (no pun intended) into the "stack too deep" problem, how EIP-8024 addresses the underlying issue, and why the solution is not as simple as it appears at first. Along the way, we'll explore some interesting and lesser-known aspects of the EVM.</p><h1 id="h-stack-too-deep" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Stack too deep</h1><p>The “stack too deep” error shows up in many scenarios, but they mostly boil down to using too many local variables at the same time. To illustrate this, take the following code:</p><pre data-type="codeBlock" text="contract Example {
    uint public x;

    function myFunction() public {
        uint x1 = 1;
        uint x2 = 2;
        uint x3 = 3;
        uint x4 = 4;
        uint x5 = 5;
        uint x6 = 6;
        uint x7 = 7;
        uint x8 = 8;

        x += x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8;
    }
}"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Example</span> </span>{
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> x;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">myFunction</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">uint</span> x1 <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
        <span class="hljs-keyword">uint</span> x2 <span class="hljs-operator">=</span> <span class="hljs-number">2</span>;
        <span class="hljs-keyword">uint</span> x3 <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;
        <span class="hljs-keyword">uint</span> x4 <span class="hljs-operator">=</span> <span class="hljs-number">4</span>;
        <span class="hljs-keyword">uint</span> x5 <span class="hljs-operator">=</span> <span class="hljs-number">5</span>;
        <span class="hljs-keyword">uint</span> x6 <span class="hljs-operator">=</span> <span class="hljs-number">6</span>;
        <span class="hljs-keyword">uint</span> x7 <span class="hljs-operator">=</span> <span class="hljs-number">7</span>;
        <span class="hljs-keyword">uint</span> x8 <span class="hljs-operator">=</span> <span class="hljs-number">8</span>;

        x <span class="hljs-operator">+</span><span class="hljs-operator">=</span> x1 <span class="hljs-operator">+</span> x2 <span class="hljs-operator">+</span> x3 <span class="hljs-operator">+</span> x4 <span class="hljs-operator">+</span> x5 <span class="hljs-operator">+</span> x6 <span class="hljs-operator">+</span> x7 <span class="hljs-operator">+</span> x8;
    }
}</code></pre><p>This compiles fine, but if we add one variable more…</p><pre data-type="codeBlock" text="contract Example {
    uint public x;

    function myFunction() public {
        uint x1 = 1;
        uint x2 = 2;
        uint x3 = 3;
        uint x4 = 4;
        uint x5 = 5;
        uint x6 = 6;
        uint x7 = 7;
        uint x8 = 8;
        uint x9 = 9;

        x += x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9;
    }
}"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Example</span> </span>{
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> x;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">myFunction</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">uint</span> x1 <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
        <span class="hljs-keyword">uint</span> x2 <span class="hljs-operator">=</span> <span class="hljs-number">2</span>;
        <span class="hljs-keyword">uint</span> x3 <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;
        <span class="hljs-keyword">uint</span> x4 <span class="hljs-operator">=</span> <span class="hljs-number">4</span>;
        <span class="hljs-keyword">uint</span> x5 <span class="hljs-operator">=</span> <span class="hljs-number">5</span>;
        <span class="hljs-keyword">uint</span> x6 <span class="hljs-operator">=</span> <span class="hljs-number">6</span>;
        <span class="hljs-keyword">uint</span> x7 <span class="hljs-operator">=</span> <span class="hljs-number">7</span>;
        <span class="hljs-keyword">uint</span> x8 <span class="hljs-operator">=</span> <span class="hljs-number">8</span>;
        <span class="hljs-keyword">uint</span> x9 <span class="hljs-operator">=</span> <span class="hljs-number">9</span>;

        x <span class="hljs-operator">+</span><span class="hljs-operator">=</span> x1 <span class="hljs-operator">+</span> x2 <span class="hljs-operator">+</span> x3 <span class="hljs-operator">+</span> x4 <span class="hljs-operator">+</span> x5 <span class="hljs-operator">+</span> x6 <span class="hljs-operator">+</span> x7 <span class="hljs-operator">+</span> x8 <span class="hljs-operator">+</span> x9;
    }
}</code></pre><p>Then solc (the official Solidity compiler) gives us an error:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/4baa7d824f74eaa73ffac3371b25d6117218b3c35d0fe67b260245553b70ae66.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAGCAIAAAAt7QuIAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByElEQVR4nGOw0dc/2d+/rbllfWPjjLz8UGvrngkTjhw50j9x4oyZ0+/dv19fX19RUdHY3Hzz5s3169fPnj177bp1M2ZO11BX7+3tnTVruqura1l5+YyZ0xMTEtva2ucvWLBixYrde/bs3bu3qKiIQVBAoLWh6eeXLy1NTfPnzp3U319QWLhixYr169dnZmXt3bt39uzZ/RMntrW1zZ49e+/evdu2bZsyZQrIoPnzly5fumXrFm9vnxkzp+/avWvp8qWZWZll5eVFRcUQlb29vQxiYmJNLa0dXV2tne3Xb9x4+uxZ/8SJ8fEJqampgYGBdnZ24WDg6eUlDQNiYIDGlZSSgrPhsmJiYgysrKzyCgp2dnbe3t6VlVWpqanZYLBy5ars7OzU1NTw8HCIZaysrDy8vCwszHwwwMrKysnOzsDAICggCDcaGfDx8TEEBQWXlZcbGBowgIGOjo6rq2t2draWtnZqaqqZhUVqaqqlpVVKSnJSclJcXFxRUVFcXGxqaqqdnV1vb6+3t3dbW5uGujofHx92C2RlZI2NjQ2MjCD+UlRUVFNTCwwM1AEBbUVFRR0dHTgJkQXTStLS0nApKSlJrD4QExMDAG2puzSWwmD1AAAAAElFTkSuQmCC" nextheight="173" nextwidth="869" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>There are many workarounds for this problem. Perhaps the most popular today, and the one the error message itself suggests, is to use the IR pipeline. This is an alternative compilation mode in solc which does fix the problem, but which has its own issues:</p><ul><li><p>It's slow (although getting better over time)</p></li><li><p>It can result in a worse debugging experience</p></li><li><p>It still has some expressiveness limitations<sup id="fnref:1"><a class="footnote-ref" data-id="e3962610-8728-44bd-908a-cebef6130c07" href="#fn:1" data-reference-number="1">1</a></sup></p></li></ul><p>At this point, it’s likely a Vyper guy will jump out from behind you and, before you have time to ask him how he got into your house, yell “Vyper doesn’t have stack too deep errors!”. Which is true. But this comes at the cost of not being able to do cyclic calls (including recursive functions) and, in any case, rewriting your contract in another language is hardly a practical solution.</p><h1 id="h-from-solidity-to-evm-bytecode" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">From Solidity to EVM bytecode</h1><p>To understand why the "stack too deep" error happens, we need to look at how Solidity handles variables. The EVM has three main areas<sup id="fnref:2"><a class="footnote-ref" data-id="632c9894-d55e-4dea-9a86-aebcf7a67cc6" href="#fn:2" data-reference-number="2">2</a></sup> where values can be stored: <strong>stack</strong>, <strong>memory</strong> and <strong>storage</strong>. The stack and memory start empty in each execution. The storage, on the other hand, persists between calls.</p><p>Solidity uses these three areas differently depending on the language feature. Storage variables use the permanent storage area. Local variables, on the other hand, are ephemeral, and so they have to use either the stack or memory areas.</p><h2 id="h-using-the-stack" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Using the stack</h2><p>In the previous section, we saw how a contract with eight local variables worked fine with solc's default compilation mode (normally called the "legacy pipeline"), but adding one extra variable results in a "stack too deep" error. To better understand why this happens, let's look at what goes on at the EVM level when we have seven variables. Executing that bytecode will eventually result in the stack looking like this:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/7052f17de8d7c0a5248066c92f774557c449bb70ac2ff2ecdc24898100b94f74.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAOCAIAAADBvonlAAAACXBIWXMAAAsTAAALEwEAmpwYAAACz0lEQVR4nJWTS08TURiGv16mOoVph04r9kpbOh3K9Dad6bT03k5aVBQQLYpCQhAFIyTqhpUmLAwrV5gogdgY4yVEZefCBT/BxEQXSrhYIC4MulDAsNFUiJZaSE2enNV33ifvOfkAQUkxRss1bpuj1US2iFFKijWWRYw6btwcP2yJqM1pMeaU7DFWjExOAYK7gOCrDCF3eLDe1wsYI9Y2gYYHwl8Kzi2ubHKuEz6m22qKA86UmdkFL8HdgOBOUPkVxlBAuB5KjwDqVtMZ1BwFnAUVtwulbyG/ztmi1dqgEMzKFB5Q+UtniiH8EqVrRyDXBdn4lVqqA5TuQvS/6SoOcDa/+sNDJsSmZJDrkNUw+6X/EUgxuiDQN9HBfr0jC0oG1HtcwNn3c2sMmZCYktHQmf9rUGUIsfEhvaNzR1DWgTEPH7/y2RMHjPEI14Fg3ooEhQYYg2r5oHCNDgyC1AGYF+SewlkC6plf+tZoS5gNsaj/FCi8FT0Rqg1Yon1kvPdIz+3mnvHawHm65bLv9LAh3FuMMXqBYM9+/LRloAS5MU7xnYD7KhLICI+kLkXY02zqKp8ZEekiCnumpqFZVpcqAfSxxZVNIyUoLSlHoGKBGKMBZ+U63hW+RAcG4CCNWWMElQKVrxCBF4ExC/kNIyUQZNoV7qq4Ae4CDY+bw1xyONM+hhpTmC1eS2d2vlrD/wVnF/LrOltSaUnV+04C4qxIACISRLRIzVBcXwM3CGCHQ5xYxwNCF4CGIsh3c1+MlKCqF+LHh6zeNpk2vF+PbYHVedTOd3kiXW3dt1rPjVm87fyx/nDrgJ3PklyWTfb5hYvbsEL/xtZPHZkCTYAgMyZXG6KP/V5Jf3kIvrC2o6N3Hj19effek/u5mVxuZmJyeir3PPfgxcTk9OTUszdvlxeXvs7Pr32Y+7y8+n129jWCklLMIULsgJCSaocU2w8EJX8BPe3dKRahRrkAAAAASUVORK5CYII=" nextheight="458" nextwidth="1075" class="image-node embed"><figcaption htmlattributes="[object Object]" class=""><a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://fvictorio.github.io/evmd/?bytecode=0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80630c55699c14610038578063c3780a3a14610056575b5f5ffd5b610040610060565b60405161004d9190610102565b60405180910390f35b61005e610065565b005b5f5481565b5f600190505f600290505f600390505f600490505f600590505f600690505f6007905080828486888a8c6100999190610148565b6100a39190610148565b6100ad9190610148565b6100b79190610148565b6100c19190610148565b6100cb9190610148565b5f5f8282546100da9190610148565b9250508190555050505050505050565b5f819050919050565b6100fc816100ea565b82525050565b5f6020820190506101155f8301846100f3565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610152826100ea565b915061015d836100ea565b92508282019050808211156101755761017461011b565b5b9291505056fea2646970667358221220038b786a38f4bc15d6b9cbd59d0dfce61e3de20d156ad0d23ecfcffd9338227864736f6c634300081e0033&amp;mode=call&amp;calldata=0xc3780a3a"><u>Try it on evmd</u></a></figcaption></figure><p>And if we have eight variables (the limit before getting the error), the contents of the stack will be:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/5d689882059ff54137b21843c9697e31aad88e9fd826d308dc8ca30a412e1cc1.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAANCAIAAABHKvtLAAAACXBIWXMAAAsTAAALEwEAmpwYAAACv0lEQVR4nI2TXUhTYRjHn52dsx21pvNsZzv7UMfmOvvezs7Z+Zwum2Yfm2luitWFwi4Cb6KL8EK8ELuxyESUhKAsqIiSMAMRCoLoptBRYUT3IhF0WVfFNhOVTYQf79Xzvr/3/7zPC3iVGyMY3MRa3Z1eblBLcnUO2eRJIPooRjA7aEgWVN4Hj1dZIeNypUhKRPTh3QV7ig0M4D5XKDU1vQB4FQ2kiFH8MXbAL14GkgdSUJlEIIXdoJQM4M1/+9kpZ+z2BBs4W1sfBXJ/WQkVKYCOaQp1zcw+LAkEzBxrZvo9fA7qmKJjPyglAXg/ft5s49JAcO5wmqiPglEoW1xAF7EHUrem7v0XULwznKG5HBBsZYFn/euPdr4bp2Sp5SJRz1QSqIqCxkBqavr+HkEhQYUbISYRtMHFl+/l0EnUxHsDqQMSVBJk/fJwodcEV16A+l+9yccjpzGLwikDh01QfdRrFy854kN86lrywnSTknMlc8Hu4QZlyCoN7uCI5zC6+9P3X+18l5mIypG0joiCkT9UAsSi4A0tTqZf6BjR2BKaxpZadxKzxRGLsgPe0AraUH5ji5V6a8xSWOgrG7SMoKqaBiKmMcccwR6u9aqaFHC7bPAkERMPerZwShHUxAPQ+Y2tqJhBLLKbz1acn7JvoLUIjmCPcmrM6ExhNsng61BTEhhi+6aoJEApyR09fxjB7VICFSngFqGZ6eNPjKhNEm6T9HQbSkmq4tuW0FgUQP1FQa/WpnjF7AEt2v4HvjOTN+6CRuMGHaMyslY6HZSvQC2LWEXClyzsP8JATXibuihA8/qXTUbOqCnJGTl3gKAwcniQ5vteLL2FxPH+mfnno+OzE5ML128+Gh2fG5uYHx2/U1znniy+W17NL62sLa2sLS5/+P3nb5O7TWeTg2IWJTmEYMuCkhxgPi+fffrs9T9rH+N44xyBVAAAAABJRU5ErkJggg==" nextheight="416" nextwidth="992" class="image-node embed"><figcaption htmlattributes="[object Object]" class=""><a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://fvictorio.github.io/evmd/?bytecode=0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80630c55699c14610038578063c3780a3a14610056575b5f5ffd5b610040610060565b60405161004d9190610113565b60405180910390f35b61005e610065565b005b5f5481565b5f600190505f600290505f600390505f600490505f600590505f600690505f600790505f6008905080828486888a8c8e61009f9190610159565b6100a99190610159565b6100b39190610159565b6100bd9190610159565b6100c79190610159565b6100d19190610159565b6100db9190610159565b5f5f8282546100ea9190610159565b925050819055505050505050505050565b5f819050919050565b61010d816100fb565b82525050565b5f6020820190506101265f830184610104565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610163826100fb565b915061016e836100fb565b92508282019050808211156101865761018561012c565b5b9291505056fea2646970667358221220f870e8b81a215b972f24d27a2e2e08222024680e52ffd449b7032a252fb8f16364736f6c634300081e0033&amp;mode=call&amp;calldata=0xc3780a3a"><u>Try it on evmd</u></a></figcaption></figure><p>In both examples we can see all our variables at the top of the stack, ready to be used. But if we add one variable more, the code won't compile. Why? The stack can have up to 1024 elements, surely we can keep adding values there?</p><p>Take a look at the previous images again, but this time pay attention to the instructions that were executed immediately before. With seven variables, we have a sequence of instructions that looks like <code>DUP1 | DUP3 | ... | DUP13</code>. With eight variables, we have a slightly longer sequence with a <code>DUP15</code> at the end. You don't have to be a math genius to guess that a ninth variable would then add a <code>DUP17</code> instruction. But if you check <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.evm.codes/">the list of EVM instructions</a>, you’ll see that there are only 16 <code>DUP*</code> instructions, from <code>DUP1</code> to <code>DUP16</code>. The same happens with <code>SWAP*</code> instructions. In other words, <strong>we can only access the top 16 elements<sup id="fnref:3"><a class="footnote-ref" data-id="e4fe4e46-7de9-4c88-bc34-38d78b000344" href="#fn:3" data-reference-number="3">3</a></sup> of the stack</strong>. Anything beyond that is "too deep".</p><p>Why this particular code was generated doesn't really matter. This is a contrived example, and in a more realistic scenario the stack might be used more efficiently. But there will always be a limit to the number of elements from the stack that you can use together at a given time.</p><h2 id="h-using-memory" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Using memory</h2><p>Given this limitation of the EVM stack, why not use the memory area instead? This is actually what the IR pipeline and Vyper do, but the EVM memory has a couple of peculiarities of its own that make it less than ideal for storing local variables. The details are complex (check the aside below if you want to learn more), but the bottom line is that putting variables in memory comes with a significant cost in complexity and gas usage.</p><div data-type="details" class="details"><p data-type="detailsSummary" class="detailsSummary"><span style="margin-right: 8px">▼</span>EVM memory and local variables</p><div data-type="detailsContent" class="detailsContent"><p>The EVM memory has at least two characteristics that make it a less than ideal fit for storing local variables:</p><ul><li><p>It’s not <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://en.wikipedia.org/wiki/Random_access">random access</a></p></li><li><p>It has an “infinite” length</p></li></ul><p>Of course, memory is not really infinite. By this I mean that the available memory grows as it's used: each time you read or write some new part, the available memory will expand to include it. But the cost of that expansion grows with the memory size in a quadratic way. This means that using the lower part of the memory area is cheaper, which explains in turn why it doesn’t have the random access property: accessing any two areas doesn’t always have the same cost.</p><p>These characteristics affect the management of local variables in two ways. One is that the more variables you use, the more costly it will be. And this is not only about the variables in an individual function: if you have multiple (internal) calls, they will share the memory area, expanding it and increasing the gas used each time. The second problem is related to the fact that EVM memory has a beginning but not an end, preventing the normal compiler trick of using one end of the available memory for the stack and the other one for the heap.</p><p>These peculiarities of the EVM memory are the reason why the IR pipeline and Vyper have a limited expressiveness with respect to recursion and cyclic calls. Check the resources at the end if you want to learn more.</p></div></div><h2 id="h-fixing-the-evm" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Fixing the EVM</h2><p>In summary, the compiler has to put local variables somewhere, and the two areas where it can do it are the stack and memory. Both have limitations: memory comes with its own trade-offs, and the stack only lets you access its top 16 elements. If we want to fix this at the EVM level, we have two possibilities:</p><ul><li><p>Make memory less weird, as in <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://eips.ethereum.org/EIPS/eip-7923"><u>EIP-7923</u></a>.</p></li><li><p>Make more items in the stack accessible, as in EIP-8024, the subject of this article.</p></li></ul><h1 id="h-new-instructions" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">New instructions</h1><p>As we saw, the fact that only the top 16 items of the stack can be accessed is the underlying issue that causes "stack too deep" errors. If we raised that limit high enough, the error would go away. The most straightforward way to make that happen is by adding new instructions.</p><p>One possibility is to add new variants of the existing instructions. That is, to add new instructions like <code>DUP17</code>, <code>SWAP17</code>, <code>DUP18</code>, etc. This doesn't seem like a great idea. If we wanted to allow access to the top 100 elements of the stack, we would have to add 168 new instructions. Leaving aside the fact that there aren't that many available opcodes, this would be ugly and wasteful.</p><p>A better idea is to add only two new instructions, <code>DUPN</code> and <code>SWAPN</code>, that take an arbitrary N as input. Where does that input come from? There are many options, but the two most obvious are:</p><ul><li><p>Take N from the stack. To duplicate the 20th element, you'd execute a <code>PUSH1 20</code> instruction followed by <code>DUPN</code>.</p></li><li><p>Use the next byte after the instruction, like <code>PUSH1</code> does. With this option, a single <code>DUPN 20</code> instruction would duplicate the 20th element.</p></li></ul><p>It's easy to argue that the second approach is much better: the resulting bytecode is smaller (two bytes vs three), it consumes less gas (one instruction vs two), and it's more friendly to static analysis.</p><p>Arguments encoded directly in the bytecode like that are called <strong>immediates</strong>. Right now, only the <code>PUSH*</code> instructions use them; every other instruction that has an input takes it from the stack. These <code>PUSH*</code> immediates have different lengths depending on the instruction: one byte for <code>PUSH1</code> (e.g., <code>PUSH1 0x14</code>), two bytes for <code>PUSH2</code> (e.g., <code>PUSH2 0x1234</code>), etc. For our two new instructions, a one-byte immediate would be enough to let us access the top 256 elements of the stack, a significant improvement over the current 16 limit.</p><p>In fact, we could even do a little better. Using <code>DUPN</code> with a value like 5 wouldn't make sense given that <code>DUP5</code> already exists (<code>DUPN 5</code> would have the same behavior). But we could define our new instructions so that the value of the immediate is not interpreted verbatim. Instead of saying that <code>DUPN X</code> duplicates the Xth element of the stack, we could say that it refers to the item at position <code>X + 17</code>: <code>DUPN 0</code> would duplicate the 17th element, <code>DUPN 1</code> the 18th, and so on. This way, the combination of existing <code>DUP*</code> instructions plus the new <code>DUPN</code> would let us access the top 272 elements.</p><p>This all sounds great. Sadly, things aren't that simple. To explain why, we need to take a detour and talk about jumps.</p><h1 id="h-jumpdest-analysis" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">JUMPDEST analysis</h1><p>The EVM works in a simple, sequential way: it executes an instruction, moves to the next one, executes it, advances again. The only exception to this behavior is the <code>JUMP</code> instruction<sup id="fnref:4"><a class="footnote-ref" data-id="3f689144-6274-46e0-b461-636903c0f2d9" href="#fn:4" data-reference-number="4">4</a></sup>, which takes an item from the stack and continues the execution at that position. But it can't just jump anywhere in the bytecode: the destination has to be <code>JUMPDEST (0x5b)</code>, a special instruction that serves only as a marker for valid jump destinations.</p><p>You'd be excused for thinking that this is a simple check: read the byte at the jump position and verify that its value is <code>0x5b</code>, the <code>JUMPDEST</code> opcode. But that doesn't work. Imagine there's a <code>PUSH1 0x5b</code> instruction in the code. Is its second byte a valid jump destination?</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/c65fd58ddf4874e7f8c528f312bb342be7b88f89a51f61a26e8e1f858f3fa07a.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAFCAIAAACreXkmAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABIklEQVR4nGOQV3OLS56SljVbVt2bgUGOiV+XiV+XgU0TwmBCQoz8egzs6pJKDrmly0GKeXWYRY3xI04xYwYJJcfQ1EmxOTPjcme3TzptYJ9p4JheOWWTukU8GtKySRFSdhJTsHfwrIjNmCCk7M4uZQtHQsru6ibhyCJs0vasYmYMEirOee2bYsvmzdrw6PHn/+U9u8v6dq678Cm/ZRMaKu7aaeyUzsKvzSBiqqAXyCvvyCZlA0fcsnZCym7IIiBrxEwZ2AR02aVsxVTcTRwycktXsUvZcsjYaNvFMoiaoyFGMQtmESMOMRM2aXtGMQtkx2JFUB8wcahzSloq63qzi5kyixixi5mCrGXXgjCQEauYGZuALoeADouYOUgxoQiAxAEAbq1cfacXWekAAAAASUVORK5CYII=" nextheight="149" nextwidth="911" class="image-node embed"><figcaption htmlattributes="[object Object]" class=""><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://fvictorio.github.io/evmd/?bytecode=0x600456605b00&amp;mode=deploy">Try it on evmd</a></figcaption></figure><p>For the EVM, it's not: you get an exceptional halt and the transaction reverts.</p><p>This is where JUMPDEST analysis comes in. Before executing any code, you need to process the whole bytecode to find out which positions are valid jump destinations. This, incidentally, has to start from the beginning; you can't just look at the bytes before a <code>0x5b</code> and figure out if it's a valid <code>JUMPDEST</code> or not.</p><div data-type="details" class="details"><p data-type="detailsSummary" class="detailsSummary"><span style="margin-right: 8px">▼</span>Why not?</p><div data-type="detailsContent" class="detailsContent"><p>Why can’t you figure out locally if a <code>0x5b</code> byte is a <code>JUMPDEST</code> instruction?</p><p>You could make the following argument: since the “biggest” push instruction is <code>PUSH32</code>, it’s possible to determine whether some <code>0x5b</code> is a valid jump destination by looking at the previous 32 bytes: check if the one immediately before is a <code>PUSH1</code>, or if the one before that is a <code>PUSH2</code>, or if the one 32 positions away is a <code>PUSH32</code>. If none of those is true, then it’s a valid <code>JUMPDEST</code>.</p><p>It’s easy to prove that this approach doesn’t work. Imagine that the last byte you check is <code>0x7f</code>, which corresponds to the <code>PUSH32</code> opcode.</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/6f7f8dc40d279aa637e18fcacb5d84eb9684b628f717620cd3c8c4bacd2557e9.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAMCAIAAACMdijuAAAACXBIWXMAAAsTAAALEwEAmpwYAAACLUlEQVR4nK1UTWgTQRSek4UeqiVarIeihEIPAQ8FzSXVeKiILbSBFsQa8SL0IDkUCr35dxA0h9VQhFbEpmC1ad30IBQRj3uYCnve1kQbcTbZiZCanYTZJE+SR5c2TaGIH9/hvXnz3jfzHjMklUqbZraJOctyafH8PxDrpFJpYppZ2I9qreZUqy7LUoojoCRExZGOLDmyVHEkQA0ALJ4nOctyS1ccBwA+Li9PDg5Oj4ZmxsZvBwYW5+c3t7a+bmzouq6qqr4LSim6lNJvm8b9p7GO4ITvxlTfWKT72p219S8AkLOsFgLx5y9u+s4P93hD3t6Rs95PKytuiFLqbpZSoluWEgAev1wkviHSe7VO//jc0gcAMM1sC4HkwkJ/WztpoL+tHQVKQmBFuQshBAoUbRsAHsXeEHIcs8ixvtfv1w4VeDs7G+zsCpw4deVkd6DD83l19Sg3eFAXOEdO+0nPAOm6MLeUPFRAjcfvDlyeGhqeGQ3duuh/FYu5M0gkEntngC6lNPM9Pf3wGbk0cWbkXuf1SRIMv0uut5iBe7SibZel/MVYzrIKDQgh0ECbMcY5d9cZY7Zd/LOzg7TtIp41n//dLCAb91UURVVVj8cTiUQAgHPOGCsUCtilaDQaDoexP4qiPGkA0xVFwXWs00IAoWmaYRiqquJuxphhGJxztLUG0NU0DTuGibquG4axT+DgQ/uPqD+0H9vb/ADcPwNd/DOaoq69N4quuzOT+fkXKf+81D2CmqQAAAAASUVORK5CYII=" nextheight="506" nextwidth="1367" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>Does that tell you that the <code>0x5b</code> at <code>i</code> is part of a <code>PUSH32</code> immediate, and therefore an invalid jump destination? Well, no! That <code>0x7f</code> byte could in turn be the immediate argument of a <code>PUSH1</code> just beyond the range you checked:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/2dc858f9b3c50b1ba9d306ac52048eb50a2de2dda7132910ff178d1e56229093.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAMCAIAAACMdijuAAAACXBIWXMAAAsTAAALEwEAmpwYAAACP0lEQVR4nLWTP4gTQRTGRwhYpQw2ooV2IhYRRLFRLGyukEOCnY14YmUb/FMETRMIQdDiBIuoKWQOFuTAi5BmmmkGnGYUN/FYYWazk/P2NHPnTcQn2Rf3TCeiP17xvZlhPvZ7O6TfHxgTGxMndvQPy2R39vsDYkwMAC4jTVPnxu6v2Nnenkz8xO9Ma+IBfgBAYkdkNNpwznW7XSGEUkoIQSlljMmM1QwppciglKIQQnDOseWcf3j/rnq/Sc4sHr108/DC9eK5KyurbwBgmCTk8+ZmmqZCCOdcr9fLtZQyDMMoipRS8AvOea6999h+290FgHsPn5Ij58nBs9M6sfCo/RIAjIlnBlLKarVaKpVarZZSKgiCcrlcqVSCIEADn8E5R+G9d86hwdfxGADuNp+QPQ4tv1iZMxBCSCk555iSEIIxxjPCMPyTL7jdXCbkACkeJ6WTZP+xx8/onoFz7vXaWhiGSqkoijqdDs5Da80YC4IgnwFu5TPAlnMeDQa37jwgpxaLF6/tu3CVnL78nL6azWCYJACQ/T/OWhuGYZrhnIuiSGudtyhQa62ttfm61tq58ZetLSznxt8nEwAYjTZmBm+lrNfrtVqNEOK9b7ValNJCobC0dAMArLXohCnV6/VKpYL5NBqNWgbm1mg0cN17P2dgre31elLKdruNtyilKKV4WmutlLLWomYZ2DLGMDE0wGznDPCh/SemD+3j+rq1NjbxMEmMiTFcfOjGxDZjmCTDJEGd7+b6911s85NR9Okn3hXB6iRBHvQAAAAASUVORK5CYII=" nextheight="506" nextwidth="1367" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>And, of course, you can't even be sure whether that's actually a <code>PUSH1</code> or if it's part of the immediate of an instruction before it. There’s no way around it: you have to start from the beginning.</p></div></div><p>The reason JUMPDEST analysis is relevant to this discussion is that our new instructions take immediates. Why is that a big deal? We could just include them as part of the analysis and forbid a jump to the second byte of <code>SWAPN 0x5b</code>. Except we can't. To understand this, let's assume that the opcode of <code>SWAPN</code> is going to be <code>0xaa</code> and that the following bytecode is already deployed onchain:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/8a4169aa592703f27097addd42b8a3889025621b59e00cc0bc5f62982bd2027d.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAGCAIAAAAt7QuIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABVElEQVR4nGMQljQzt022ccgUkrZiYFRiE9BlE9BlYFCBMNiQELugPgODgrCkmaV9Oo+4KSOHBqYaNMTKqc6grBOQmDM/t2KVk2+lpXMhl5yTkJq7XVABt5wjGuJXdmPi11XS9s+rWGvrXsgmZcMuZQtHfPLO4qoeyCJs0vYcYiYMkuruhR2bMhpXLdj28sn3/00zTk1dd2/F8ffFXTvRUMWEgwa2SWz8OswSFi7+RfzyTmxSNnAkpOympBeELMIuZQuygEfSXFonUFDZNTF39uPX/+cvfxiRNEVaK0BE3VNU3RsZiWn4ckiac4oZs0vZMoiaI5vFJmXDIGqOJgi1gIVfj03aXl7XX90s1ie0lVPagYFXn0XShlHMAg0xiFmxCBhwiJmwSdsjBwUuBA0iVk51HklzWXVnZn4dBhZVVhF9DjETTjFjTAQSF9DmFtTBpQCregBb2mk6YbKCQwAAAABJRU5ErkJggg==" nextheight="168" nextwidth="913" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>This is a perfectly valid bytecode. There is no instruction for opcode <code>0xaa</code> yet, but that doesn’t matter because we are jumping over it to a valid destination.<sup id="fnref:5"><a class="footnote-ref" data-id="2abfedc7-857a-4360-b31c-fef7ab609487" href="#fn:5" data-reference-number="5">5</a></sup> But what happens if we upgrade the EVM and <code>0xaa</code> becomes the opcode for <code>SWAPN</code>? The disassembled code now looks like this:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/8682205244418a4c3b1935f60448a397f64d77b777993246bbd66f150aca158f.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAFCAIAAACreXkmAAAACXBIWXMAAAsTAAALEwEAmpwYAAABH0lEQVR4nGOQUXHxC20OieoQkXdiYJBl5NVh5NVhYFCFMBiREAOvLgOrqrSyU2R8H0gxry6riDEcMQsYMPIbIouwiJiw8mkziCk5+sZ2+sV2hST1Z5evUbOI13VKTa2fJWcUjoaUzWLYJcxF5O3tvauCY1u4ZB1YJG3gSEDJTVE/CFmEWdKWTcSIQUzFOaVueWjetOlrHrz++z+raUNR37bFR58lVa9AQ2kN69XMI0GOEjaVUPNglbBmlrCCI04Ze25Ze2QRFkkbVmFDBmY+XUYJa0EFB33r5KCoHkZRczZpa1kDPwZhU3QkYs7Mr8cmYsQsacsgYo7sWKwI6gMmdnUuSQtFLQ9QwAlDAxEtfBFhyq/Dzq8NYmDIYlfPpw0APA5VWeyYHGYAAAAASUVORK5CYII=" nextheight="154" nextwidth="913" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>Oops, now we are jumping to an invalid destination, even when the underlying bytecode is the same! This is a breaking change.</p><p>Conversely, an invalid destination can become valid. In the following example, we are trying to jump to a <code>0x5b</code> which is the immediate of a <code>PUSH1</code> instruction:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/a64c231c1abc3e678050756dc79d34d9145a00b857c5d9777a95bdaa155aa98c.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAGCAIAAAAt7QuIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABXUlEQVR4nGMQkTa3dsq0ccgUkrZiYFBkE9BlE9BlYFCBMNiQELugPgODsoi0ua1jFo+4KSOHBpoCZn4dNBFWTnUGZZ2A+Ky5uRWrXPxq7TzLueScRNQ9PWKrueSc0JCAsjsTv66Stn9B5QaXgAo2KRt2KVs44pN3ltTwRRZhk7bnEDNhkFR3L+rcltG4asG2l6/+/p+67t7Udfdmb3tY3r8XDdVMPWpgm8TGr8MsYeHoW8gr78gmZQNHQspuSnpByCLsUrYgCzjFjMU1fHnlHZPz5x4/8z8iZXJ25bKUyhncco5Cyu7ISFDFg0PSnFPMmF3KlkHUHNksNikbBlFzNEGoBSz8elxyTvK6/tpWiQ5elQz8RgwipuKaniySNmiIWcqWTdSYQ8yETdoeOShwIWgQMTAocIoZi8lbM3JoMLCocooZc4oZM3BoYo1kVk51Vk51dkF9TFmsiJVTHQBUamhYw9+AwAAAAABJRU5ErkJggg==" nextheight="169" nextwidth="913" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>This will result in an "invalid jump" exception today. But if we upgrade the EVM with our new instruction...</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/07d6081f0ef424cf4b76373144c17eb2d406f8f3dc898820c76a7745e4649c56.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAGCAIAAAAt7QuIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWUlEQVR4nGMQkTa3dsq0ccgUkrZiYJBnE9BlE9BlYFCBMNiQELugPgODsoi0ua1jFo+4KSOHBpoCZn4dNBFWTnUGZZ2A+Ky5uRWrXPxq7TzLhVQ8JbX97IMKueSc0JCAsjsTv66Stn9B5QaXgAo2KRt2KVs44pN3ltTwRRZhk7bnEDNhkFR3L+rclt6wfOH2109//C/q3NGx+NyBx//L+/eioZqpRw1sk9j4dZglLBx9C3nlHdmkbOBISNlNSS8IWYRdyhZkAaeYsbiGL6+8Y0LWjBdv/0cmTZUzCIwu7BFSdkdDgioeHJLmnGLG7FK2DKLmyGaxSdkwiJqjCUItYOHX45Jzktf1VzeLdQ9oFFX1YhSzYOAyYJG0QUPMUrZsosYcYiZs0vbIQYELQYOIgUGBU8xYTN6ahV+bgUWVhV+bTUCXU8wYaySzcqqzcqqzC+pjymJFrJzqAE0zZ3jqa++gAAAAAElFTkSuQmCC" nextheight="169" nextwidth="913" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>The opcode of <code>PUSH1</code> becomes the immediate of <code>SWAPN</code> and the <code>0x5b</code> becomes a valid destination.</p><p>At this point you might be wondering why there would be an invalid instruction in the middle of some code. One reason is that the EVM doesn't have separate sections of a "program" for code and data. Any arbitrary data included in the contract to be used in runtime (through an instruction like <code>CODECOPY</code>) has to be part of the bytecode. Since this is just data, it can easily have bytes that correspond to nonexistent instructions.</p><p>In practice, this kind of data is always placed at the end of the bytecode, not in the middle of it. But that doesn't matter. A breaking change in something like the EVM is serious, even if we speculate that the impact will be low.</p><h1 id="h-eip-8024" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-8024</h1><p>And now, at last, we have all the context we need to talk about the EIP-8024 proposal.</p><p>To recap: we'd like to be able to access deeper parts of the stack to allow high-level languages like Solidity to use it more freely for local variables. To do that, we want to add two new instructions, <code>DUPN</code> and <code>SWAPN</code>, that take as their input a one-byte immediate, letting us access way more than 16 stack elements. But we need to avoid introducing breaking changes related to JUMPDEST analysis.</p><p>One possible solution is to forbid the problematic bytes: <code>0x5b</code> (JUMPDEST) and the <code>0x60</code>–<code>0x7f</code> range (<code>PUSH1</code>–<code>PUSH32</code>). We can also simplify and disallow the complete <code>0x5b</code>–<code>0x7f</code> range, even if that includes four instructions that aren't problematic. Restricting the input range like this doesn't feel right though. A compiler would have access to more parts of the stack, but it won't be able to emit instructions like <code>DUPN 91</code> and it will have to work its way around it.</p><p>This is far from ideal. We can do better. And, in fact, we've already hinted at a solution when we suggested interpreting a <code>0</code> value as <code>17</code>.</p><p>Let's reframe the problem. We have a restricted range we can use for the immediate values: from <code>0x0</code> to <code>0x5a</code> (0 to 90) and from <code>0x80</code> to <code>0xff</code> (128 to 255). We want to map that range to a continuous range starting from 17.</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/578dc87c021d772ecfcab12f490a0fd8e80e42f2e41e824e40768ed66c226bc7.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAIAAABaL8vzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB4klEQVR4nK2Tz4vTQBTHH3gUT14EwaK7HrwIPXnw4H/grdCaxk0jESr7a8x2a9TAWuylmMWC5KCHHnroQlhzCVKFQA/OJX+ACIIGtLQBUzYtjt1M1pE2WGu7lD345XP4PuYN3/cGBlzXHfT7wb8iAxIOw3mGUTTPYUhn2iIaRTQihHS6Xeh0u2xKYXjIGHtpvEo/vC2V8+KTu1I5n1VyvCquKLlNnpNvcSiTeSDk5GwWcZyczkh30isliVdFXhWlcp5XRU4ReFX48OkjIT//BlBKGWP7r/c7bjuzI8B1gJvnIHUesktwFeAawDJcALgMkAC4cebsFYDTAJcATsHoaEQSQFgedVwEWIK379+Fw3B2gzim8WYvX16Td4sbFRlVtlBla6Mibz8rlu5v7txbLa2tT3i6uv74ESq8UOTdYtwW39quKm7b7fcHswH/V9/93vEB9GSKxlBKj34dzTMJ8Cilvu8TQjzPiw3G2BzLMIzYmKZpWdaMMcbCGC/a4Ache41GMplMpVKJRAIhpOu6aZr1el3TNF3XNU2zbbvZbLZardjYto0xrtVqCKFCofC8Wl38RB5jbPoTUEodx5lMOj/4tLEsy3GcRQGfv7hBEPh+7yAIDv6YSXlCfL93LF+/tX8DnUPnLjlXdAQAAAAASUVORK5CYII=" nextheight="358" nextwidth="1101" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">n is the value of the immediate, which is decoded as an operand in the useful 17–235 range</figcaption></figure><p>One option is to define our decode function like this:</p><pre data-type="codeBlock" text="def decode(n):
    assert 0 &lt;= n &lt;= 90 or 128 &lt;= n &lt;= 255
  
    if n &lt;= 90:
        return n + 17

    return n - 20"><code>def decode(n):
    <span class="hljs-built_in">assert</span> <span class="hljs-number">0</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> n <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> <span class="hljs-number">90</span> or <span class="hljs-number">128</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> n <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> <span class="hljs-number">255</span>
  
    <span class="hljs-keyword">if</span> n <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> <span class="hljs-number">90</span>:
        <span class="hljs-keyword">return</span> n <span class="hljs-operator">+</span> <span class="hljs-number">17</span>

    <span class="hljs-keyword">return</span> n <span class="hljs-operator">-</span> <span class="hljs-number">20</span></code></pre><p>Which maps our ranges this way:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/260908599780ef4c869759945c222eeb097f12a39bc14620f5575c79573fe8c3.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAIAAABaL8vzAAAACXBIWXMAAAsTAAALEwEAmpwYAAACkklEQVR4nG2Tz2sTQRTHRzzowYsXES96kdSiJw8WL94E/wFBCE2abfOraaGpummgnXRbVl2hF6GClQihGEiVkk3Zy2JziQRcVnJoSiosdaOJzWKnyabdpNmUJ00w1qQfvocZ3gfeezCDVDW/X63q/2PsG416ozf1ZrM3hw2z1zwyjwzD2CmV0E6pBCcwzUMAePMxPBiivNy4+9molxsfCrkoxj2CXQHKEbQP0TYbdrmmHI7AkH1q0Obz2R3zHopxU4zby41TjNuOnTZMbf/YNozavwamaQJAnI/v5IvWeQrdP48eXkWPriHKgu4gdO8cuomuI9SPkAWhB5eu3ELoMkJ9CF28gNCNVuHu2TPOfnS7ZfSh1NfPjXqjd4PjNm9j7wanqBHsmXj5JPBqenja7WJ8o4yPC9DsxMTzyclOXvgfh0JPh2c9nrkxz9zYzOtZZ8jrDHmpGXe+mNf1aneDNr+Kxc1sNpPJ7FerACDLsqIouVwOAJqtCbpmkmX529aWoigAsJnNKooiyzIA/N7dPX2DTCYT5+PRaLRQKADA8vvlT+vrcT4OAPVarWmaJ6PreiQSEQQhmUwCwOrqqiiK4XC400AzTZMQYhiGpmmEEACIxWIY42AwGIlERFGkaZplWYZh2tUu0ul0W+A4Lp1OMwzDsixN04SQcqWCDozah5WVgYEBq9VqsVhoml5aWuJ5PhaLLS4uRqNRSZJomsYYsyybSqUIIaqqapqmqmqhRTgc9vv9GOOFhYUvksRxHMaYYeZyudxB6xVpAHDyE5imKUkS/5e1REIQhLVEIplc17RjuYuNjQ2e5wVB6Mg8z4uiSMhuuVJF29/zuq4TsleuVMqVSvvQuXaFkL3enGq25Z+F4h/OQs2tPVe33QAAAABJRU5ErkJggg==" nextheight="358" nextwidth="1101" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>But we can use an even tighter definition through modular arithmetic:</p><pre data-type="codeBlock" text="def decode(n):
    assert 0 &lt;= n &lt;= 90 or 128 &lt;= n &lt;= 255
    
    return (n + 145) % 256"><code>def decode(n):
    <span class="hljs-built_in">assert</span> <span class="hljs-number">0</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> n <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> <span class="hljs-number">90</span> or <span class="hljs-number">128</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> n <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> <span class="hljs-number">255</span>
    
    <span class="hljs-keyword">return</span> (n <span class="hljs-operator">+</span> <span class="hljs-number">145</span>) <span class="hljs-operator">%</span> <span class="hljs-number">256</span></code></pre><p>Which results in the following mapping:</p><figure float="none" data-type="figure" class="img-center"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/6225e67b193d50bea70e2e0909124a6eb94b231ad8669029b3ec06bb913aadc9.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAIAAABaL8vzAAAACXBIWXMAAAsTAAALEwEAmpwYAAACjUlEQVR4nG2Tz2vTYBjHX/GgBy9eRLzoRTqHnjw4vHgT/AcEoaxds/XXusGcmq6wpXs3qsZbPQhGKoyyQKaMZtJLcL1UcyiRXDo6IcxUW9fg3q3plnZNxyNroc52X76H5+H5wPd54H2Rrhf3azXzf1n7VrPR7Hej1er3YdPuJ4/sI8uytisVtF2pwAnZ9iEAvP2YGI5SQXbS/3w8yE6ORH0U9o8xvjDlibhHaJeL8flmPJ7wiHtm2BUKuT2LAQr7KewPspMU9rsZr4uhtn5uWVb9X4Bt2wCQElPbxbJzkUL3z6OHV9Gja4hyoDsI3TuHbqLrCA0i5EDowaUrtxC6jNAAQhcvIHSjPbh79ox3EN1uEwMo++1Ls9Hsv+A45p3wfniGGmMCU6+ehl/Pjs76fTg0jkNsmI5NTb2Ynu765eMn0eiz0flAYGEisDAx92beGw16o0Fqzl8sF02z1hvQ0e9yeSOfV1V1v1YDAEVRNE0rFAoA0Gpv0LOToijfNzc1TQOAjXxe0zRFUQDgz87O6ReoqpoSUzzPl0olAEguJz+vr6fEFAA06vWWbZ+0aZrJ5WQ6nc5kMgCwuroqSVIikegGGLZtE0IsyzIMgxACAIIgMAxD0/TS0pIkSTRNx2IxjHFn2iNZljsAy7KyLGOMY7EYTdOEkL1qFR1Y9Q8rK0NDQ06n0+Fw0DTNcRzP8yzLYow5jsvlcgzDsCwbiUSy2SwhRNd1wzB0XS+VSrqu8zwvCALHcYIgfJXleDyOMY7H44VC4aD9igwAOPkJbNvO5XLptkRR7BaZTMYwjuEeqarawURR/LS21ikkSSJkZ69aQ1s/iqZpErK7V63uVaudotv2mJDdfp9KduBfpfJfaZTOlG2rlt4AAAAASUVORK5CYII=" nextheight="358" nextwidth="1101" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>In other words, a bytecode that includes the <code>DUPN 128</code> instruction will duplicate the <code>17th</code> element, and <code>DUPN 0</code> will duplicate the 145th one. This second approach is the one that the EIP proposes.</p><p>The EIP also adds a new <code>EXCHANGE</code> instruction that lets you swap two elements deeper in the stack, instead of always involving the top element like <code>SWAPN</code> does.<sup id="fnref:6"><a class="footnote-ref" data-id="964676a9-d5ee-4fc6-9a7a-f2aa1fa078ed" href="#fn:6" data-reference-number="6">6</a></sup> Similar to the other two new instructions, <code>EXCHANGE</code> takes as input a one-byte immediate which is only allowed to have values that don’t break JUMPDEST analysis. This single byte is used to encode two operands, <code>M</code> and <code>N</code>. The instruction then swaps the <code>M+1</code> and <code>N+1</code> stack items. The exact details are interesting, but this article is long enough as it is; check the resources at the end if you want to learn more.</p><h1 id="h-closing-words" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Closing words</h1><p>While EIP-8024 hasn’t been officially confirmed as part of Glamsterdam, it has already been implemented by clients and tested in devnets<sup id="fnref:7"><a class="footnote-ref" data-id="1f88fb55-1c66-4e7d-8c85-9ffb415da88e" href="#fn:7" data-reference-number="7">7</a></sup>, meaning it has a very high chance of being included in the upgrade. If that happens, solc’s legacy pipeline will finally be able to get rid of the “stack too deep” error for good without incurring any loss of expressiveness in the language. And that’s not all: the alternative IR-pipeline, vyper, and any other compilers will also benefit from these new capabilities.</p><div data-type="subscribeButton" class="center-contents"><a class="email-subscribe-button" href="https://paragraph.com/@cethology/subscribe">Subscribe</a></div><h1 id="h-resources" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Resources</h1><p>Check <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-8024">the EIP</a> and <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum-magicians.org/t/eip-8024-backward-compatible-swapn-dupn-exchange/25486">the Ethereum Magicians discussion</a> for more details about the <code>EXCHANGE</code> instruction and how it encodes the input in a single byte.</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/frangio">The EIP author</a> has written some great explainers about adjacent topics that are completely worth reading:</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://frang.io/blog/codegen-for-stack-machines/">Compiling for the EVM: Codegen for Stack Machines</a>, about why other stack-based virtual machines (JVM, Wasm, CLR) don’t have the “stack too deep” error.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://frang.io/blog/spilling-in-evm/">Spilling in the EVM</a>, which continues the previous post, discussing the technique of moving values from stack to memory.</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum-magicians.org/t/evm-immediates/25605">EVM Immediates</a>, an Ethereum magicians post that explores the solution space for the problem of adding new instructions with immediates.</p></li></ul><ol class="footnotes"><li id="fn:1" data-id="e3962610-8728-44bd-908a-cebef6130c07"><p>To elaborate: you can still have code that won't compile if, for example, you have a lot of local variables <em>and</em> they are used in a recursive function. Admittedly this is not an everyday scenario, but it shows that the solution is not perfect.</p></li><li id="fn:2" data-id="632c9894-d55e-4dea-9a86-aebcf7a67cc6"><p>There are other areas, like transient storage or the return data region, but those don't really matter for this discussion.</p></li><li id="fn:3" data-id="e4fe4e46-7de9-4c88-bc34-38d78b000344"><p>Technically, the <code>DUP*</code> instructions access the top 16 elements while the <code>SWAP*</code> instructions use the top 17 items, because <code>SWAPX</code> swaps the 1st and X+1th elements (otherwise <code>SWAP1</code> would be a no-op).</p></li><li id="fn:4" data-id="3f689144-6274-46e0-b461-636903c0f2d9"><p>There's also <code>JUMPI</code>, which also jumps to some destination but only if some condition is true.</p></li><li id="fn:5" data-id="2abfedc7-857a-4360-b31c-fef7ab609487"><p>Bytes that don't correspond to opcodes of existing instructions can show up in the code, but executing them results in an exceptional halt and the transaction reverts consuming all remaining gas.</p></li><li id="fn:6" data-id="964676a9-d5ee-4fc6-9a7a-f2aa1fa078ed"><p>This is useful for compilers for reasons that go over my head.</p></li><li id="fn:7" data-id="1f88fb55-1c66-4e7d-8c85-9ffb415da88e"><p>Devnets are temporary, development-only testnets used by Ethereum core developers to test EIPs before they reach public testnets.</p></li></ol>]]></content:encoded>
            <author>cethology@newsletter.paragraph.com (Franco Victorio)</author>
            <category>ethereum</category>
            <category>solidity</category>
            <category>evm</category>
            <category>glamsterdam</category>
            <enclosure url="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/286540b1b9a89896340b63dc6c514a2d43cd1f74c0018dbdf678a6e81657cf24.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Understanding Block-Level Access Lists]]></title>
            <link>https://paragraph.com/@cethology/understanding-block-level-access-lists</link>
            <guid>lPCt2Hia1fSOm5Ah7Voe</guid>
            <pubDate>Wed, 04 Mar 2026 16:07:17 GMT</pubDate>
            <description><![CDATA[EIP-7928 (Block-Level Access Lists) is the headliner of the upcoming Glamsterdam upgrade, expected to activate mid-year. The EIP website summarizes it as a feature that unlocks “parallel transaction execution on Ethereum”. In this article we’ll see what that means, how the EIP works, and why it’s designed the way it is.]]></description>
            <content:encoded><![CDATA[<p>EIP-7928 (Block-Level Access Lists) is the headliner<sup id="fnref:1"><a class="footnote-ref" data-id="0d178b53-ff09-44a6-963b-8dfd81673493" href="#fn:1" data-reference-number="1">1</a></sup> of the upcoming Glamsterdam upgrade, expected to activate mid-year. The <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://blockaccesslist.xyz/"><u>EIP website</u></a> summarizes it as a feature that unlocks “parallel transaction execution on Ethereum”. In this article we’ll see what that means, how the EIP works, and why it’s designed the way it is.</p><p>This will be a roundabout explanation: we’ll intentionally go through a false start to better understand the actual solution. If you only want a quick overview, I recommend watching <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://x.com/uttam_singhk/status/1929873903435817321?s=20"><u>this short video</u></a> instead.</p><h1 id="h-block-proposal-and-validation" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Block proposal and validation</h1><p>In Ethereum, each new block is proposed by a randomly chosen validator and propagated to the rest of the network, where other nodes receive it and check that it’s valid. The most time-consuming part of this validation is re-executing all the transactions in the block to verify that the resulting state matches the expected one.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/d0e038397e77fadc6a1c8d1c9943268dbe9815c6c68864ad436b5b3084b47ba7.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAARCAIAAAAzPjmrAAAACXBIWXMAAAsTAAALEwEAmpwYAAACLUlEQVR4nK2TP2zaQBTGPTEz3sTE5q1M3ujGxuapYrypYrTUoZ4qtxOpIuR0Ozp5wkenXjqgiCGN3AaFgyHlyAJnomLZHYiuS9BV5SIaQvjX9tMbLL3n9/vu3Tvt81nwrc/WRbfbu+z3hyM+HI02lK2Liw7VhqOR3CaE0PtaTe6vSRRtAjDGMMaccwBANpuVUgoh/h5wO7t9kCaEZLNZhBAAIJVK/StAadFCCHF09E6dIJ/PQwgppYyxRXYDbJH9A5hEEf7YmN5MVQXnvO773W5PfedyuVKpRCltND7scghKaRzHS4CzdvDseem8fQ4hrFarx8efVIWc22FzqVup+76U0nVd27ZXjcdz6bp+cPB2CXBy2tKfPmk2m+l0ulAoqO63s9lqC8uyEEK6rqdSKSF+cs7DcMznarVahmEghDRNM01TShmG4zvA9ffrF29eMsYKhQKEcMNqmaYJIQQAaJqmpjEYXDHGLvt9QgiEkBCSTqfL5fIS4OS09erw9ddOW0oZx3Ed48WIHp2vZVmriyuE4JwzxjKZjOM4P5IkjuM7wPRm2qbtSTRRpb8Zvq9+FkJQSu8zhBDFYtEwDM/zHvLn91Qulx3H4ZwnSbL2oV10OgjVOOee5wEACCH/5x3cl+d5xWLRtm1N04Ig2L3vwscWQBAElUpFbQXnfC/vOwGUEYyxruuU0n3nsx2ghDGGELquu26vtgCC4MtgcLUhwnCcJMkkijaXPRrdbu8XtZCS22OIsrYAAAAASUVORK5CYII=" nextheight="974" nextwidth="1876" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">A block is proposed, propagated, and validated.</figcaption></figure><p>Transactions within a block have an order determined by the block builder. This order has to be respected when a block is verified; if it’s not, the resulting state could be different. For example, imagine a block with only two transactions, both calling the same contract:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/a5e21393ed0adc3871059f4411ddc472e0edd4bd44f4601769a5936923e4d38e.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAOCAIAAADBvonlAAAACXBIWXMAAAsTAAALEwEAmpwYAAADhUlEQVR4nGVUTWwbRRQeKoTEBRGBSZFDD8jEahsTWipoBEiY/mQjBTdO3QApLj9BkKQiOBYiKT8V14LQliLVVQVaDrTmtBfocmnHcPEeUFZRFY8FnbwVqnfWSN4fS7E2JwbZgwYXnkarndX37fvefO8N2qjVGo4jFqV0o0YIqYstgN1wHM/3AW57vk8pbXme6zYppRJPKbWsdYEUz5bnAdze3LRbnrdRq6GG4/C+KBaLi4uLctsO2/mT2Znjh5cLbwdB6Pu+6zajKJIAAFAUpVwuc87F92vXfshNH1aOPn3h/OdB0EYA9s+VCgAIQjqdjsVinHPTNAkhGONk4v7CaeXxPQ9RSkUOxpiu677vc85930cIqarKOdd13fPDXC7z6ktjC2+9kJ/NEPJ7twLLshhjIoGiKPF4nHNe7YWqqofSu5975pGpzEFC6ludTsvzWp5nGEbUC845QkjTtCiKMMaWtT5+9PnZmbGZ3IH5d2bXrJtIaJdotReyXsbYiePK6MjQTG4CwBZHJLRHUeT7fhRFiqJUq1VJ+b585cD+xOjIro/OvO+wPxEh9XbYLVyQLcvCGIutOI1K5RfD+OnXtTXGmOs2XbfJGJN4xhjGmBAiKQD29Rv4+g0MYAOAMPkv6ZuiKKlUqr+Ce+9C9yAU37mDMRYEYcvzgiCUeABIJpOiaFGZpn0dH0RDgyh/cqpbQcNxyuUyxhVpMkKIc24YPxqGUSpdmsoc/PbyyqH0bkLIVqfT0wiapkkRCCHReLqum6Y5Oal8c2n10w9n33x9en39ZjdBO/xX0dzcXDKZlHJM01SO7Ns5gJ58YggAgiAUbSpt4JzHYrFSqSQpZ8+e2T86+NijO5bfe6NWq3dNFgThmGEY0jERKyvFFyfHC4XTwl7xlCEolmXJfABwInds+tjE1atXWp53x6BF/wvXbT47NjKcGEjt3SU86B+0/4DbYSgGbTgRG04MzM+/1vWAUlqtVns2YAmVKTdqtVP58YtfLmUzT92it8SgAYBhGLqu9wuXLx9/slq6sPzd5Q+W3s13j6jhOIZhqKqqadpWp9P/939ugiP77rsbpfY8CGCLLgKwNU377Nw5jHE/WHDPq1+M7n3g4QFUWDrVnWRC6j3R21G0TTdBdLpctv3H6mrxlZczCwtz9fpvYowbjtOTst29+O6kBEFommY2OzGdVUoXv6Kb8DeIJf2wV3aUMQAAAABJRU5ErkJggg==" nextheight="813" nextwidth="1862" class="image-node embed"><figcaption htmlattributes="[object Object]" class=""><strong>Example 1</strong>: Two transactions that result in a different final state depending on their execution order.</figcaption></figure><p>Here we need to execute transactions in order to get the expected state. But this is not always the case. Take this other example:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/a2c907e9ecf1d45776d05bc9ba82ee1c2eb2df3dc95fc472efa8ef0e18d9342f.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAOCAIAAADBvonlAAAACXBIWXMAAAsTAAALEwEAmpwYAAADfElEQVR4nGVTTWwbRRSegzlwAqlF5QKHgOTSKhUIt1Sg5g+1dQuJXZy1kePEbqyi1q1KiiCqDyGoCDAHx0gEuSd8QNWeiIVoprcNB9jcVlEVz7ZoM6uq3llXeGe3Upy9Mch9YmS1T6PVzOx78733fe+h7RZpOw4sy7K2W4QQE46U2m3H8Th32CPfDyzL6npe1/Msy5L+Dx4+JISAJ3y7nufxwOOB7wfbrRZy3Y4YsFKplMvl5JFznp4+m/jwxOIXJd8POOeu2wnDUDq0HSeZSKiqKoSA+/X135OTY8nJsdXVmsc5otTWNI1SCgGxWCwSiQghdF3HGP+xsRF9/cV87r133xmi1Pb9oOt5jDGMMedcCMEYQwhVKhUhhKqqhJjXFkqTZw6nz72VzyV3dux+Ba1WizH2LICmabVabb5w+sbybEYZI8Tc7fW6nsc5BwBIGSGkqirnXNO0P//SZ7KpH1eufv9tsVy+3GqZT1NUe2KyXtftFM8rc9n47EwSnh6kCDbFYtEwDHlsNn9VUu/PZeM/rVY7nX/6FIX/G+fcMAxN02APtrm5ifEdw9jinHc9z3U7cC99dF0nhMBxb6/HGLu9jpvN30Bz1HYcCSCEUBQlGo1COmEYUmq/vB+9cfCFA/v6nr4fQAWyCEppNBoFDYDnm/XVoVefO3JoX3r6jMMe9cNUVdU0DQLi8ThCSAiBMVZVtbqycnJi+Oebi68NPW8YW5AmpbRer0tOEELAaqPRaDabx48f/fqr/FI5k06f3tq629dAygVtGovFoEGFEJq2oXx0YqmcGR85aFnWbq8HFcBfiIpEIo1GQ4YsLFxeuDL1ZfnjYiFFyN+IUirZDMMQY6zruqQoDMPl5aWZbKpcXpQiS3kgZG1tDUQGAEqpMp2YyaZu3frFdTt9kWUujDH57pObfym1J0bfnjx7dHz0TUqpBJBdBBhge3s9IcQdfHti9EgmNfLp1Qv9OaDU1nUdZBhUG8wwjJMTw9/dKByLvSLnoO04GGOZ+FNdW61WPr92bqmcOZ9P9Oeg7TgwUI1GY7fXG3xdCEGIOfXBsSufnBo+9BIhBAAYY/V6vVKpyNYYBPihVi3mx+fnRuYLyXv3LGSa9yHrx8Fja4e6bmdw2faD69c/u3RxtlDIEWLCKLQdB1Jx3Q6l9qC/7we6rmeUqUsXZyuVb0zz/n8KcgfsVDzM3QAAAABJRU5ErkJggg==" nextheight="814" nextwidth="1863" class="image-node embed"><figcaption htmlattributes="[object Object]" class=""><strong>Example 2</strong>: Two transactions that result in the same final state regardless of their execution order.</figcaption></figure><p>In this case, the order in which we execute the transactions doesn’t matter because the resulting state is the same. Re-executing these transactions sequentially would be wasteful: we could run both of them at the same time without affecting the result.</p><p>These examples illustrate that sometimes transactions can be parallelized and sometimes not. Can we use this somehow to have a faster re-execution?</p><h1 id="h-dependency-graphs" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Dependency graphs</h1><p>We can describe Example 1 in the previous section by saying that tx 2 depends on tx 1. But what do we mean exactly by “depends”? For now let’s use this definition:</p><blockquote><p>Given two transactions A and B, we say that B depends on A if:</p><ul><li><p>A precedes B in the block.</p></li><li><p>A writes some state that B reads.</p></li></ul></blockquote><p>In Example 1, tx 2 reads a storage slot (<code>x</code>) that was previously written by tx 1, causing a dependency. Keep in mind that "state" can mean a contract's storage, as in that example, but it can also mean other parts of the world state, like the balance of an account or the code at some address.</p><p>Now let’s ask, what would happen if we could know in advance all the dependencies in a block? Ignore for now how we would figure that out; just assume we do. If we had that information, we could use it to have some degree of parallelization during execution. For example, given a block with these transactions:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/40b7fab9f96fa54ebb9f0e061093f843c6f97202b6d4b80201320ef821776d04.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAALCAIAAACRcxhWAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB70lEQVR4nJWSQW4bIRSGWfkK7GfbiHTPBTgBB2g4gE1HXtqmzgGQoq4yCerWo5DuY5Ou45YjoKgHwJGaXRO/yPMsOnWqqv3FYubpe+8HfshwOJpMZ5PpbDgc1fV4OvtQKpPprK7HdT2ez0/retyvIzyfn+JHgUej9wcwaS4uAWCzeQCA1Al+13b7nFLKOR/Uc86vYRxS4EXbEuc+4Y9SqurEOS+dMUbGGNattWWK1rqqKsbYaxh5YwwAtFdX+xNorTnnxYkxhnuklDrncF+U0hACABhjEDhoLDA2rlbLm+Vqb3B8/NY5RwihlOacOecxxhCCEEJrTQgRQnjvcRZjzHs/6OScw0OEEKSU1lqEnXNCiPXXb3sDzrlzzhijlEopMcawh3dO2Om9V0oVA2utUgqvpcApJSlljNEYc3LybmeAGTjnKKV4m5xzKSWemjGGQ1NKpBMAeO8ppTHGPiylJIRorQEghDAYDFJK19eff4XsnMN8ECpPRUpZVZUQwlqLwcZOnHNMHuORUuac8aVwzjGtXciLtgWAn09P8G8KIVRVhfeQUjLGUEq993+EdwZnZx8B4P7+e+5ps3koq18vD9xaK4R4c3Sktc45b7fP2NUnH388NheX5LxpFm37v+tuvb5Zrm5vv9yt13/BzpvmBb2Fr/d+vWTdAAAAAElFTkSuQmCC" nextheight="339" nextwidth="1026" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">A block with four transactions and only one dependency among them.</figcaption></figure><p>Then we could execute the first three transactions in parallel, and then<sup id="fnref:2"><a class="footnote-ref" data-id="ec68f29f-1471-4433-aaed-97af0d79caee" href="#fn:2" data-reference-number="2">2</a></sup> execute the last  transaction:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/d3c844cd1f255ed3f77da298fd9f3394f0b7cef3315da316042f1f9dd2761a2a.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAARCAIAAAAzPjmrAAAACXBIWXMAAAsTAAALEwEAmpwYAAACVElEQVR4nK1UMU8bMRT237iVlZn15tuKvBLP3j0Wma7+AaZLkdyNKAnK0Mh0QqcjTCc5bFf50gWiF+hSJIuursijxykhl6Xf9PT02Z/f954fuRiP7+7vZrPbqoWyLPO8qKqqrud1PT85+fTl7Gy5fGgIy+XDcDj6eHw8m93W9RyTDdmYr0i+GI/JZGLjBkIIdT2PMTrnGGNaayEE57whSCk5559PTymlRVFg0jnX6x0hWQgRY7zKc3J5+T3GCAB4byPgvY8xMsasfX2BlFIpFWO01jLGMAkAWZbhwbaYEEJrPZvdkuFwFGMk/5BlGQCEEH4/Pd1Mp5xz7z0hhHNujMEiKKUhhP39fUKI1ppzDgBFUQghnHOEkDRNtdZSyuqHf63AGKOUstbiW0II94vFn+dnxhgAeO8BwBiDhasV0BMAoJSiARh470MIWmul1JvAZg/QImMMpXQwGGCAyRACpRQfxDlHsRij1hrJGADA9fX0VSCs0Ba4XywwttYqpbTWeHuDLMsIIY3p75LzvNhaAQqEluoanHPtuXqX/CJw3u+jcW04567yvJ2pqmqNU1XVh8PDsiz9dpz3+zsq6AZjbM23NbxZhD1oOrFTAACklEmSCCE6bNzdg3cRQsiybDAYNGPWJYCrAn9ZkiR7e3s41B0C1lopJf47a63WGsW6BJIkIYQcHBwIIX49PnZXYFcCUso0Ta21OPj/0yLvPW4UbEaaptva8FbBGmNnk2+m017viDHW6x0557bRXrbpt8nk53y++Q+KougYcADA7Q8rbKMNh6O/Fiw2mrhtFZcAAAAASUVORK5CYII=" nextheight="559" nextwidth="1028" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Executing transactions in parallel using a dependency graph.</figcaption></figure><p>At this point you might be wondering how realistic this is for real blocks. Here's an actual dependency graph, taken from <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://dependency.pics/">dependency.pics</a>:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/0d479fd802efe67fca62f8f382cc081fa894a3fadb2a52f83629897ce9aa3b31.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAATCAIAAAB+9pigAAAACXBIWXMAAAsTAAALEwEAmpwYAAADNElEQVR4nLWU708ScRzHbzVmT3JZm7K2Spr9mqsn/Xgi1rPsX7CWFa6tVlddzZJaGp6GhIjACeRVlLEoG6EnUcZgCCMaLCqL0wf+2HKiD5Km1PeQH34bnEOCWuXyvXtw38997vP6ft6f7x0CIYzF4qkrKQhhbW1tTU0NhmFnURTDLgoE1Sh6DkVRsbiZjRytqsJxXCxuFgiqG3C8rr7+8JEjfH65VCpNVWMVhykhMEc0HXA5HQ673e10vPX5XljMLqfDZu0bpAMet8vjdrmcjnd+n+e12/rqpc/rCQbHB+mAz+sZGx3JrZYEuN/4rzW2iSSESEJIFKRCq1eShs7HZvXdJxqdUaMzEnceK0mDjLiv0OoVWn27rkuh1Wt0XUrSoNDqJQqdjLjfqnkoUehEEuJ6k7KxRavVPQEALABuytTI6g35G0uRFUVIUmuRAh6SqYJiTmEJkr+JU1iSvEFW5XG3IsgapIDHKSzJ427jcLewORzu1jzudgRB1m/bOzMbXrSIdZ+maRxvsNlsEMIDB/arVCqCIAwGQygUyuy6l6IAABaLOdcQto5c3ur1vmGXSUAsFguHv0EI33/40IDj5ufPIYQHD1YIr1693dHxoPPh5ORULBYHAHwHDMNEunt6ZmZnKYqai0YZJsIwEQAAw0S+A8DWkUpbnE7XT0NmyQMDA0KhsJeiIITlfP5FDFOplPfu3Q2FpjO3aTQaAQAmk+l3HTQ1Nrrd7sUO0g/qm2TreKWVgjNJ8sp1RZt3bi7dt5tfYe9fyGaTK4+fng59PXEKG/s8kQWYi0YhhMU79txoav0FwNJnJ8jOj0PJ0yYjSPTStdPnL5vM1i+hmcwqmg4dw0Q+Do2Ew8lzkqnE/DyE0OnxD4+O/8Iimqb9fj8bsdvtarVaKpUGg8GsKhRFpbv5o7IBXq83DSBJUi6X5wIsFkumY7lK/xSyAY5+Z3r6+keGC9ilYydOBgJ01vvt6g7W63/uQCi69ehpNxupE7ftLDvE21VmMPZm7bcavTIRnFoKID2Dv7d4iYBEPJGYT2Ramal/wucC3i5jB23t5LMe87IAEqkPxGLt/zQ0nF7+T8Dy6QehBASx8vcYsgAAAABJRU5ErkJggg==" nextheight="700" nextwidth="1200" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Dependency graph for block 23710031 (<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://dependency.pics/detail.html?block=23710031">source</a>).</figcaption></figure><p>In this case, the whole block could be re-executed in just four batches, which seems pretty good for a block with 161 transactions. But we might not always be that lucky:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/25922c2d490a6a82520bebf93e3b7f6a66630a28ba20743f504cc920128af81a.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAATCAIAAAB+9pigAAAACXBIWXMAAAsTAAALEwEAmpwYAAACRElEQVR4nLWUz2sTQRTH56h4ED14kaCYimi1VESsXrQUhFL/BU9SqLRecikIguIPtujaQ3fbSfM704ZuQkxoghIKCtIUjYqJSdZNbLC1bKoHe7Mm3Ume7GyyFHrxkP0yMPCdmfeZee8xCJgobVAmAPB6vS6XC2M8NSVgjHn+uSAIgihKkiSIIsaY4yZCoZAkhXmeJ2QuEAhyHDc6OhYkBAC0Hc0IRWkDABDskapuyHL+q1wol+RvZeX9u5V8Plcs5KrqRrkkK4pcLOTXvlcqq6ViIVdZLW1t/a5uqopS/PWzujcaarJp7cdmIrW89OaDyx8TPVH3fNJNEi4ST6ZW0hk59jLtJolZf3zWHw/HX0eTb2e8MTdJspEg0itfKDkXWRI90WlPBHujPrI4Kc5n82UdoGkaADwVfKilfd19Q8fO9R8/e83eO9A20Ymeflv31dMXBxE6jBA6c2mo68J1e+/Aka4+tm5DB+w9V26ggycROooQ2m87TxYWdYCR94VYCh06dXnw5qcvilkSAMhkizwm6Y8FZupOZV1dzmQBwHg6ANTqO2bG2cGGpmnmaguAA5GR8Sdsd333gFYLUNPRmLZrNdOhlBrmn+2/mqZX2Ihj5KYFcJIXd+49M66zu0SU0jq7oOk020/5T7UAgk8aGX+sNxnDdlDIAEzrgEcWAgR3eNjx0EKAMxgdu8tZCMDtLrIsRT5p2PHAQsBMIGxtipx6DSYsbdPwbatrcMtx3xJAk02VdfVzUf9djT+rk4DOhtsL+Adz92OqyfSlNgAAAABJRU5ErkJggg==" nextheight="700" nextwidth="1200" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Dependency graph for block 23710002 (<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://dependency.pics/detail.html?block=23710002">source</a>).</figcaption></figure><p>Here we have a long dependency chain, significantly reducing the speedup we could get from parallelization.</p><p>In summary, if we (somehow) had the dependency graph of a set of transactions, we could re-execute them with some degree of parallelization, but how much benefit we'd obtain from that would depend on the structure of the graph. It's easy to imagine a pathological case where each transaction depends on the previous one, making sequential re-execution inevitable.</p><h1 id="h-building-dependency-graphs" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Building dependency graphs</h1><p>So far we’ve assumed that we have a dependency graph we can work with, but where does that dependency graph come from? How can we know which transactions depend on which?</p><p>We said that there is a dependency between two transactions if one of them writes some state that the other one has to read later. But since the EVM is Turing complete, there’s no way to know this in advance without actually executing them. Maybe we could take a conservative approach and say something like “if two transactions call the same contract, they have a dependency”, but it’s easy to show that this doesn’t work. In Example 2 we saw two transactions that call the same contract but can be executed independently:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/6b8ad0edf051f6f94b22867a2af634dbb232cb6d228cf011cb029df578c0edd2.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAAACXBIWXMAAAsTAAALEwEAmpwYAAACWklEQVR4nLWVTWgTQRTH55Zj8BDLnLRKsqVlIaAotVYCUlYK9SOGLIIhKniphzToWgmeeqnQXHSDRnvIoaQKuZiL2whdPeQ2B3cvmR7seLCTYrYK2Uv35AgZWEOTdrOtPt5lZnbm995/5r0FXwzj+9ZWr3/d3PzRalHa7Ls6oBuGCRp4g+1jlFLLstgRrIE3+gAcx2GMVSqVYDAYCoUopf8FACFUVVWW5Xw+707+G4CLAQCIonjo0z0AlFLQMadjXChKKSHE5e0B98ZxkESapgEAUqmUaZp8iBAqFouKoiCE3C91/TP3Wu3jzs7PQTPgDJ4BY8y2bcYYIUTXdYwxz4kxdjN+NRI+nr49fSt5+eIFQYgMd3b/Hkgiy7IEYUSSJM7jJzqOgxDSNA1jzBiLhOHDzMyjufjs/SsrpVzs0ujq6tturQ4C8MCdLlldjG3b1er7dPrumWj4cfbGfDYxn02sLD9NJmLPX6g+AG7UrM/Sbsv6NTZ6Yn2tsL72OjN7rd1GM9PnVbXgA1Cv1zVN24e9yxgbHxc/1V5mHlwfOgbabZRMxHwAEEKBQABCWCqVuvXhViy+SqXuTE5ElwtzpTdPFhfuNYx34hj8oNW8AXw5l8tJklQul+PxePcejLF7yUtLz06fGjp3VpiciA6fDE1NxfZI6vFMIYT8mfIhIaTcMV50g5S3d6sAAPBgdV1HCFWrVUVR3ItxeswHgBAiCCMAAEIILwvPeH1kwEvXrWSnq/n4anwez1SWZUmSjvLP8S60QzfqvwDDMLeb25Q2e52Qb33nB3fDMP8AnbrkMDhQ/+YAAAAASUVORK5CYII=" nextheight="627" nextwidth="942" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Two transactions call the same contract but can be executed independently.</figcaption></figure><p>Worse still, we could have two transactions that call different contracts but still have a dependency between them:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/3473aa028c17ae4647d4cd74fcaaeabebfce7dee146e96bf89639438d819f3b6.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAD80lEQVR4nLVVbUhbVxg+MPyxSmjB0mKcq23iEj+6rQY/MKatAZ2yMNY5dV1J8AOcg/VHOob9kw7bLlEkLSyW0a1gYcHVsI8QOklYksFZoHcpu7PJdUtulnhjTKJ1F5p7B/UO1jPiyWyarE5/7OFwOZfznve57/N+XBAIUivJJMuyDzKZB5nMSjJZsGJLS/mvDMMU2zxtBYIUCAQphJDVagUAvDs6ihASBAE9HX89eoR2jFCYzhFUVVVBCEUiUb53vJ+cMMqkB9uUdcrWWpn0gFRaKQh/7oIgFA4jhEwmU0lJiV6vzz/e2HiIEFI0yD3zHy/HXDRlz2T8mm6F1Tr7n4EWEjAMAwAwGAz5NzFBS1O948vJXxa+9nlvxOj5M2+pb83NFRMIedjYeIhPHxPo9XoAgFqtZlmWJEliExDCc+c+kMue151WqZRHamT7hnXtQ7qu69c/ZVl2y92OIuB5HgBgNpsRQql/wDBMPJ5oaqxzOq7e89sIeDOV+H5A22W1zv4WiUAIbTabxWLBHOPjF9pPNr3xemfvqVdam1+cnDBuEoRzSbbb7QCAvr6+YolekFY4vpnkuLvLMRfH3W1tkeAcsCw7vwme5xFCleJnv/riostxxem46nJM1crF8XhiJZkEoTCNrSGEqVSqQFaUzf9H9XWHVMqjKuVRxTGJXH6E5//YOk2lUmazeXr6mlSyf1jXrj4ha258znLl7LGXD9+547+/vp4j2F7Q9d/ZeDyRTKXj8QT+3vwvoGkaQh8AwP/DjM97wz0/HQt/WykufYJgGwiF1VLYBByX+dw6u3cPWI65zo5qNN0vcRxZLT3wE7mACbJJJggCV1Gxa5Ikyw/uqRSXAgD2lgKJpJym6XyJbLa5iYmpasn+aMgRDd+Ohm9nMkStvDxI/ZrNAU5yTU2N3W5Xq9VbrSAIAlZjYEB7/v2eVMK78OPs/VXY85rCaLyMC4/aBDbr7DzR1dGgPd3R/+bJ40qZqq05V0X5ESgUiuIIentPmS4Nx2gnAW8ux757u19pNF4WBMHtdlsslpmZma38QejzeLwejxdCX2GjCYIAABCJRFgTgiDwc2zsvEbT/d7oq6rWw5JDz9TJ97U0VoyPX6CoRRyB2+3Gvoob+wkCg8FQVlYGAMAXGIbBjUaSP/f39Xx2beye/5bX+Qm96BjSqbFExSWXPy3w0MUEuSoCAAwODubfxJuhQe2U6R2OI5eiTo4jtWeOX7r44a6HXTbdAIyMjBQTEAQhk4q7OhrblPUd7Q2y6gqKytbFTvA4Ap7n8fz5V7t0ejUSiUYi0cXFUDq9tkPvhRJt08nFf6FdEASC1NrqWjq9+n+sQJD6Gxq7TmIfOqgoAAAAAElFTkSuQmCC" nextheight="627" nextwidth="942" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Two transactions call different contracts but can't be executed independently.</figcaption></figure><p>We can’t escape the fact that we need to execute the transactions to build the dependency graph. But we want the dependency graph to speed up the execution of a block’s transactions…</p><p>This seems like an unsolvable problem, but let’s remember how block production works: a single validator proposes a block and the rest of the nodes in the network verify it. This means that the block proposer<sup id="fnref:3"><a class="footnote-ref" data-id="5a846bb1-a372-40e7-a0de-b75614e69276" href="#fn:3" data-reference-number="3">3</a></sup> could execute the transactions sequentially, build the dependency graph in the process, and propagate the graph along with the block. The other validators then can use this data to parallelize re-execution.</p><p>To build the dependency graph, we just need to run the transactions sequentially and keep track of which state is read or written by which transactions.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/a7dc589ca3bf1abd08b3a9afd173d342c51d0fcdf1fc13a936bee2f4f06a2cdf.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAANCAIAAABHKvtLAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZ0lEQVR4nJWTy23DMAyGtUMDeIsskD0ygSbwBt5AGxC9NgBT5Bi3TM82IOVsAXTOMcAFYsCFozxU5WD3gy6Saf18/FL7smy8D6uq68Z7nqK5x0+uqq6Vc8fhjog4d7R/YeZ4S0TMPMyjbU+jgEQAvIdLA9ZarXWcPhEBwEyBc9epj80m5C4il74POcYpA0C8RcSiKIhorkCWZerO22LxGsTMIhJvjTF5niMiRYTMYkRkFDgcfvAKEW23n5MCAWNMXCgArFarpNDdbnfpe1XV9e2na6OGF4houVwmn4gorgARQyfjE2YeK0hc9CpgrwxTaK2LokgOR4Hy6zvYI3i0bU+xqUTEWitTMDMiJoZs25Nzx2cFj9C4Umvter2mKQAgz/Nkzrch78vy4aIQkWWZMSYoMbPWWimFiMkDTEjM/Ryyc8eQAgCEhgBAHP0Y4L8ID358yVVdX/o+rMb7c9dNdnwm565rvP8FldQSp3fHyqEAAAAASUVORK5CYII=" nextheight="919" nextwidth="2249" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Two transactions use the same piece of state.</figcaption></figure><p>In this example, tx <code>i</code> writes to <code>state[x]</code>, which is later read by tx <code>j</code>, so we would add a dependency from <code>j</code> to <code>i</code>. If we did this for all transactions we'd get a list of dependencies for each one, representing the dependency graph of the block.</p><p>This approach seems to work. But we don't get full parallelization and the worst cases are still sequential. Can we do better?</p><h1 id="h-block-level-access-lists" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Block-Level Access Lists</h1><p>Let's change our strategy and ask instead: what would we need to be able to run all transactions in parallel, no matter what?</p><p>Let's look at the previous example again. Transaction <code>j</code> has a dependency because it reads <code>state[x]</code>, which was modified by transaction <code>i</code>. But if <code>j</code> knew in advance what the value of <code>state[x]</code> would be when it needs it, then there's no reason why it couldn't be executed independently. And we are already keeping track of state changes to build the dependency graph. Why not propagate <em>those</em> instead?</p><p>In other words, for each transaction we can keep track of the state they write, and the final value of that state. For the example above, the resulting state diff would be:</p><pre data-type="codeBlock" text="{
  [i]: {
    [x]: 200
  }
}"><code>{
  <span class="hljs-selector-attr">[i]</span>: {
    <span class="hljs-selector-attr">[x]</span>: <span class="hljs-number">200</span>
  }
}</code></pre><p>With this information, we can execute all transactions in parallel using a simple algorithm for reading from the state:</p><pre data-type="codeBlock" text="def read(j, x):
    last = find_last_writer(j, x)  # last transaction before j that modified x
    if last:
        return state_diff[last][x]
    else:
        return state[x]"><code>def <span class="hljs-keyword">read</span>(j, <span class="hljs-keyword">x</span>):
    <span class="hljs-keyword">last</span> = find_last_writer(j, <span class="hljs-keyword">x</span>)  <span class="hljs-comment"># last transaction before j that modified x</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">last</span>:
        <span class="hljs-keyword">return</span> state_diff[<span class="hljs-keyword">last</span>][<span class="hljs-keyword">x</span>]
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">state</span>[<span class="hljs-keyword">x</span>]</code></pre><p>And that's it. That's what Block-Level Access Lists (BALs) do. Almost.</p><p>In reality, BALs include both the state changes and the list of reads. The previous example would be something like:</p><pre data-type="codeBlock" text="{
    [i]: {
        writes: {
            [x]: 200
        },
        reads: []
    },
    [j]: {
        writes: {},
        reads: [x]
    }
}"><code>{
    <span class="hljs-selector-attr">[i]</span>: {
        writes: {
            <span class="hljs-selector-attr">[x]</span>: <span class="hljs-number">200</span>
        },
        reads: []
    },
    <span class="hljs-selector-attr">[j]</span>: {
        writes: {},
        reads: [x]
    }
}</code></pre><p>The reason for this is I/O. When we say "read state" we are talking about disk access, a relatively slow operation. If we know all the writes and reads in advance, then we know all the state that will be needed by all transactions, allowing us to prefetch the necessary data in one go before the start of the execution. This means we can execute all transactions in parallel <em>and</em> we don't need to perform any disk I/O while we do it.<sup id="fnref:4"><a class="footnote-ref" data-id="6ff5e2a0-350c-49a9-a16c-51c87c79f694" href="#fn:4" data-reference-number="4">4</a></sup></p><p>The actual BAL format is way more complex than what I shared. It looks more like this:</p><pre data-type="codeBlock" text="BlockAccessList = List[AccountChanges]

AccountChanges = [
    Address,
    List[SlotChanges],
    List[StorageKey],
    List[BalanceChange],
    List[NonceChange],
    List[CodeChange] 
]

SlotChanges = [StorageKey, List[StorageChange]]

# etc"><code>BlockAccessList = List<span class="hljs-selector-attr">[AccountChanges]</span>

AccountChanges = <span class="hljs-selector-attr">[    Address,    List[SlotChanges]</span>,
    List<span class="hljs-selector-attr">[StorageKey]</span>,
    List<span class="hljs-selector-attr">[BalanceChange]</span>,
    List<span class="hljs-selector-attr">[NonceChange]</span>,
    List<span class="hljs-selector-attr">[CodeChange]</span> 
]

SlotChanges = <span class="hljs-selector-attr">[StorageKey, List[StorageChange]</span>]

# etc</code></pre><p>This is because, as we mentioned before, "state" can mean several things: storage slots, balances, nonces, code, etc. You can see the full definition of a BAL in <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://eips.ethereum.org/EIPS/eip-7928"><u>the EIP</u></a>.</p><p>While the speedup we get from parallel execution + I/O prefetch seems great, there are downsides. We now have to share both the block and its BAL. This increases propagation times, which could, in principle, have a negative effect that is more significant than the performance wins we get from the BAL. The argument is that this trade-off is worth it. See for example <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://notes.ethereum.org/@misilva/r1CyZf_Vlx">Worse-case analysis for BALs</a>.<sup id="fnref:5"><a class="footnote-ref" data-id="7b479010-ab66-4a9a-9646-86d871fbcc97" href="#fn:5" data-reference-number="5">5</a></sup></p><h1 id="h-loose-ends" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Loose ends</h1><p>There are several other aspects of Block-Level Access Lists that are important (or at least interesting) but that weren’t essential to the explanation above.</p><h2 id="h-user-facing-changes" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">User-facing changes</h2><p>Outside the inner workings of the protocol, BALs don’t change things too much. As far as I know, the two main changes are in the block header and in the JSON-RPC layer.</p><ul><li><p>The block header gets a new field, <code>blockAccessListHash</code>, with the hash of the BAL. This lets you check that the BAL of a given block is correct without having to re-execute the block, which can be useful in contexts like execution-less validation.</p></li><li><p>There is a new <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://ethereum.github.io/execution-apis/api/methods/eth_getBlockAccessList"><u>eth_getBlockAccessList</u></a> method that can be used to get the BAL of a given block. BALs are not available forever though: clients are only required to keep them around for 3553 epochs (~2 weeks), after which they can be pruned. BAL hashes, being part of the block header, are of course kept forever.</p></li></ul><h2 id="h-why-faster-re-execution" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Why faster re-execution?</h2><p>During our discussion we have taken for granted that faster re-execution is good, so good that it's even worth using a headliner for it. But why is that the case? To be honest, this isn’t mentioned in the EIP. The only explanation I’ve seen is in <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://ethereum-magicians.org/t/eip-7928-block-level-access-lists-the-case-for-glamsterdam/24343"><u>the headliner proposal</u></a>:</p><blockquote><p>The community has expressed a clear desire: Ethereum L1 must scale to meet the needs of users and developers. BALs unlock performance gains critical for <strong>higher throughput</strong> and/or shorter slot times. They also pave the way for <strong>zkEVM-based</strong> light nodes (executionless + stateless), full nodes (executionless + stateful) and partially stateless execution.</p></blockquote><p>Which seems fair enough.</p><h2 id="h-bals-vs-optional-access-lists" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">BALs vs Optional Access Lists</h2><p>The term “access list” already exists in Ethereum: transactions can include an optional access list<sup id="fnref:6"><a class="footnote-ref" data-id="8b560a7d-cc16-4185-bc5a-79ac368e1e4f" href="#fn:6" data-reference-number="6">6</a></sup>, which is a very different concept:</p><ul><li><p>They work at the transaction level, not at the whole block level.</p></li><li><p>They are only about accesses and don’t include state diffs.</p></li><li><p>They don’t have to be complete. A transaction could read some state and not include it in its access list.</p></li><li><p>They don’t even have to be <em>correct</em>. A block is rejected if its BAL is not exactly the one it should be. A transaction’s access list could include items that are not actually accessed.</p></li><li><p>And, as it says on the tin, they are optional. You can send transactions without access lists.</p></li></ul><p>It’s unclear to me how transaction-level access lists will be affected once BALs are added. I’ve seen some people implying they will no longer make sense long-term, but I don’t fully understand why.</p><h2 id="h-a-historical-note" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">A historical note</h2><p>I find it very interesting to look at the <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://notes.ethereum.org/@dankrad/SJawZpx1yx"><u>earliest proposal</u></a><sup id="fnref:7"><a class="footnote-ref" data-id="7d5f8806-819e-4566-b86d-3027d0cbde0e" href="#fn:7" data-reference-number="7">7</a></sup>:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/72819ace065bf9dd361c7aa7beacf38762d9cd6f060cea2ab9c401d0f0ada6b4.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAIAAADCaIt+AAAACXBIWXMAAAsTAAALEwEAmpwYAAAGXElEQVR4nLVWb0wTZxi/mMwPy9yyzGwsm3wYEz/ANmwGZtnJziLF0E76xxbFng7OXu82S7RQlGajMOkLsye2d6bcsOUS/mzSjWx8cODmElPqByHR1tg/c8iWSHUFHaRbpIC0Cz2s0rF/yfzlSfM8v957v3v7/J57C10aGQ2FfggEQ16vzx8I+K5d8/mupiIQDPl8V71eX4oJhX7493FpZBTyen2Jxwav17cs4B52Z2RkHNIfxgncYDDoa/Q4gVttVgDMdUfqjtYfBcAslUk1uOb2rdv84lgsNjc3zycpJk3AHwgsC7hcLugBnn56XVoCQVBm5gY+uXjx4qr3WhVLAoFgiF8wPTMdDt+KxWLRaDRZzoTDYT6PRqO/RG5HJiOx2Kw/EAgGgzcnJvwB//Ufr4/duOHxeMbGxngmMhm5OTERjUbTdzA6OpqdnY0giEAgEIlEJEkKhUK1Wm001svlChRFSZI8qDuIYRhJkrhWo6/Ry+VyqUxqNBpJktASBIZhpeJSLUFIZTKPx/NQwOe7mkgkxsfHWZY9+81ZirKMjo7+X032p3bg8Qy/+GIGTuAKpUIul/EasdVwb/be/cX7C/MLfLkYj/PJ/cX7qc/FeDxd4Lvvv4MgSCQSZWZuEAgEQ0Pn0joZe8Qq0ST4+/L577/9/udHWdHkxXh87MaNs9+cdbm+4DiOYRiH00FRlNVmpWnbcctxjuOSrgVnXH28O/+bixbjcZZl5XIZnIRcLt+r3ksQuEQixgm8vFyFoqhEIobht6urdWdcfd3d3QCAnp5uhmEMBoPD6fwHgUQiQVEWGIbzC/IRBKmu1r377k6xWIyiaLGo2GAwlIpL1Wq1XC7DsCoYhjEMY1l2YGDA5XIxDDM4OPgPAvdmZ93DbofTYW+3d3V3DQx83dnpZFnW5XI1mBrOuPocTofD6ejp6eY4jqJO9PZ+thiPL8wvJBJLTf717t3UxESjv6Ua9qiLPIUIIpGID+o+UKlUJEnI5bJScalUJlOqlFKZFMOqMAyrrKo0Go0lJSUoqhYKhVKZtFhUjGwTVlfrJBKJUqVcu/aJnJwckiTTBS57L7/88kubNm3K25yXvWkJeZvztmwpyMjIKCgo2Jj9albWK7m5OTAMCwSbN2a/iiAIDMPItm0lJSXFouL8gvzMzA3J58Ds7fbe3s/SBy0yGenq7nI4HSzL0rRtcHCQYRiWtTebmxsbTQCYT506ZTQaG0wmjuMAMOtr9BRFAWA+Wn+Upm0s22401h+3HH9gsHgyHtmBe9j93Pr1zebmPbvLlSqlUFiUep88wPLs/D34X3+VHriH3RAE4QQOw3BObm5WVlY4HP6rZbFHRimtjMVm+dFbmJ9Pt+n1UKiz03nG1TcwMPDtuXNWmw0As9FoBMDMMAwAwGqzMQzTbG6mKKqz02lvtzc2miyWT7q6uwAANG3lOC7tNb5C4IL7AqbRSBUKjZZAioTSXbuKd+woKt6OFBUhRUXinTv3V1ZKFYq3tsKH9Xq4sBAuLFQoFYI335Tu2oW+t3/P3gqkSMie7mizWtusJ+/cubtCYG5uvo/jSrZufQaCtCjqoG2nabqn49NTraDDZj3xcSPdAno7Ohy0jWpq+ryTswIz3QIcDGOnLK2mhg6btcNmpZqaTtN027Fj1tbW6ZnpFS4KXrlypKpqS15eyVa49J3Cyt27DxFaLYraKQuOqqt2q3BUvXP7dj1JVJSVVZSVHSK0OKoWI0hFWVmdTvdeuVJPEjiq1qIoplaTSuX8XCxdoB7D8t94fctruesg6CkIen7dky8kz8xn16xpqjtipyjw4UcOhmk1mdpPtJ2maSsArSZTq6nBTlF2iloqG5uYlhb7yZOHVOXR6ekVLgr6/XUfvK/DMKalZai/f/DLL4b6+4f6+7/q6hrq7782MhK8coWPkNebypfj8uXleMBcGxl56CJeIDIZoU621R6p+/b8eb7nqUNjMR5PxZ8ZnkxjVhw4KRf5fD4AzCRJ7t+3j6IsKKqu2LOnulqnOaAhSaK2toY/mQEwY1hVnaHWYDBgGKY/fLi2tmb/PjR5gRrDMM2BAwzDrCIwPTMzNTU5Pj4eDocjk5HxJKampiKTkXASPBMOh29OTPz80088MzU1xX/LlylyFYHHAX+qB48JS38d3R6PPxB4TOH2eP4AQxwrFh3pPqMAAAAASUVORK5CYII=" nextheight="825" nextwidth="877" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Screenshot from early BALs proposal</figcaption></figure><p>As you can see, the original idea only included reads and it was all about I/O prefetching. Writes were added later to enable parallel execution. In that sense, the way BALs developed is the opposite to how I explained them here. But since the “marketing” of the feature has focused a lot on parallel execution, I thought it made sense to start there. I also think it’s a better way to understand them.</p><p>This early iteration also explains why BALs have that name, which seems like a misnomer to me. It’s not 100% wrong, since a write is also an access, but it feels slightly off. In any case, it’s way too late to change it now.</p><hr><p>Thanks for reading! In the next article, we’ll explore <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-8024">EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE)</a>, another EIP that will (probably) be included in Glamsterdam, and the one that, fingers crossed, will kill Solidity’s infamous “stack too deep” error for good.</p><p>If you'd like to be notified when new articles are published, you can subscribe to the blog.</p><div data-type="subscribeButton" class="center-contents"><a class="email-subscribe-button" href="https://paragraph.com/@cethology/subscribe">Subscribe</a></div><ol class="footnotes"><li id="fn:1" data-id="0d178b53-ff09-44a6-963b-8dfd81673493"><p>Headliners are the flagship features included in a network upgrade. To learn more about how they are decided, check <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://ethereum-magicians.org/t/community-consensus-fork-headliners-acd-working-groups/24088"><u>this Ethereum Magicians post</u></a>.</p></li><li id="fn:2" data-id="ec68f29f-1471-4433-aaed-97af0d79caee"><p>We could also execute tx 4 immediately after tx 2 finishes. For simplicity we are using execution batches here.</p><br></li><li id="fn:3" data-id="5a846bb1-a372-40e7-a0de-b75614e69276"><p>I’m playing loose with the terminology here. The block builder and the block proposer could be different entities, as is in fact usually the case. I’m using them as synonyms here because it simplifies the explanation.</p></li><li id="fn:4" data-id="6ff5e2a0-350c-49a9-a16c-51c87c79f694"><p>I don’t know which of these benefits has the bigger impact. It doesn’t seem obvious to me that parallel execution results in a bigger speedup than I/O prefetch. Hopefully we’ll have actual numbers on this at some point.</p></li><li id="fn:5" data-id="7b479010-ab66-4a9a-9646-86d871fbcc97"><p>The first time I was learning about BALs I was surprised no one mentioned state bloat. Surely that was another downside? But BALs don’t become a permanent part of the blockchain state and, as explained in the <em>User-facing changes</em> section, they can be pruned after ~2 weeks.</p></li><li id="fn:6" data-id="8b560a7d-cc16-4185-bc5a-79ac368e1e4f"><p>Optional access lists were introduced in <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-2930">EIP-2930</a> mainly to allow “unbricking” contracts that could become unusable after certain gas repricings included in the same upgrade. The explanation is complex, but see <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/@fvictorio/gas-costs-after-berlin">this old (and outdated) article of mine</a> if you are interested.</p></li><li id="fn:7" data-id="7d5f8806-819e-4566-b86d-3027d0cbde0e"><p>Or at least the earliest linked document in <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://blockaccesslist.xyz/"><u>the EIP website</u></a>. Apparently, the idea had been toyed with before, for example in <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://ethresear.ch/t/block-access-list/9357"><u>this 2021 post</u></a>.</p></li></ol>]]></content:encoded>
            <author>cethology@newsletter.paragraph.com (Franco Victorio)</author>
            <category>ethereum</category>
            <category>glamsterdam</category>
            <enclosure url="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/62b235d5cb9721849074527be35fa1667c8c648ed6b70916a6605b8c9284f3ac.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Deterministic Deployments, Part 3: Other Approaches]]></title>
            <link>https://paragraph.com/@cethology/deterministic-deployments-part-3-other-approaches</link>
            <guid>85zBrbYnB9d6yZEgiZWS</guid>
            <pubDate>Thu, 26 Feb 2026 14:01:45 GMT</pubDate>
            <description><![CDATA[This is the third and final article in a series about deterministic deployments. In this entry we'll explore some additional approaches, including a couple of relatively recent and quite interesting ones.]]></description>
            <content:encoded><![CDATA[<p>This is the third and final article in a series about deterministic deployments. In the first two entries (<a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://paragraph.com/@cethology/deterministic-deployments-part-1-the-basics">1</a>, <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://paragraph.com/@cethology/deterministic-deployments-part-2-create2-based-approaches">2</a>) we covered a lot of ground: managed private keys, Nick's method, pre-signed transactions, CREATE2 factories, and CREATE3. In this entry we'll explore some additional approaches, including a couple of relatively recent and quite interesting ones.</p><h1 id="h-preinstalls" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Preinstalls</h1><p>All the methods we've discussed so far are available to anyone who wants to use them. For completeness, though, we should mention another way to get a contract at a given address: include it at the protocol level. That is, the contract exists at that address simply because the rules of the chain say it does. Contracts included this way are usually called <strong>preinstalls</strong>.<sup id="fnref:1"><a class="footnote-ref" data-id="9abdb6bf-e5cc-4c75-82ec-8188fd9cce84" href="#fn:1" data-reference-number="1">1</a></sup></p><p>This is exactly what OP Stack chains do: the four CREATE2 factories we discussed in <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://paragraph.com/@cethology/deterministic-deployments-part-2-create2-based-approaches">the previous article</a>, along with <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://specs.optimism.io/protocol/preinstalls.html">many other contracts</a>, are available without having been deployed through a transaction.</p><p>Preinstalls make a chain more "batteries included", but they can also serve as a useful last resort. Consider a contract that was deployed many times using Nick's method but that can't be deployed in some new chain because the gas limit of the transaction is too tight. In this case, the chain could decide to add the contract as a preinstall in a network upgrade. It's not a method you would choose first, but it's good to know the option exists.</p><h1 id="h-rip-7740" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">RIP-7740</h1><p>RIPs are Rollup Improvement Proposals, a mechanism for standardizing behavior across different rollups. <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7740.md">RIP-7740</a> simply proposes that all rollups preinstall a set of CREATE2 factories at their usual addresses, so that anyone can count on their availability. The specified set is precisely the four contracts from our previous article: Arachnid's Deterministic Deployment Proxy, create2deployer, Safe Singleton Factory, and CreateX.</p><h1 id="h-eip-7997" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">EIP-7997</h1><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-7997">EIP-7997</a> proposes adding a CREATE2 factory as a predeploy<sup id="fnref:2"><a class="footnote-ref" data-id="22380615-8271-4f02-924a-625f753f42f4" href="#fn:2" data-reference-number="2">2</a></sup> to the Ethereum L1 and, by extension, to any EVM-compatible chain that wants to stay in sync with the protocol. This would be a minimal factory with a behavior similar to that of Arachnid's Deterministic Deployment Proxy.<sup id="fnref:3"><a class="footnote-ref" data-id="68810dc2-4386-4e60-ac67-a4d4e9a8e523" href="#fn:3" data-reference-number="3">3</a></sup></p><p>One interesting property of this approach is that a predeploy doesn't have an ECDSA private key associated with it, making it quantum-resistant. The CREATE2 factories we discussed before don't offer this guarantee: if ECDSA is ever broken, a future attacker could derive the deployer key and potentially front-run any deployment on new chains with other code. Whether this is a realistic concern is debatable (it requires both ECDSA being broken <em>and</em> that new EVM chains are still being created) but it's a clean property to have regardless.</p><h1 id="h-erc-7955" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">ERC-7955</h1><p>As its name indicates, <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://eips.ethereum.org/EIPS/eip-7955">ERC-7955</a> is an ERC rather than an EIP <sup id="fnref:4"><a class="footnote-ref" data-id="dad1b0d3-7346-4512-b24d-0fc95d2bb653" href="#fn:4" data-reference-number="4">4</a></sup>, meaning it doesn't require any changes to the protocol. Instead, it proposes a standardized mechanism to perform a deterministic deployment, and uses it to bootstrap a CREATE2 factory.</p><p>The proposed mechanism assumes that <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eip7702.io/">EIP-7702</a> (Set EOA Account Code) is available in the target chain. We are not going to explain EIP-7702 here, but let's quickly recap two aspects that matter for the ERC:</p><ul><li><p>EIP-7702 lets an EOA "transform" itself into a contract. More precisely: any transaction sent to an EIP-7702-transformed EOA results in a delegate call to some specified address.</p></li><li><p>For an EOA to delegate its execution, it doesn't need to send a transaction itself. It only needs to sign an <em>authorization</em>, and that authorization needs to be included in an EIP-7702 transaction, which in turn can be sent from any address.</p></li></ul><p>With that in mind, here's how ERC-7955 works:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/49a9a31b61cb484acfb9844472e255150c0f684d16877a6f68c1335cee2b7575.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAAACXBIWXMAAAsTAAALEwEAmpwYAAADvElEQVR4nJ2UYUwbVRzAX2JC9sFoE4dpRq0GiYkhMzgw2IRl/TBnxS0wWbq0KecyAsPRQ6fs5MMoc7gOWe+FmcmqzB4dtYddyGV+YFeWTU4NJFdH5Aah4bp9GDd0btdVhw9IJM/AdbXpKDB+eR/u5V3e7//+7/9/QBQj8pMwPj4RiVyPRK6PSeNr/hydmgKKouAsqKqa+l6Yn8cYc1z/O7veeHvn6+btr1re2hYMBjDGCKFsO8Tj8awCnuf1er0kSdpU2wXS7V9/1SSNhqTRUD/7Oek8tHGBJEkAAJZlOY4jCIKiKFGMGF/Y0nehTf8cAABwoVMk+cHGBbIsAwAQQoqiiMsgNLffuu9s58elJXmlJXnfMa3U0SMbF0AIdTqdLMvpKTp9+lTTR1XDgm9o8BzdXu9qad64QBAEs9mcfs/Lx5pq+sTpcOyz76/8kKy/dfNmtq3/F9y5M7N6FBnM/oPuq/H7ajweT+C1SApUVVUUBT1GKj/4UQQ8P1C5Z4fDXu6wl1fb3x0Mh5cXF1cTIDTncrmsVmvGGs/zhYWFsiw/0s9ijNvaWs/QzugE9+tIby/T8niZpseXFGCMKyoqAAAQQrfb7XK5IIR1dXUEQQAA3G53cXGxyWSiaVoURYNBH+w58fJLT21+GrD+z9bVBxhjiqKMRiPP8zk5OQRBsCzL87wgCAAASZIKCgpMJtOZzs5weLCxseFbb/O213KLtuYGGFdNDaE1vOb4+SeBbDhQ7aiqduxtdNZqaV+qdIZhIIQIIQghs4wgCDzPm81mWZZFUdSmv42NnTx5nG6vl0b7IiM9Pi+Vn2985lkdSZIQwqajFADA56WGrniHh86T9bt9vm6McVJgsVgyTseyrF6vT79nrb3JhgNHyIPOw+/X1thEUSQIwmq1ut1uDw3LykpCwbairbmErexE60GPp2NJkF6mGdlcMbkIzaUGxrj12Bex2C2tBJwNtWyPa0susL5X2tLsSAqyNVqqElZhZDjiZ0KpUBrJusuXaGk0FJ3gjn1qO9d1djXBemC6g38l/k495oyve++e7eRh26GaSrvVIkkSQnNPJvjj97sjw5HoZGxg4GrAf9HT0RWdjIUv/5h4kOzqfxcXU2ONtyiD6GTsy87zO3dUbQKGfEPx87pXXswrKntz9yZgAGBz+S7b8RaPdivpLAluT0+vM/yZmbuhvh++8fZ6OrquXfvlEjcQuNAf8F/0M6HvWS7xILEwv7CC4MaNcWXdqKr6cPah9nj8ee+emsaK/9+env4PXX5kMyuzOi4AAAAASUVORK5CYII=" nextheight="1309" nextwidth="2031" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">ERC-7955</figcaption></figure><p>A well-known private key, corresponding to <code>ERC7955EOA</code> in the diagram, is used to sign an authorization that delegates to <code>SomeCreate2Factory</code>. Neither the address nor the code of this factory matter, as long as it can be used to deploy contracts with CREATE2.</p><p>Once the EOA is delegated, a transaction is sent to it (from any account) to deploy another factory: <code>ERC7955Factory</code>, the one we actually want to have at a deterministic address. The key insight is this: when the delegated EOA is called and it delegate-calls <code>SomeCreate2Factory</code>, the “sender” parameter used to compute the deployment address is <code>ERC7955EOA</code>, not the one from <code>SomeCreate2Factory</code> (as it would happen in a non-delegate call). That's why the identity of the factory doesn't matter: the resulting address is determined by the well-known EOA's address, the salt, and the init code, all of which are fixed in advance.</p><p>One way I like to think about this method is that we're executing the <code>CREATE2</code> opcode from an EOA, something that was impossible before EIP-7702. This combines the address stability of a managed key with <code>CREATE2</code>'s nonce-independence.</p><p>A big upside of ERC-7955 is that there's no need to keep a private key safe; in fact, the key is intentionally made public. Anyone who wants to deploy the factory in a new chain can use it to sign the required EIP-7702 authorization. The main downside is that this method requires EIP-7702, a relatively recent feature, to be available.</p><p>There's also a subtler downside. Since the private key is well-known, anyone could sign a different authorization with the same nonce and front-run<sup id="fnref:5"><a class="footnote-ref" data-id="353a5eeb-5a91-4ece-a9ce-6cde951b063b" href="#fn:5" data-reference-number="5">5</a></sup> the delegation, temporarily blocking the deployment. That said, this isn't a fatal flaw: a new authorization can always be signed with a higher nonce, and we can keep trying until one goes through. And, in the face of a stubborn attacker, we can always use a private mempool to guarantee the authorization is included.</p><h1 id="h-wrapping-up" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Wrapping up</h1><p>We've covered a broad range of techniques, from the simplest ones to the most recent:</p><ul><li><p><strong>Managed private keys</strong>, <strong>pre-signed transactions</strong>, and <strong>Nick's method</strong>, which are based on normal deployment transactions and have different trade-offs around key custody, replay protection, and permissionlessness.</p></li><li><p>CREATE2-based approaches like <strong>CREATE2 factories</strong> and <strong>CREATE3</strong>. Given the wide availability of CREATE2 factories, these should be the first option to consider for anyone wanting to deploy a contract in a deterministic way.</p></li><li><p>The methods discussed in this article: <strong>preinstall</strong>-based ideas or the clever <strong>ERC-7955</strong>.</p></li></ul><p>It's worth pointing out that both ERC-7955 and EIP-7997 were proposed less than a year ago. It wouldn't be surprising if new approaches keep emerging.</p><hr><p>Thanks for reading! If you'd like to be notified when new articles are published, you can subscribe to the blog.</p><div data-type="subscribeButton" class="center-contents"><a class="email-subscribe-button" href="https://paragraph.com/@cethology/subscribe">Subscribe</a></div><ol class="footnotes"><li id="fn:1" data-id="9abdb6bf-e5cc-4c75-82ec-8188fd9cce84"><p>For a detailed explanation on the difference between precompiles, preinstalls, predeploys, and system contracts, check my answer <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.stackexchange.com/a/172166/19897">here</a>.</p></li><li id="fn:2" data-id="22380615-8271-4f02-924a-625f753f42f4"><p>In this context, there isn't much difference between preinstall and predeploy. See the link in the previous footnote for more details.</p></li><li id="fn:3" data-id="68810dc2-4386-4e60-ac67-a4d4e9a8e523"><p>Both use the first 32 bytes of the calldata as the CREATE2 salt, and the rest as the init code. But they have (at least) two small differences:  1) The EIP-7997 factory returns 32 bytes, while Arachnid's returns 20, and 2) The EIP-7997 code forwards the revert reason if the contract creation fails, while Arachnid's doesn't.</p></li><li id="fn:4" data-id="dad1b0d3-7346-4512-b24d-0fc95d2bb653"><p>EIP stands for Ethereum Improvement Proposal, while ERC means Ethereum Request for Comments.</p><br></li><li id="fn:5" data-id="353a5eeb-5a91-4ece-a9ce-6cde951b063b"><p>Since the contents of the mempool are public, an attacker could see a transaction that includes an authorization signed by a well-known private key, use that key to sign a different authorization with the same nonce, and pay a higher gas price to incentivize the block builder to include it first. Once included, the original transaction becomes invalid, because it contains an authorization with a nonce that has already been used.</p><br></li></ol>]]></content:encoded>
            <author>cethology@newsletter.paragraph.com (Franco Victorio)</author>
            <enclosure url="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/372bcaabd276eff2cb1b59e3652fa59eff49c3da21f92f0a4b8b1800017fce73.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Deterministic Deployments, Part 2: CREATE2-based approaches]]></title>
            <link>https://paragraph.com/@cethology/deterministic-deployments-part-2-create2-based-approaches</link>
            <guid>stxSlqD10bqrVXS3UEtf</guid>
            <pubDate>Fri, 13 Feb 2026 08:24:25 GMT</pubDate>
            <description><![CDATA[This is the second article in a series about deterministic deployments, where we explore the question: how can we deploy a contract at the same address in multiple chains? In the first part we discussed three possible answers: deploying with the same private key and nonce, using Nick’s method, and pre-signing transactions. In this entry we’ll discuss two other approaches: CREATE2 factories and CREATE3.]]></description>
            <content:encoded><![CDATA[<p>This is the second article in a series about deterministic deployments, where we explore the question: how can we deploy a contract at the same address in multiple chains?</p><p>In <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://paragraph.com/@cethology/deterministic-deployments-part-1-the-basics">the first part</a> we discussed three possible answers: deploying with the same private key and nonce, using Nick’s method, and pre-signing transactions. In this entry we’ll discuss two other approaches: <code>CREATE2</code> factories and <code>CREATE3</code>.</p><h1 id="h-the-create2-opcode" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The CREATE2 opcode</h1><p>The two methods we’ll explain in this article are based on the <code>CREATE2</code> opcode, so let’s quickly review how it works.</p><p>As we saw in the previous entry, contracts can create other contracts using the <code>CREATE</code> and <code>CREATE2</code> opcodes. The <code>CREATE</code> opcode behaves in essentially the same way as deployment transactions:</p><ul><li><p>Some init code is executed to get the runtime code of the new contract.</p></li><li><p>The address of the created contract depends on the address and nonce of the sender. For deployment transactions, the sender is the EOA that signed the transaction. For <code>CREATE</code>, it’s the address of the contract executing the opcode.</p></li></ul><p>The <code>CREATE2</code> opcode is similar to <code>CREATE</code>, but the way the address is computed changes. Remember that, for <code>CREATE</code> and deployment transactions, the formula is:</p><pre data-type="codeBlock" text="address = keccak256(rlp([sender, nonce]))[12:]"><code><span class="hljs-selector-tag">address</span> = keccak256(rlp(<span class="hljs-selector-attr">[sender, nonce]</span>))<span class="hljs-selector-attr">[12:]</span></code></pre><p>For <code>CREATE2</code>, on the other hand, it’s:</p><pre data-type="codeBlock" text="address = keccak256(0xff ++ sender ++ salt ++ keccak256(initcode))[12:]"><code><span class="hljs-keyword">address</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-number">0xff</span> <span class="hljs-operator">+</span><span class="hljs-operator">+</span> sender <span class="hljs-operator">+</span><span class="hljs-operator">+</span> salt <span class="hljs-operator">+</span><span class="hljs-operator">+</span> <span class="hljs-built_in">keccak256</span>(initcode))[<span class="hljs-number">12</span>:]</code></pre><p>where <code>salt</code> is an extra 32-byte input passed to the opcode.</p><p>Notice which parameters affect the resulting address now. The sender is still one, but instead of the nonce we have a salt and the hash of the init code.</p><h1 id="h-create2-factories" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">CREATE2 factories</h1><p>Suppose there is a factory contract that receives some init code and a salt, and executes the <code>CREATE2</code> opcode with them. Let’s also suppose this factory is deployed in two chains, and we use both of them to deploy a contract.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/7c4a780e0b5a927f6a474c7d2fbb2a40d0cdfe91e58b76cd02ce82798a84b91e.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAOCAIAAADBvonlAAAACXBIWXMAAAsTAAALEwEAmpwYAAAC0ElEQVR4nIVUTUwTQRQeYyIxUSuxovZkIvIXsCuiVKOmCBQRNSEGmRijK6ghKn+riVKMhlaMoVMPhOLJ1Ish5WRWL7JZQugGD9aJMUjLhSbDCab1QLOLB8kaOrIuJSlf3mHefPP2e+/N2wGTYeXnzMym9vUbXrVIhNk6KhIxqI2BgBCiZ4WqqpRSVVU1TVPXQE1gFANzjdilVGpzAUqppmkfxQ8+X//QkN/T1zsy8p4J67quaZofvfL5+n2+/p6eh4oSzkgOEDKfXeD38jLG3/dYQGnxLttewJXutu3bepNvQQgFAgGO4/IsoDA/p6wk98SxvDu3oK7r47KMMWa1rlbA6iJrYMosQUKI1+uxWq0AgLZWl+tcceNF+wVXZc72nRxnlyQJQliQn9vUWNF8pbLhfElXx21N0ziOgxCyz4JQaLSoqEhVVZ7nOY5DCDENQghCiOd5hBCldFyWX3ifDwy89HqejY19liQJIRQMBkVRfBd8K3Q/EITOp+4nX6amMlvk978GAIiiCCF0pgEh5NLAGLNDhJClVGrjtRsL84UZ1D+BUGgUAKAoCsY4mgYhRBAEp9PpdrtZKymliURyYZEyo5Qa/SSEGPsLi5SQ+UyBaDTq9XrNWRiQJMntdkMIQ6HRrs622uqKulqHq+a40H3PuCdK6eVL1cUFtsLDB7gjBwcHUTp0xTxFxPDNTTAkMcbl5RUAgNMn91t3gLOnbDlbAACA4+yCIAAAtgFgL7WUlVjqXYc621v+rKxACHmeJ4QYAtmgpv+jq00N9+82Ch3X+Ot1XR2tVVU1DodDFMXh4TfOM/bu9ubHj27U1x719PXquh5I4/+YZheg6Z4mEklR/CTLE2PSOCHzhBCMsSRJ0Wg0FpudDCuyPDEZVtKH140DmIvHN63APBuqqv5KJo11BhhlxK4+FT+mp+fi8ewWi83GYrPmJ8zYyaCYa479C8q0BnFat5OBAAAAAElFTkSuQmCC" nextheight="857" nextwidth="2028" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>Will the created contract have the same address in both cases?</p><p>As we explained in the previous section, the address will depend on:</p><ul><li><p>The init code and salt passed to <code>CREATE2</code></p></li><li><p>The address of the factory</p></li></ul><p>Since we are deploying the same contract, the init code will be identical. And we are passing the salt, so we can just use the same value in both cases.</p><p>In other words, <strong>the address of the deployed contract will be the same as long as the address of the factory is the same in both chains</strong>. But this is exactly the thing we want to solve in the first place! It’s a chicken-and-egg problem. That being said, if we manage to solve it, we are solving the problem for everyone else, because the factory contract can then be used permissionlessly.</p><p>In the previous article we saw some approaches to tackle exactly this problem. Could we use one of those to deploy a CREATE2 factory in many chains that anyone can use? As it turns out, at least four such factories exist. Let’s briefly explore each one.</p><h2 id="h-1-arachnids-deterministic-deployment-proxy" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">1. Arachnid’s Deterministic Deployment Proxy</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Arachnid/deterministic-deployment-proxy">Repo</a></p><p>This is the earliest of the CREATE2 factories we’ll discuss: it was deployed <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/tx/0xeddf9e61fb9d8f5111840daef55e5fde0041f5702856532cdbb5a02998033d26">more than five years ago</a>. It’s a very minimal contract that just receives a salt and an init code, calls <code>CREATE2</code> with them, and returns the address of the created contract (or reverts if the address already had code). Its whole runtime is just <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Arachnid/deterministic-deployment-proxy/blob/be3c5974db5028d502537209329ff2e730ed336c/source/deterministic-deployment-proxy.yul">69 bytes of low-level code</a>.</p><p>This factory is deployed using Nick’s method.<sup id="fnref:1"><a class="footnote-ref" data-id="da10c3f4-749f-47e7-80fd-f32667ffcaff" href="#fn:1" data-reference-number="1">1</a></sup> The deployment transaction looks like this:</p><pre data-type="codeBlock" text="{
  &quot;to&quot;: null, // it's a deployment transaction
  &quot;input&quot;: &quot;0x6045...0cf3&quot;, // init code
  // ...other fields...
  &quot;r&quot;: &quot;0x2222222222222222222222222222222222222222222222222222222222222222&quot;,
  &quot;s&quot;: &quot;0x2222222222222222222222222222222222222222222222222222222222222222&quot;,
  &quot;v&quot;: &quot;0x1b&quot;,
}"><code><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"to"</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">null</span></span><span class="hljs-punctuation">,</span> <span class="hljs-comment">// it's a deployment transaction</span>
  <span class="hljs-attr">"input"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0x6045...0cf3"</span><span class="hljs-punctuation">,</span> <span class="hljs-comment">// init code</span>
  <span class="hljs-comment">// ...other fields...</span>
  <span class="hljs-attr">"r"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0x2222222222222222222222222222222222222222222222222222222222222222"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"s"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0x2222222222222222222222222222222222222222222222222222222222222222"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"v"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0x1b"</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">}</span></code></pre><p>Notice the values of the <code>r</code> and <code>s</code> fields. In our explanation of Nick’s method, we said that any random values have a good chance of being a valid signature, and that’s true. But the problem with using random values is that it’s impossible to prove you don’t actually have a private key that generated the signature. That’s why this transaction uses <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://en.wikipedia.org/wiki/Nothing-up-my-sleeve_number">nothing-up-my-sleeve numbers</a> like <code>0x2222...2222</code>.</p><h2 id="h-2-create2deployer" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2. create2deployer</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pcaversaccio/create2deployer">Repo</a></p><p>Unlike the previous factory, <code>create2deployer</code> is written in Solidity. This makes it more complex and its runtime much bigger, but in turn it means it can have some useful view functions and it’s easier to call from other contracts. This factory is deployed using a managed private key.</p><h2 id="h-3-safe-singleton-factory" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3. Safe Singleton Factory</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/safe-fndn/safe-singleton-factory">Repo</a></p><p>This is a fork of Arachnid’s Deterministic Deployment Proxy made by the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://safe.global/">Safe</a> team. The only difference is how it’s deployed: it uses a managed private key instead of Nick’s method, allowing it to be deployed in chains that enforce replay protection.</p><h2 id="h-4-createx" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4. CreateX</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pcaversaccio/createx">Repo</a></p><p>CreateX was developed by the same author of <code>create2deployer</code>, and improves upon it in at least two ways:</p><ul><li><p>It has even more features (like CREATE3, which we’ll explain in a minute)</p></li><li><p>It’s deployed using pre-signed transactions<sup id="fnref:2"><a class="footnote-ref" data-id="3156404e-0830-47c6-b149-057e4ab8cf4d" href="#fn:2" data-reference-number="2">2</a></sup>, with a managed private key kept as a backup</p></li></ul><h2 id="h-availability" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Availability</h2><p>These four factories have been widely deployed. All of them <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.evmdiff.com/features?feature=deployedContracts">are available</a> in the chains listed by evmdiff. I went over the list of rollups in l2beat and had to reach the 15th listed chain to find an example of a missing factory: Morph <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://explorer.morph.network/address/0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2?tab=contract">doesn’t have create2deployer</a>, at least at the time of writing this.</p><p>Even if some factory is missing in a given chain, chances are that CreateX will be there: it's been deployed to <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://createx.rocks/deployments">over 180 chains</a>.</p><h1 id="h-create3" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">CREATE3</h1><p>In our discussion of CREATE2 factories we said that we can deterministically deploy a contract as long as:</p><ul><li><p>There is a CREATE2 factory at the same address in all the target chains</p></li><li><p>We use the same salt and init code each time</p></li></ul><p>As we just saw, factories are widely available, so we can consider the first point as a given. And in all of these factories, the salt is provided by the caller, so we can just use the same value each time.</p><p>What about the init code? Since we want to deploy the same contract, it’s natural to assume it will also be the same each time, but this is not necessarily true. One simple counterexample is a contract that receives constructor arguments whose values might change between chains. Constructor arguments are included as part of the init code<sup id="fnref:3"><a class="footnote-ref" data-id="b82ea2cb-1f02-466f-a089-58342f99d3c6" href="#fn:3" data-reference-number="3">3</a></sup>, meaning that the <code>CREATE2</code>-generated address would change.<sup id="fnref:4"><a class="footnote-ref" data-id="1b2ea75c-4574-4140-b104-e2936faad6df" href="#fn:4" data-reference-number="4">4</a></sup></p><p>In cases like that, <code>CREATE2</code> is not an option because the init code will affect the deployment address; there’s no way around that. The methods based on deployment transactions we explored in the previous article don’t have this issue, but they involve a significantly bigger effort.</p><p>If we don’t want to use deployment transactions and we can’t use <code>CREATE2</code>, there’s only one mechanism left to deploy a contract: the <code>CREATE</code> opcode.</p><p>Suppose we have a contract factory with the following characteristics:</p><ul><li><p>It has a method that receives the init code of a contract and deploys it using <code>CREATE</code>.</p></li><li><p>It has the same address in every chain.</p></li><li><p>It has the same nonce in every chain.</p></li></ul><p>The first two conditions are easy (the code of such factory is straightforward, and we can deploy it using a CREATE2 factory) but the third one is harder: we can’t control who uses this <code>CREATE</code>-based factory, and each time someone uses it its nonce is incremented. But what if we deploy a factory like this and, in the same transaction, we use it to deploy our contract? That’s the essence of the <code>CREATE3</code> approach.<sup id="fnref:5"><a class="footnote-ref" data-id="b8e8d23a-a6c1-472e-ae90-f611c5decc18" href="#fn:5" data-reference-number="5">5</a></sup></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/698f3de29fd182032340f369a6d5e6c2a0f70eff8fa7be798e3ddc2fa6efa1fe.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAKCAIAAABaL8vzAAAACXBIWXMAAAsTAAALEwEAmpwYAAACTElEQVR4nGO4eu36vfv3H2GAb0jgxcuXjx49evHyJano6rXrDJ8+f46IiCgqKvr////Xr1//g8Hr168LCzKLCjLLy3KLCrMePHjwHx38BSMC4MXLlwz///9nYGDg5ORsbm729vIODgqura0VFREOC7Y8dmjegd0z8rN8GxvrFi1e/Pr1a4ImYrdASkoqISHh+vXrZtYWcYnxp8+fbWqq19cR62lLb65LiIm0i4+PlZCW0tXTdXBwCAsLKyzI9PN1DgnxCgv13rBhHR7PgSz48/dvc3Pz1KlT////f/32jVv3bz199Xz58qVCPAxhwaYeLppmxlKREWGrV63u6enp6u6OjIxWkuffual3+cKGOdNLwkK94WH7FQzQfQCJwEePHiHZ/3/58qUxkXbPn++/fXNLXVVMYIBvb29fT0/38WPHli1fLiPBKsrLwMDAoKrMGhHmjxx5N27c3L1795EjR/bt3Xv9+vXXr18zIBmNAK9fv85Ii42N8Q8L9Q4P8/Hw8ACZx8BgZm0RFh4hKcZQkOUZH20bH2UtJyu6d++B7du27du7d9OmzaYm2qbGsppqIqGBlknxwfcfPMRuwf///5GT6VdUvwf6uXq6GXm4Gvl7m9vbWenpG2hoadQ21haXlYkJs6gpM0iLMbg7qzja6V27fhOnBWjx9vP7zx/fv0M4f/7+hdv95+/fFy9fPHjy4OK1K5++fTl06MDcOVPWr1s6bUrvqpXL37x5y3Dv/v1Pnz+TiiB++vr162swgLM/ff4Md9TXr1/v3b8PAAvZzfmNfOQTAAAAAElFTkSuQmCC" nextheight="679" nextwidth="2153" class="image-node embed"><figcaption htmlattributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>In this diagram we have a <code>Create3Factory</code> contract with a <code>deployCreate3</code> method.<sup id="fnref:6"><a class="footnote-ref" data-id="fe3446d6-a1e8-4d8a-943d-34f47f83f4c5" href="#fn:6" data-reference-number="6">6</a></sup> The parameters in green are the ones provided by the user. How will the address of <code>MyContract</code> be derived?</p><p>We are assuming that <code>Create3Factory</code> exists at the same address in every chain. This means that the two other addresses are:</p><ul><li><p>The address of the one-time-use CREATE factory. The init code for this contract is always the same, and so its address will depend only on the user-provided salt.</p></li><li><p>The address of the contract we want to deploy. Because the one-time-use factory has just been created, its nonce is guaranteed to be 1. This means that only its address will affect the address of our contract.</p></li></ul><p>Putting it all together, the address of our deployed contract depends only on the address of the one-time-use CREATE factory, which itself depends only on the user-provided salt. In other words, the address of our contract will depend only on the salt we pass to <code>deployCreate3</code>. It won’t be affected by its init code, the sender address, or any of the nonces involved.</p><p>While this does what we want, it introduces a new problem. When we deploy our contract in some chain using a salt <code>S</code>, anyone can go to some other chain and use the same factory and salt to deploy <em>any</em> contract. This is not great. The usual workaround is to implement <code>deployCreate3</code> so that the address of the deployed contract can only be obtained by a given <code>msg.sender</code> (there are a couple of ways to accomplish this; see <sup id="fnref:7"><a class="footnote-ref" data-id="55906884-9330-4663-8a36-501e1f776a56" href="#fn:7" data-reference-number="7">7</a></sup>). In a way, this takes us back to where we started: we need to manage a private key so that the same <code>msg.sender</code> is used every time. But eliminating the nonce dependency is a significant improvement.</p><h1 id="h-whats-next" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">What’s next</h1><p>We’ve covered a lot of ground in these two articles:</p><ul><li><p>Managed private keys and pre-signed transactions</p></li><li><p>Nick’s method</p></li><li><p><code>CREATE2</code>-based approaches, like CREATE2 factories and CREATE3</p></li></ul><p>But there’s still more. In the next and final entry of this series we’ll explore some other approaches to the problem, including the very clever <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-7955">ERC-7955</a>. You can <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://paragraph.com/@cethology?modal=subscribe">subscribe</a> to the blog to get notified when the third part is published.</p><br><ol class="footnotes"><li id="fn:1" data-id="da10c3f4-749f-47e7-80fd-f32667ffcaff"><p>Which makes sense, because <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Arachnid">Arachnid</a> is the Nick after which the method is named.</p></li><li id="fn:2" data-id="3156404e-0830-47c6-b149-057e4ab8cf4d"><p>In fact, this is the only project I’m aware of that uses pre-signed transactions, and it’s where I learned about that approach.</p></li><li id="fn:3" data-id="b82ea2cb-1f02-466f-a089-58342f99d3c6"><p>In the previous article we said that the init code is executed to return the runtime code of the deployed contract. While this is correct, it’s an incomplete description. The init code also performs initialization which, in Solidity terms, means executing the constructor. The constructor arguments therefore need to be available, and the way this is done is by including them in the init code.</p></li><li id="fn:4" data-id="1b2ea75c-4574-4140-b104-e2936faad6df"><p>I’m using this as an example of something that makes the init code change, but I should point out that there’s a simple workaround: instead of initializing the contract in the constructor, the contract can have an <code>init</code> method that can only be called once, and it’s immediately called with the proper arguments during the same deployment transaction. If you are using CreateX, you can do this with the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pcaversaccio/createx/blob/fb153a82585542e94ce6100727713b42ba419cf9/src/CreateX.sol#L372C14-L372C34">deployCreate2AndInit</a> method.</p></li><li id="fn:5" data-id="b8e8d23a-a6c1-472e-ae90-f611c5decc18"><p>CREATE3 was originally <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/EIPs/pull/3171">proposed as a new opcode</a>, but it was withdrawn because there was a workaround that seemed good enough. The approach we are explaining here is a different workaround though, and the one I believe most people have in mind when they say CREATE3.</p></li><li id="fn:6" data-id="fe3446d6-a1e8-4d8a-943d-34f47f83f4c5"><p>For simplicity I’m assuming there is a deterministically deployed <code>Create3Factory</code> with this functionality, but the pattern is still possible if there isn’t. In that case, you need to deploy a contract which calls the CREATE2 factory and then calls the one-time-use factory. This contract does <em>not</em> need to be deterministically deployed.</p></li><li id="fn:7" data-id="55906884-9330-4663-8a36-501e1f776a56"><p>One way to ensure a target address can only be used by a given caller is to modify the salt: instead of just forwarding it to <code>CREATE2</code>, the factory creates a new salt by hashing the user-provided salt and <code>msg.sender</code>. If there aren’t other functions that let you use <code>CREATE2</code> with an arbitrary salt, then this approach guarantees that the resulting address can only be used by that sender. This is not the only approach though. CreateX, for example, does <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/pcaversaccio/createx/?tab=readme-ov-file#permissioned-deploy-protection-and-cross-chain-redeploy-protection">something different</a>.</p></li></ol>]]></content:encoded>
            <author>cethology@newsletter.paragraph.com (Franco Victorio)</author>
            <category>ethereum</category>
            <enclosure url="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/7ae030c6e32093f21e923218fb4ff501520ef8f0f8e6a6651634cb9f65e637c3.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Deterministic Deployments, Part 1: The Basics]]></title>
            <link>https://paragraph.com/@cethology/deterministic-deployments-part-1-the-basics</link>
            <guid>EhPzTJ8dxELPsdAuaJby</guid>
            <pubDate>Thu, 05 Feb 2026 08:37:46 GMT</pubDate>
            <description><![CDATA[This is the first article in a series about deterministic deployments: how can we deploy a contract at the same address in multiple chains? This apparently simple problem has many possible solutions, and new ideas continue to emerge. In this entry we’ll go over three possible approaches: carefully managing a private key, using Nick’s method, or pre-signing transactions.]]></description>
            <content:encoded><![CDATA[<p>This is the first article in a series about deterministic deployments: how can we deploy a contract at the same address in multiple chains? This apparently simple problem has many possible solutions, and new ideas continue to emerge.</p><p>In this entry we’ll go over three possible approaches: carefully <strong>managing a private key</strong>, using <strong>Nick’s method</strong>, or <strong>pre-signing transactions</strong>. The rest of the series will cover other approaches like CREATE2 factories, CREATE3, ERC-7955, and more.</p><h1 id="h-basics" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Basics</h1><p>Before starting, let’s review some basic concepts that are necessary to understand the rest of the discussion. Feel free to skip this section if you are familiar with the following:</p><ul><li><p>How deployment transactions work</p></li><li><p>The difference between init and runtime code</p></li><li><p>How contract addresses are derived</p></li><li><p>The way in which the CREATE opcode behaves like deployment transactions</p></li></ul><h2 id="h-normal-vs-deployment-transactions" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Normal vs deployment transactions</h2><p>We can think of Ethereum transactions as falling into two groups:</p><ul><li><p>Transactions that have a recipient address. This recipient can be an EOA<sup id="fnref:1"><a class="footnote-ref" data-id="aec118b1-d2bf-4868-9800-a7ef7f9940e1" href="#fn:1" data-reference-number="1">1</a></sup> or a contract. If it’s a contract, its code is executed. Let’s call these <strong>normal transactions</strong>.</p></li><li><p>Transactions that don’t have a recipient. These are used to create a new contract, so we’ll call them <strong>deployment transactions</strong>.</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/9fcb319af1f70998fa2ad07cfe702db0446da548d1baf3cc60af7207d8e57091.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAEVUlEQVR4nJWVT2zbVBzHXzVxIQcOQVWRUKWhoNFKIwyt5RDElMJwO5SsWg1lmK1Nw6FMrCDAPZlLygFriMxiSwZaFSSmXMAHUFXUREWs6WF1hJS6qK3TKX6JmP0SicfbofGJPRS/1Qtt6eCrd/H746/f7/f5/QzK5S2yI4SQYRiEkHq9gRAihNxaWSGEQAgRQvV6wxt4R4QQwzDY5r0ql7dAtVqjO3Icp/2RUrqxaei6zvO8KIp0P+m6HolEE4kEO75r1bLt3QaWbTuOk3blnl8jhAAABEHIZDKSJMmynE6ns9ms6gohBACIxWL/1YDdGgDg9/sppaVVnVLKDFRVnXIlimIsFpucnOzp6YEQAgDC4fD/u8GcK0ppsViklAYCAXYhSqmiKBzHtcfE5/OpqvqvBpZtszUm9tgW4rW/7t2LxWL5fJ5tu3w5GZ+Y+PzSJQgh2xOJRNnqXrUMtptNSZL8fn8wGKyYZsU0EULVas2ybYTQzaXC7UplZubThVwOY7yxsQEh3G42McaWbVu2DSEURVFVVYwxcgUhZEuWbZdWdXB9dravr6+7u9vv9/M8XzFNQgjGuF5vEEJuLhW2m82rqdTy8jLj2DuMELpj3bFsm5C7lo3wn8SEtXL5tkdw64M2DVAxTS8BjuN4F2cyTZjP50OhEOOkXSziN258M3iyPz4WDTz1+OQ7ZwZP9qdSV7zVfZLMDBiCXpIBABzHGYaRSCTS6bSiKIlEQpIkQXi7s/OxbzOflNd/+Oj908uLX//0YzIeFw4ysGzbMAyfz9fR0QEh3Ng0HMcBAEQiUcdxVFWVZXlqakpRlJ8XF7/7Xj375nDg8KHBV3qOH+tqFcS5ExcmYwcZ1OuNQqEgy7IkSXpLa5RSjuOy2SzbI8tyOBwWRZG9Qtf1sbHRsbHReOzsubfOnBNGVldLB9XBrhwU3RDFJyYYiIQQTdN0XZ+fn0cI7YvmbkxLpZIHRsU0i8ViOye5fN6y7VsrKxXTZJMY40bjD5ccu1qtQQgZ3O6oMFIZQvcx/W19nT23N0vGKCGkVCphjL9IJhdyOYapR2q93mAFwbqLO49Kq2tsrGi/1mq/V0zzHyHaK4xxoVB4eWCA47i9mFarNa8FUEqnP34veLS7q/OR3meeHDhx7Lww3KoDz0BV1Uwmk8/nVVXV9VaPo5SyKmEUaZomiiLLfygUmp6eDj5//GoqJQjC+Ph4NHr6taEXriSnXh14+uK7p75MXhyO9i/kcg8MgsEg2JGiKO2/B5/Px/M8a+PZbFbTtEKhcH12doR/o1gssrhdu/ZV75Gu3iOPRk89CwAIHD703NEnfllaemAwNzcnSRKrIE3T2GS5vNXeTQVBCAaDXmclhLR3UPmzmSHuxdHRyBD30usjQx9+cME04UNywELE8zzr3oqisCInhDyU0fuYVkyTZWm72XTBueuO1qc5jlMubzmOo+s6QshLZnt7b9fef3K1WvsbTQ4xUKay9MoAAAAASUVORK5CYII=" nextheight="1047" nextwidth="1562" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Normal vs deployment transactions</figcaption></figure><h2 id="h-init-code-vs-runtime-code" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Init code vs runtime code</h2><p>When we use a normal transaction to call a contract, the code of this contract is executed and the <code>data</code> field in the transaction is used as the input. In that context, we call the contract’s code its <strong>runtime code</strong>. But when we send a deployment transaction something quite different happens: it’s the <code>data</code> field that gets executed. The result of this execution is used as the runtime code of the deployed contract.<sup id="fnref:2"><a class="footnote-ref" data-id="8901a05c-e530-4b1c-8a94-911b7b168b02" href="#fn:2" data-reference-number="2">2</a></sup> In that context, we say that the data of the deployment transaction is the <strong>init code</strong> of the contract.</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/317b849b69b416fbe207e0b351b207f86153750d31eb14cff904db4b67544120.png" blurdataurl="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAExUlEQVR4nJVVb2wTZRh/CfGL/eCHKuGDWYKZmVuiDYbhhxHJpngDbSF6EclNt25CBgkjYekSyBFNIYEDcVyEdjCgJLICwVPE+mdtwLHb3NoLye1m1l2HvXcN9K77cL4btIcf3Gva1x2FIeov9+V9n+e9373P83t+B1KpKbQAwzBUVUUI5XIzhmEghEbjcYQQhNAwjFxuxn7MBSCEVFUlyYuRSk2B6ekMXoBlWeVLjHFyUlUUhaZpn8+HHwdFUdxuj9/vJ8cfiWZ1/VGCrK5blhUsoXR+HCEEAGAYJhQKsSzLcVwwGAyHw0IJhmEAALxe738lILcGADidToyxPKZgjAmBIAgdJfh8Pq/X297eXl1dDSEEANTX1/+/G0RKwBhLkoQxrqysJBfCGPM8T1FUeU0cDocgCP9IkNV1EiMgy7ISj/85P+/1emOxGEk7dqy7rbX1syNHIIQkx+32kOhiFAnyhQLLsk6n0+VypTUtrWmGYUxPZ7K6bhjGjUHxVjq9f/+B/mjUNM1kMgkhzBcKpmlmdT2r6xBCn88nCIJpmkYJEEISyuq6PKaA02fO1NbWVlRUOJ1OmqbTmoYQMk0zl5tBCN0YFPOFwolAYGhoiOjYPmwYxp3snayuIzSb1Q3zd6TBTCp1y1Zw8YMmVZDWNLsBlmXZFyfQNBiLxerq6ohOykEqfv78ucZ1q9uaPZUvPNv+8buN61YHAsft6GOaTAiIBO0mAwAoilJV1e/3B4NBnuf9fj/LsgzTtGzZM1+G9qUmvu3ctXFUPPvj1e62NuZJBFldV1XV4XAsWbIEQpicVC3LAgC43R7LsgRB4Diuo6Pj8OEj169du/yVsOWDTZUrlja+Wb1q5fLiQHy4dke790kEudyMKIocx7EsqxQxjjGmKCocDpMcjuPq6+v37N1DXqEoSnPz5ubmzdu2Nrnfady2tWlsTH7SHDzSA6lUorbWViJEhNDw8PDVK98fOnh0YGD47tw9knb//h+nAn0n+HOXL0XO9l56iECWZVsYaU2TJKlcJ9FYLKvro/F4WtPIpmman35ytKvzwK6d+/r7r5NTaU0b+SUx8POQeGNkZEQiEvpbpr9OTJB1uVkSjSKEZFk2TfPz7u7+aNSW6dy9uXv5u/lCnliLbau53ExyUtU0KI+NxxM3M5nbaU17qESLYZqmKIpvNDRQFLVYphBC2wIwxp27d7herli+7Kmal55vWLvyI2ZTcQ5sAkEQQqFQLBYTBEFRih6HMSZTQlSUSCR8Ph/pf11dXVdXl+vVVScCAYZhWlpaPJ6Nb69/7Xh3x1sNL+7cvuGL7p2bPKv7o9EHBC6XCyyA5/ny34PD4aBpmth4OBxOJBKiKPb0nHyPfl+SJOIQPT0na6qW11Q97dnwCgCgcsXSmqrnBgYHHxBEIhGWZckEJRIJsplKTZW7KcMwLpfLdta52VmM5+2icYf2r6fWbN/GvL6mtmmLZ++e3ZoG/6UHpEQ0TRP35nk+Evmutzc0OnIz/VsmOXFLisu2WIle+38YkOJjipxU5GTxd5TWNNKlfKFQEs5s6UFkM5WasixLURTDMOxmlgZi9hvhp96TfYcPBjKZ2xjPW5ZFhHfxwpXTp/ou9H198cKV6enMX51xGxzQ+XkwAAAAAElFTkSuQmCC" nextheight="1047" nextwidth="1562" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Init code vs runtime code</figcaption></figure><p>It’s important to emphasize how different these two scenarios are. When we call a contract, the data field usually encodes the function we are calling and the arguments we are passing. That is, it’s just data. But in a deployment transaction, the data is valid EVM code.</p><h2 id="h-address-derivation" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Address derivation</h2><p>We now know that the runtime code of a newly created contract comes from executing its init code. But <em>where</em> is the contract deployed? Its address is determined by the following formula:</p><pre data-type="codeBlock" text="address = keccak256(rlp([sender, nonce]))[12:]"><code><span class="hljs-selector-tag">address</span> = keccak256(rlp(<span class="hljs-selector-attr">[sender, nonce]</span>))<span class="hljs-selector-attr">[12:]</span></code></pre><p>That is: we <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/developers/docs/data-structures-and-encoding/rlp/">RLP-encode</a> the address and nonce of the account sending the deployment transaction, hash the result, and take the last 20 bytes of that hash. But the details don’t matter that much. What’s important for our purposes is that the address of the deployed contract depends only on the address and nonce of the sender.</p><h2 id="h-the-create-opcode" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The CREATE opcode</h2><p>Transactions are not the only way to create new contracts. Existing contracts can also make deployments. One of the two ways they can do it is with the <code>CREATE</code> opcode (the other way is with the <code>CREATE2</code> opcode, which we’ll discuss in the next entry of this series).</p><p>For this discussion, there isn’t that much difference between a deployment transaction and an execution of the <code>CREATE</code> opcode:</p><ul><li><p>Both execute some init code, but the <code>CREATE</code> opcode takes it from the EVM memory of the current execution, not from the transaction data.</p></li><li><p>In both cases, the address of the created contract depends on the deployer's address and its nonce. When a contract makes a deployment, its address and nonce are used. The only nuance here is that contracts only increment their nonce when they deploy contracts, and that their starting nonce is 1.</p></li></ul><h1 id="h-approach-1-managed-private-key" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Approach 1: Managed private key</h1><p>We want to figure out how to deploy a contract at the same address in multiple chains, and we explained that the address of a contract depends only on the address and nonce of the account that sends the deployment transaction. This means that there is a straightforward solution to our problem: deploy the contract using the same private key on every chain, making sure it's the first transaction sent from that address (or the second, or the fifth; it doesn’t matter as long as the nonce is always the same).</p><p>This is the simplest answer to the problem of deterministic deployments, but it has a few downsides:</p><ul><li><p>The private key needs to be kept secure in perpetuity. If you lose access to it, then there's no way to deploy at the target address in new chains.</p></li><li><p>If for any reason the deployment transaction reverts, the nonce gets consumed and the possibility to deploy at the desired address is lost forever for that chain.</p></li><li><p>The deployment can’t be done by anyone. Only the people with access to the private key can deploy the contract.</p></li></ul><h1 id="h-approach-2-nicks-method" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Approach 2: Nick’s method</h1><p>Nick’s method is a technique to create valid transactions without using a private key. To understand how it works, we first need to have a proper view of what a transaction is made of.</p><p>In our first diagram, a transaction was represented by an object like the following:</p><pre data-type="codeBlock" text="{
  &quot;from&quot;: EOA,
  &quot;to&quot;: Foo,
  &quot;data&quot;: &quot;0x1234...&quot;,
  // ...other fields
}
"><code><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"from"</span><span class="hljs-punctuation">:</span> EOA<span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"to"</span><span class="hljs-punctuation">:</span> Foo<span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"data"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0x1234..."</span><span class="hljs-punctuation">,</span>
  <span class="hljs-comment">// ...other fields</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>While this is a useful simplification, it’s not technically correct. A more accurate representation is:</p><pre data-type="codeBlock" text="{
  &quot;to&quot;: Foo,
  &quot;data&quot;: &quot;0x1234...&quot;,
  // ...other fields...
  &quot;r&quot;: &quot;0xabcd...&quot;,
  &quot;s&quot;: &quot;0xfedc...&quot;,
  &quot;v&quot;: 27,
}
"><code><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"to"</span><span class="hljs-punctuation">:</span> Foo<span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"data"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0x1234..."</span><span class="hljs-punctuation">,</span>
  <span class="hljs-comment">// ...other fields...</span>
  <span class="hljs-attr">"r"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0xabcd..."</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"s"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0xfedc..."</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"v"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">27</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>The <code>r</code>/<code>s</code>/<code>v</code> fields are the values of the transaction signature. Notice we have removed the <code>from</code> field: since the sender address can be recovered from the signature values, including it in the transaction is redundant and wasteful.</p><p>The normal way signature values are obtained is by encoding the transaction and signing it with a private key. But here’s the thing: it’s easy to use arbitrary <code>r</code>/<code>s</code>/<code>v</code> values and get a valid signature for a given unsigned transaction.<sup id="fnref:3"><a class="footnote-ref" data-id="0615626c-80c6-4037-8aab-e847a0bf68f9" href="#fn:3" data-reference-number="3">3</a></sup> The <code>from</code> that corresponds to this signed transaction will be effectively random, but if we fund that address and then propagate the signed transaction, it will be mined.</p><p>We can use this method to manufacture a signed deployment transaction and propagate it in multiple chains. The address of the deployed contract will be the same in every chain, because the sender is the same and the nonce is always 0 (we have to use a nonce of 0 for the transaction to be valid, because the sender address, being effectively random, is completely unlikely to have a non-zero nonce).</p><p>This method, normally called Nick’s method<sup id="fnref:4"><a class="footnote-ref" data-id="fb0eb627-d202-4dc9-9315-efe67df4091a" href="#fn:4" data-reference-number="4">4</a></sup>, improves upon two of the downsides of the managed private key approach of the previous section:</p><ul><li><p>There’s no need to keep a PK secure because there’s none involved.</p></li><li><p>Since the manufactured transaction is public, anyone can deploy the contract in a new chain by funding the sender and propagating the transaction.</p></li></ul><p>But the method has a couple of downsides that the previous approach doesn’t have:</p><ul><li><p>The transaction can never be changed, because that would invalidate the signature. This means that all the fields have to be valid for every possible chain. If, for example, the gas limit is too tight and we propagate the transaction in a chain where opcodes are pricier, then the execution will revert with an out-of-gas error. In that case, the possibility of deploying to that address will be lost forever.</p></li><li><p>Nick’s method only works on chains that don’t enforce <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-155">replay protection</a><sup id="fnref:5"><a class="footnote-ref" data-id="d222f1ea-2b6c-432b-b56b-ad1ceaa71cf3" href="#fn:5" data-reference-number="5">5</a></sup>, because we are intentionally replaying the same transaction in every target chain.</p></li></ul><h1 id="h-approach-3-pre-signed-transactions" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Approach 3: Pre-signed transactions</h1><p>This approach can be seen as an extension to the managed private key approach rather than its own thing. I’m explaining it in a separate section because it’s easier to understand its trade-offs after having gone over Nick’s method.</p><p>We said that the managed private key approach means keeping a PK secure forever, and that only those with access to it can deploy the contract. But, as we saw in the previous section, anyone can propagate a signed transaction as long as it’s valid. This means that a deployment transaction can be signed (without replay protection) and shared publicly so that anyone can use it in other chains.</p><p>In a way, this approach combines the best aspects of the previous two methods:</p><ul><li><p>As with Nick’s method, anyone can make the deployment, even if the PK gets lost.</p></li><li><p>Unlike Nick’s method, we can use the managed PK as a fallback if the pre-signed transaction doesn’t work (for example, in chains that enforce replay protection).</p></li><li><p>And we can sign and share more than one transaction that deploys the same contract. This is useful to have pre-signed transactions with different gas limits.<sup id="fnref:6"><a class="footnote-ref" data-id="6ec2436a-7592-4579-acbb-b27377f32caf" href="#fn:6" data-reference-number="6">6</a></sup></p></li></ul><h1 id="h-whats-next" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">What’s next</h1><p>In the next article of the series we’ll look into two other approaches to the deterministic deployments problem: CREATE2 factories and CREATE3. You can <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://paragraph.com/@cethology?modal=subscribe">subscribe</a> to the blog to get notified when the second part is published.</p><ol class="footnotes"><li id="fn:1" data-id="aec118b1-d2bf-4868-9800-a7ef7f9940e1"><p>Externally Owned Accounts. These are accounts that don't have code and are controlled by a private key.</p><br></li><li id="fn:2" data-id="8901a05c-e530-4b1c-8a94-911b7b168b02"><p>More precisely: if the execution doesn’t revert, the return data contains the runtime code that will be used for the deployed contract. For example, the code <code>0x620102035f526003601df3</code> returns <code>0x010203</code>. If you sent a deployment transaction with that init code, a contract with the (nonsense) runtime code <code>0x010203</code> would be created. If you want to see how that works in more depth, check <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://www.evm.codes/playground?fork=osaka&amp;unit=Wei&amp;codeType=Mnemonic&amp;code=%27JTh~Q_pcod~takesG8tY%20fromXWsCw~first%20LV3V0ZLZqQjakesjwCarguments%3Aq-K4position%20inX_fGFjYq-5ize4number_f%20FjYWstarting%20atGgivenKqInBase%2CBod~isjhre~F%20long.%20AndWsinc~th~8w~Ld%20get6left-padded%20with%20zeroesWit5tart6atXK%202973729ZQ%27~e%20qZJj%20t_%20oZ%5CnYCreturnX%20memoryW%2C%20V%200x010203DQRETURNLMSTOREK_ffsetJ%2F%2F%20Gjh~FbytesDZPUSHCo%20B_ur%20c8valu~7D1%206s%205%20s4%3AG%0145678BCDFGJKLQVWXYZ_jq~_"><u>this EVM playground</u></a>.</p><br></li><li id="fn:3" data-id="0615626c-80c6-4037-8aab-e847a0bf68f9"><p>Basically: if we pick random <code>r</code> and <code>s</code> values, we have an almost 50% chance of getting a valid signature. <code>v</code> can then be derived from <code>r</code> and <code>s</code>.</p></li><li id="fn:4" data-id="fb0eb627-d202-4dc9-9315-efe67df4091a"><p>Named after Nick Johnson, who explained in <a target="_blank" rel="noopener noreferrer" class="dont-break-out notion-link-token notion-focusable-token notion-enable-hover" href="https://weka.medium.com/how-to-send-ether-to-11-440-people-187e332566b7"><u>this 2016 article</u></a> how the method was used for a rather different problem. I’ve also seen this method called “one-time-account transactions”, which is more self-descriptive but less cool. Actually, I think the only person that uses that term is Nick himself, whom I guess is too modest to call it Nick’s method.</p></li><li id="fn:5" data-id="d222f1ea-2b6c-432b-b56b-ad1ceaa71cf3"><p>Transactions with replay protection include the chain id as part of the signed transaction. If the chain id is different from the current chain, the transaction is considered invalid.</p></li><li id="fn:6" data-id="6ec2436a-7592-4579-acbb-b27377f32caf"><p>Notice that this has a griefing risk. If the pre-signed transaction with the lowest gas limit doesn’t work on a given chain, then someone could fund the sender and propagate it so that it reverts with an out-of-gas error, preventing the contract from being deployed to the target address.</p><br></li></ol>]]></content:encoded>
            <author>cethology@newsletter.paragraph.com (Franco Victorio)</author>
            <category>ethereum</category>
            <enclosure url="https://img.paragraph.com/cdn-cgi/image/format=auto/https://storage.googleapis.com/papyrus_images/8c35b5a3461176c1b2ceb60248d160b055711a60ad2ef0fcc518b68345caa83e.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>