Ethereum is turning into a labyrinth of unnecessary complexity with EOF - Let's reconsider EOF

These are general criticisms of the rollup centric roadmap, not EOF in particular. If EOF were not proposed these would still be problems, in the same way they are stated.

Note that one engineer from Optimism said that if EOF ships on mainnet it will appear on the op-stack. This is because of OP’s architecture that relies on lightly modified L1 clients, they will get the bulk of the work for free. The parts of the EVM that EOF would modify are fairly separated from the pieces that are changed to accomodate the other parts of the OP stack such as the sequencer.

5 Likes

What are the benefits of EOF to Ethereum users, app devs and to dapp consumers?

3 Likes

An important consideration though is the potential increase in version management for L2s, as the ability to deploy multi-chain gets easier. In the current system smart contract devs can opt out of using new features that are not enabled on every chain they deploy to. For example, simply setting your foundry EVM-version config to the oldest one supported by all the chains you want to deploy on to ensure bytecode uniformity. Shipping on l1 first makes managing different bytecode versions mandatory for all devs, which can be a real headache before we even discuss having to maintain two different versions of the codebase if you plan on working in low-level assembly where opcode changes are impactful. We’ve seen this directly with transient storage not being implemented at scale because of chains that still haven’t implemented it.

Awesome! As @pdobacz pointed out up-thread and I replied, I had written the originally-linked thread before that was possible and had not checked up on the changes :+1:

I’m not denying that there are reasons for having this behavior. My point here is that there are interesting (and arguably valuable) use cases for being able to know things about the code deployed to other addresses. By removing this functionality, it creates new problems for application developers. As a consequence of creating those problems, there is a very explicit disincentive for developers (like myself) to adopt EOF.

This is not the only reason to want to pass something less than all-but-one-64th of the available gas to a sub-context when performing a call. Particularly if the called contract is only somewhat trusted by the calling contract. Again, there are valuable design patterns enabled by the ability to pass less gas when performing a call, and the removal of that capability makes my life harder.

Additionally, designing contracts around opcode repricing has been a thing for ages already. A contract that is designed in such a way that it is brittle to opcode gas costs is already a badly-designed contract.


Overall, while I appreciate the detailed responses, @shemnon, that you’ve given to the criticism in this thread, but I think that taken in totality they’re clearly missing the point here that EOF is being sold as “a VM improvement that will benefit smart contract developers”, when in fact it is actually being driven by the needs/agenda of the execution layer to the detriment of smart contract developers. It’s this discrepancy that causes me to bristle and which I think will ultimately stand in between EOF and adoption. That’s not to say that I don’t see and appreciate the reasons why EOF may be a net improvement to Ethereum, but when I look shortsightedly at the path of least resistance, that path is “ignore EOF if adopted and oppose it before it is because, it makes my life harder”.

And I also endorse the general thesis of the OP: this is additional complexity that is patently unnecessary from the perspective of the smart contract developer.

Can you name the use cases specifically? As noted above, we’re gathering feedback and acting on it (examples: TXCREATE, HASCODE, EXTCODEADDRESS, metadata section… not always it’s feasible to include in EOFv1/Osaka, but one needs to know the use cases to even start discussing them).

Again - please be more specific and name the use cases, ones which don’t fall into the latter category of brittle.

I feel like OP listed many benefits in the first half of the post, and @shemnon laid out why EOF is currently the optimal known path forward, if we want to have these benefits. They’re not short-term nor direct benefits, but rather enablers and quirk-removers. They are aimed to benefit sc developers via better and more maintainable compilers, SA, stability against gas repricings, lax contract size limits, EVMMAX etc.

And please, elaborate on the above 2 points or anything else that shortsightedly makes life harder, maybe as with TXCREATE it isn’t so bad.

One thing is it opens the door to larger smart contracts EIP-7830: Contract size limit increase for EOF to 64kB

In practice developers deploy multiple smart contracts and use strange contrivances like ERC-2535: Diamonds, Multi-Facet Proxy to get around the contract limit.

However this adds complexity and opens door to hacks from improper permissions for smart contract comms or initialization gaps.

2 Likes

What were the actual hacks when contract size limitations lead to user funds being stolen/inaccessible? Knowing a dollar value would be interesting. Thanks

To me the argument that greater contract size equals more correct contract code is unclear. I can only anecdotally speak for myself and when I develop smart contracts, the smaller they are the more likely they are to be correct and safe. That said, maybe the Diamond pattern is at fault for producing issues, not the contract size limitation. Can your problem be fixed by reforming the Diamond contract pattern? I‘m worried that we‘re adding smth here that is super complex but has no direct benefit to consumers. Idk if you‘ve looked at the price and sentiment recently… it‘s not good xD so it‘d be wise to prioritize, even if that means orphaning some branches

