MIP 3 - Linear EVM memory cost

MIP3 - Linear EVM memory cost

This proposal reprices EVM memory expansion from a quadratic cost model to a linear one. To bound total memory usage within a block, each transaction is explicitly limited by its peak memory usage. The motivation is to better align EVM gas costs with real resource usage and to introduce an explicit invariant on total memory usage at the block level.

Any feedback or discussion is appreciated on:

  • the proposed linear memory cost model
  • the explicit per-transaction memory limit
  • potential DoS concerns
  • developer ergonomics and usage concerns
3 Likes

Hello, I have questions to the design and spec:

1. How does the call which exceeds memory exit? revert or exceptionally halt? If revert, does the gas to extend memory get charged before reverting? and other instruction gas charges (esp. *CALL)? exceptionally halt is more consistent with current OOM behavior (out-of-gas), but revert is more friendly. The spec says “revert” but I want to confirm that’s what was meant and spec out the fine details.

2. For all calculation related to memory limit, the memory size isn’t rounded up to the nearest word, i.e. if caller frame allocates `limit - 1byte` memory, callee frame can still allocate `1byte` of memory? This is in the spec, but want to confirm.

  1. Can we expand discussion with EIP-7686 and EIP-7923 in the Rationale section, i.e. why is MIP-3 chosen over them? (my understanding is that EIP-7686 would combine unfavorably with Monad’s “charge-gas-limit” rule, what about the paging from EIP-7923?)
  2. Similar to EIP-7923, behavior of MSIZE after the activation should be confirmed (esp. given the lack of rounding to word in memory limit calculation.) In other words - would MSIZE return limit or limit - 1byte in the case mentioned in (2.)?

Some Responses:

  1. Exceeding the memory limit causes the current call frame to revert rather than halt. One rationale for this decision is compatibility with ERC-4337 bundlers for the following reason: if exceeding the memory limit resulted in an exceptional halt a transaction could DOS erc4337 bundlers by using the memory limit. If compatibility with erc4337 is not necessary then this can be changed to halt.
  1. Expansion cost remains in terms of 32 byte word expansion. This will be defined more explicitly in the MIP.

  2. EIP-7686 and EIP-7923 are designed for ethereum execution specifically. For example in EIP-7686, it defines a strict bound on memory expansion via gas. This defines the amount of expandable memory to be linearly dependent on the amount of gas forwarded in the call. This is not compatible to the constraints of Monad execution. As it would cause a higher memory footprint per transaction.

  3. MSIZE semantics are unchanged. It returns the size of active memory in bytes. So the case where caller frame allocates limit - 1byte memory is rounded up to the limit memory. The child call would not have any remaining memory to allocate.

Do you have some specific new incompatibility which MIP3 would introduce if it exceptionally halted on OOM? Since the OOM is local to the call frame, it would have the same effect as calling Op.INVALID, so I don’t see memory limit giving more opportunity to DOS.

Got it, thx for confirming, I think that is unambiguous in the MIP3 :+1: , I was just making extra sure

Got it, I think including in the Rationale section would be nice for posterity. What about the latter EIP-7923, can you expand on how did it not fit our case, was it just the added complexity?

Got it, and to be extra clear, if caller allocates 1 bytes, MSIZE returns 32. If caller calls callee which calls MSIZE, the inner MSIZE returns 0. That on one hand makes sense, on the other it might provide lesser utility to those who would like to use MSIZE to check how much more mem they can use.

Do you have some specific new incompatibility which MIP3 would introduce if it exceptionally halted on OOM? Since the OOM is local to the call frame, it would have the same effect as calling Op.INVALID, so I don’t see memory limit giving more opportunity to DOS.

It would cause bundles to fail if a subcall used all the memory and the transaction was halted. This does not seem desirable if you are running a bundler.

Got it, I think including in the Rationale section would be nice for posterity. What about the latter EIP-7923, can you expand on how did it not fit our case, was it just the added complexity?

Benchmarks and rationale in EIP-7923 were not reproducible.

Got it, and to be extra clear, if caller allocates 1 bytes, MSIZE returns 32. If caller calls callee which calls MSIZE, the inner MSIZE returns 0. That on one hand makes sense, on the other it might provide lesser utility to those who would like to use MSIZE to check how much more mem they can use.

I think your point is that you would like a way to check how much memory could be used in child calls. I dont think an opcode is necessary for this corner case. Memory usage becomes more predictable.

MSIZE is meant to measure memory allocation within the current context. Not across the call stack. One could imagine an opcode that reports memory usage across all active call frames. In the current quadratic model, MSIZE already partially serves this purpose: the quadratic cost implicitly defines an upper bound on memory usage. the nonlinearity of this cost makes predicting memory usage across calls difficult. In the proposed linear model, memory usage in terms of gas becomes more predictable. This reduces the need for such an opcode.

The only practical use case for this hypothetical opcode would be dynamically measuring memory so that subcalls consuming large amounts could branch or revert intelligently. In practice, the only scenario where this matters is a contract deliberately allocating excessive memory to sabotage subcalls. Such contracts would naturally be avoided, making the opcode’s benefit negligible.

Note that checking ethereum history for memory usage, the peak amount of memory of a transaction was observed to be ~2MB and the average was ~2kb.

Maybe I should rephrase. To me the choice here is between:

  1. A callee frame exceeding limit has same effect as calling REVERT(0, 0), unused gas is returned to caller (current design)
  2. A callee frame exceeding limit has same effect as calling INVALID. Unused gas of that callee frame is consumed

I’m not considering halting or reverting the entire transaction, apologies if my earlier post sounded like that. In both cases the caller frame continues and the callee’s memory allocation is freed. A subcall should in neither case affect the bundle any more than possible today, does this make sense?

A follow-up on the revert vs exceptional halt topic. Assuming we’re on the same page per my recent reply :backhand_index_pointing_up: , I think it’s better to keep reverting (returning unspent gas to caller, like you have it specced currently). Consuming unspent gas would correspond with a bug in the contract code in question (like calling INVALID or running a static mode violation). Running OOM is not 100% up to the code in question (but also up to parent frames), so consuming all gas is not warranted.

That being said, if we revert and return gas to the caller, there is a question of what does “unspent” mean if OOM happens in a CALL opcode, since gas gets charged for various stages of setting up the CALL. I would recommend the memory check and potential OOM-revert happen after gas charges, i.e. cold access, memory expansion, value transfer gas charges all happen before (EDIT: not “after”). This makes it harder for us to leave an opcode which undercharges if it goes out of memory.

On a similar note, let the static mode check happen after memory check, wherever it applies (CALL, LOGn, CREATEn)

I see what you are referring to now.

This makes sense for pricing the resources correctly. Thanks for this feedback. I will spec/think more about this and post updates.

From a smart contract developer’s perspective, I think the linear memory pricing model can significantly improve gas cost predictability, especially for contracts that rely on larger memory allocations.

One operational concern I have is on the RPC side. Since memory expansion becomes cheaper up to the 8MB cap, memory-heavy transactions may become more common.
Is there any estimated or recommended guideline for RPC operators in terms of memory-aware concurrency (e.g. parallel execution or eth_call limits) to avoid potential OOM issues?

To follow up, the following updated spec addresses this topic:

  1. The memory expansion gas model is updated to charge num_of_words // 2.
  2. Peak memory usage across all call contexts is capped at 8 MB.
  3. If a memory access exceeds the 8 MB limit, the call terminates with OutOfGas. The immediate call context fails, and no gas is returned to the parent call.
1 Like

A specific guidelines will be updated for RPC operators. This is being discussed. However, there should not be necessarily a large change. If concurrency is limited to 64 transactions then the max memory usage on the evm side is roughly limited to be 512 mb. Max memory usage would then be calibrated on a per box basis.

1 Like

This reverting, rather than halting, definitely removes a DOS vector. Now the impact is similar in size to traditional just-enough-gas-targeting methods that would cause selective reverts in child calls. These are pretty well understood at this point. This adds the additional wrinkle that the upcoming memory revert is undetectable, however I’ve never seen someone check gas amounts remaining before making a call in the wild, so I don’t think it will change the status quo.