I don’t think EOF actually does open the door to larger smart contracts. EIP-170 was quite clear that VM bytecode preprocessing was only one of several DoS vectors that grow linearly in the size (and these are well documented in the discussions around it).

The correct solution here is removing contract size limits and replacing with gas metering on loading code, as I recently proposed in EIPs 7903 and 7907 (there is predecessor work here too, for instance EIP-5027)

The sheer length and depth of this response already serves as proof of the unnecessary complexity that EOF introduces. Yet, despite all this effort, the core issue I raised—the full extent of complexity we’re facing—remains unaddressed. If it takes this much explanation just to justify EOF, isn’t that a sign we’re heading in the wrong direction? Ethereum should prioritise simplicity, not more labyrinthine abstractions.

@MariusVanDerWijden has written an excellent section on the complexity here:

The biggest issue I see is that EOF is extremely complex. It introduces RJUMP, RJUMPI, RJUMPV, CALLF, RETF, JUMPF, EOFCREATE, RETURNCONTRACT, DATALOAD, DATALOADN, DATASIZE, DATACOPY, DUPN, SWAPN, EXCHANGE, RETURNDATALOAD, EXTCALL, EXTDELEGATECALL and EXTSTATICCALL.

All-in-all EOF introduces 19 new opcodes. Additionally it removes 16 opcodes present in the existing EVM from EOF and changes the behavior of multiple opcodes in both versions. These changes have already resulted in two consensus issues in Besu and evmone (used by erigon) even before EOF made it to mainnet. Luckily these issues were found by Martin Holst Swende before they made it into a release.

All these new opcodes must be tested, especially because some of these (like EOFCREATE and RETURNCONTRACT) interact with each other.

Additionally EOF introduces the code and stack validation. This means that the code is verified before it’s being deployed. Therefore the code validity now becomes part of consensus which means that bugs in the code or stack validation will result in consensus bugs. Even worse, if the code and stack validation spec has a bug, a faulty contract could be deployed which would result in undefined behavior if called.

But you keep ignoring these concerns. You can go ahead and gamble with Ethereum’s core machinery, but I won’t stand by and watch. I care about not messing up the foundation—because if we screw up the core, there won’t be a core anymore.

3 Likes

Btw why is the discussion here centered around the quality of the improvement? This seems to me unnecessarily pure in regard to what we want to achieve. A quick reminder, we aren’t in academia. We have a token price to increase.

It must be pretty clear to everyone who is locked into Ethereum prices that there are fundamental things wrong with it since the blobs upgrade. Clearly we have to fix a bunch of things in the short term, and we have to serve application developers so that they can onboard more users, to make the blob-addition logic make sense finally. We’re not going to be able to increase the total fee accrual by just ignoring the issue, kicking the can down the road, or by increasing prices. So inevitably we’re going to have to onboard more users. I’m pretty sure there is a maximum capacity of things the core devs can do within a time frame. Why are we asking them to do a bunch of refactors for a goal that doesn’t align to fix short term issues?

But if the only argument here is that contract sizes can be increased, then I’m not sure why we’re even discussing how high quality the EIP is. Clearly increasing contract sizes is not going to fix the obvious short coming of the current Ethereum configuration. Clearly whether the EIP is high quality doesn’t matter as to how to prioritize the proposal within the set of tasks that are needed to be done to fix the current protocol’s short comings.

I’ve been thinking through some practical implications of EOF’s removal of capped-gas calls, and it seems problematic for common patterns that rely on “push” Ether transfers.

Consider a common auction pattern:

function bid(uint256 id) external payable {
    Auction storage auction = auctions[id];

    address previousBidder = auction.latestBidder;
    uint256 previousValue  = auction.latestBid;
    address bidder         = msg.sender;
    uint256 value          = msg.value;

    if (value < _currentBidPrice(auction)) revert MinimumBidNotMet();
    if (block.timestamp > auction.endTimestamp) revert AuctionNotActive();

    auction.latestBid    = uint112(value);
    auction.latestBidder = bidder;

    (bool success,) = payable(previousBidder).call{value: previousValue, gas: 60_000}("");

    if (!success) {
       balances[previousBidder] += previousValue;
    }

    emit Bid(id, value, bidder);
}

In this snippet, the call to previousBidder uses a fixed gas limit (gas: 60_000) to ensure the bidder can run minimal logic without significantly impacting the rest of our transaction. This pattern is widely used today to prevent the recipient from consuming too much gas and breaking subsequent logic in the calling contract.

This pattern isn’t possible under EOF as explicit gas limits are removed, giving the callee nearly all available gas.

How does EOF intend to handle scenarios like this? One possible answer is that push-based withdrawals would no longer be safe or recommended—after all, even today, contracts must implement fallback logic in case transfers fail. Still, it’s useful and practical for contracts to be able to reliably react in minimal, controlled ways when receiving Ether.

Is it truly feasible or desirable to eliminate this capability protocol-wide?

1 Like

You provide a long list of questions and assertions and are now upset they were answered in turn? If we replied with one liners would you have then complained that we didn’t think deeply enough about the issues?

Go listen to ACD192 where we spent most of the meeting discussing it. It includes the opinion of the lead of Solidity where he endorses EOF as what he wants “by orders of magnitude”. And at the end of the meeting Marius agreed to disagree and commit. My interpretation of his concerns was that there were many unanswered questions in his mind, but they all did have answers and had been thoguht through, as we discussed on that call.

And I do not view this gambling ethereum’s core. Solidity’s comment indicates to me we are shoring up a sandy foundation rather than applying more duct tape and balling wire to an insufficient solution. While some of these problems can be fixed in the compiler we need to reflect on the question of whether or not that is the right place to fix it. A fix in the wrong layer can cause more damage.

There are fixes that need to be done to the VM that take it up to more modern standards, rater than the 1950’s “stream of bytes” standard we have had. The benefits in a container and other features like stack validation will enable compilers to provide even better utility than they do now, much more so than the compiler working alone with an outdated model. I respect that we have differing opinions on this but I stand with Daniel’s assertion in ACD 192 that this is the correct solution for the compiler and the layers below it.

8 Likes

I feel like my underlying point (the forest) is being missed for the specific technical details of my concerns (the trees). My point is that there is complexity and burden being foisted on the chain and on smart contract developers without immediate benefit. To crib a cynical phrase from American politics, all of these future benefits are nice, “but what have you done for me lately?”. That there are any opcodes that are removed/broken, and consequently that there are some design patterns that are no longer possible is a VERY big stick. Where is the corresponding carrot?

Regardless, I will respond point-by-point:

In the absence of a technique (like CREATE2) to verify that the code at an address was derived from trusted initcode, we want to know whether the *CALL that we’re about to make is executing the code we expect. We can’t do what Solana does and directly read the state of foreign contracts, so we have to resort to trusting the code in that address to report directly on its state.

To give a concrete example: Curve V2 Tricrypto NG factory pools are CREATE’d (not CREATE2) by a factory. If we want to know if an arbitrary pool is a pool, we need to check its bytecode. I acknowledge that there are probably other ways to skin this cat, but checking the code is an expedient solution.


Alternatively, maybe we want to do what Vyper does and store some arbitrary data in another address, load it into memory and treat it as initcode (or other arbitrary data) to work around the contract size limit.

https://etherscan.io/address/0x0000000000001ff3684f28c67538d4d072c22734#code#F7#L26

In general, the pattern that is not not supported is performing a call that you expect to revert, but to have “enough” gas to still perform some useful computation. You might say “well, just assert that the revert reason/returndata is non-null”, but sometimes you don’t have that degree of flexibility when interoperating with contracts that were written by a 3rd party.


Another application of this pattern is calling some 3rd-party contract in order to see what it does, then reverting with some machine-readable summary of that behavior. This achieves a kind of pseudo-STATICCALL and is completely dependent on the ability to discover whether a call reverted due to out-of-gas or some other reason.

Similarly, you may wish to call some 3rd-party contract that represents the agent of some entity to gracefully wind down a position, forwarding a “reasonable” amount of gas (e.g. a proportion of the block gas limit). Then if that fails to gracefully wind down the position, you forcibly liquidate. If we can only forward 63/64 of the gas, there’s no way to ensure that the “liquidation” portion of the operation has enough gas if the “wind down” portion refuses to participate. (I regret that I cannot give you a specific address where this code lives; the protocol that I’m referencing never launched.)

As you acknowledged, none of these are benefits realized immediately after the proposed Osaka hardfork. And some of these benefits accrue to smart contract/application developers only indirectly. Enablers and quirk-removers don’t make the lives of smart contract developers easier. Improving the tooling and developer experience is certainly a boon, but EOF is not the only way of achieving this (going back to @xrchz 's point about being sure to distinguish between “requires” vs “enables”).

To be abundantly clear here: I’m not saying that I don’t see or that I disagree with the value of EOF. Clearly improving/versioning the EVM is something that is needed for Ethereum to achieve its mission. But as a smart contract developer, because EOF causes problems for me with no corresponding benefit, I will not adopt it at launch.

2 Likes

The purpose of EOF is inherently long-termist. If you fundamentally disagree with that approach, I suppose that is fine but it is hard to have technical arguments around that.

The forced conversion problem this solves is an extremely powerful unlock.

Nor is moving the token price something that can be considered, aided, or even addressed by an EIP.

3 Likes

By the way I checked the old threads and I think the examples that were given at the time to argue against the gas savings of EIP 2315 may have actually been inaccurately based on an old draft of the EIP. The latest gas values in the EIP are 5 gas for JUMPSUB and 3 gas for RETURNSUB – exactly the same as CALLF and RETF. So I don’t think it makes sense to use the arguments based on “gas benefit” or “compiler benefit” against EIP-2315, since they apply equally if not more to EOF. EOF not only has the same gas costs, it is also structurally more restrictive, blocking optimizations that are conversely available via EIP-2315, and requiring substantially more complexity on the compiler side.

For reference, our POC for EOF in Vyper took nearly 1k lines of code to be EOF-compliant. And I’m not that familiar with the Solidity codebase, but it looks like the EOF POC was 14,000 lines (Implementation of EOF in Solidity [DO NOT MERGE] by rodiazet · Pull Request #15294 · ethereum/solidity · GitHub). Whereas for EIP-2315, I can’t speak for Solidity but at least on the Vyper side, EIP-2315 is a 10-20 line change.

Solidity has a EOF version that can you used today. So immediately after the hardfork SC devs won´t be plagued by stack too deep errors anymore.

EOF brings immediate cost reductions. For example for Uniswap v3 deployment costs decrease by 14% and costs for calls by 9% [1] All of that without any changes to the Solidity code.

How are those not immediate benefits?

[1] EOF Explained: What Developers Need to Know | by BuildBear Team | BuildBear Labs | Medium

3 Likes

Dapp dev here: Contract deployment costs are completely irrelevant nowadays. They are basically free on L2s and on Mainnet they’re also basically free because gas has been averaging 1gwei for the entire February.

As a contract developer you can often wait for a period when gas is cheaper to deploy a contract too. Also going from 14% to 9% really does not matter in my day to day. The core costs to producing production-ready Solidity smart contracts are the cost of the audit, the cost of the Solidity dev (have you seen their salary?), how long it takes to find one, etc. Deployment cost is completely irrelevant

1 Like

This is not what’s standing in between me and shipping products that users desire. Also EOF is a solution to this problem, but not the solution to this problem. A smarter compiler (looking at you, Vyper) also solves this problem.

Like @TimDaub said, deployment costs basically don’t matter. But 9% decrease in runtime gas is pretty neat! I recant my earlier statement that there is no benefit to smart contract developers immediately on the hardfork. I think that’s still probably not enough benefit to counteract the additional pain/burden caused by the opcode changes, but I appreciate that things are moving in a beneficial direction. :+1:

My application is more sensitive to gas consumption than most, so “gas lower” is a better reason for me to adopt EOF than it is for most projects. But I reiterate my first point: this is not what’s standing in between dApp developers and shipping products that users deesire.

Re the 14K line diff - did you stop to fact check what that is? It’s 2x Uniswap files which are in not-merged PR just for code size and gas price impact check. They are not in main repo. Moreover most of the changes/additions are in test directory. Solidity has a lot of semantic, syntax a many other tests which were updated. I’m leaving your homework for you to calculate how big was the change. I’m not going to do this. Sorry.

3 Likes

I started that sentence by saying that I’m not that familiar with the Solidity codebase. Anybody can see that it is unmerged, it’s in the text of the link. I just found the PR which looked indicative of the effort. If you want to provide updated numbers that you think are more accurate on the Solidity side, I’m happy for you to do so. I brought it up in an effort to provide more diverse sources than just Vyper, although from your reaction, it sounds like maybe I should have just skipped that step altogether.

On the other hand though, I am familiar with the Vyper codebase, and my numbers there do stand. I don’t really think going through and adding up all the EOF-related commits in solc is going to change my point that EOF is substantially more complex for compilers to implement than EIP-2315. Again, I’m happy to be corrected here, though. If it turns out that EOF only takes 10 lines to implement in solc, that would be a very interesting data point.

2 Likes