10 EVM Design Mistakes

And how not to repeat them again

Mikhail Vladimirov
Coinmonks
Published in
8 min readDec 10, 2020

--

Ethereum blockchain platform made really exciting progress since its start five years ago. It became cradle and home for many awesome things, such as DAOs, ICOs, DeFi, etc.

EVM (Ethereum Virtual Machine) is at the core of the Ethereum ecosystem. It is a distributed computer that executes the smart contract, enforcing code is the law. However, EVM itself suffers some long-standing problems originated from design mistakes made at early stages. As a professional smart contract developer and auditor, I see the consequences of these mistakes over and over again.

Here is my collection of 10 EVM design mistakes.

1. SELFDESTRUCT (SUICIDE)

EVM has SELFDESTRUCT opcode (formerly known as SUICIDE), which basically wipes out the contract that executed SELFDESTRUCT opcode. This means that the contract’s byte code and storage are deleted, and the contract’s ether balance is transferred to the address provided as a parameter to SELFDESTRUCT opcode.

The idea probably was to clean the blockchain state from the contracts that are not needed anymore. However it doesn’t work this way, as a particular contract may only be destroyed by itself, but usually, a contract doesn’t know whether it is still needed or not.

However, the problem with SELFDESTRUCT opcode is not that it doesn’t do what is was supposed to do, but rather that it breaks two important principles of Ethereum: it makes contract’s byte code mutable (deleting byte code means changing it) and it makes it possible to send ether to a contract without executing contract’s byte code.

Remember Parity Multi-Sig Wallet Self-Destruct? People lost access to crypto assets worth millions of dollars because a single smart contract destructed itself.

While it is not possible to remove SELFDESTRUCT from EVM, it is possible to discourage using it and make the corresponding predefined function in Solidity deprecated.

2. Limited Word Width

EVM is a 256-bit virtual machine, which means that it operates with fixed-width 256-bit words. When the operation result doesn’t fit into 256 bits, the higher bits are just dropped, which is known as overflow. This is a dangerous situation, as the operation may produce a result that is mathematically incorrect.

For hardware architectures, fixed-width overflowing words is a natural decision, as operations on such words could be implemented with a fixed number of logical gates and executed in a constant number of ticks. However, hardware architectures usually provide some way to know whether overflow did happen and even to obtain extra bits that didn’t fit into the result word.

For low-level programming languages, fixed-width overflowing data types is a natural decision because they map 1:1 to underlying hardware words thus providing maximum performance.

However, as mainstream architectures nowadays are only 64-bit, EVM’s 256-bit words don’t map to hardware words and thus have any way to be implemented as big integers, i.e. software-emulated arbitrary-width words.

Taking this into account, the natural decision for EVM would be to use arbitrary width non-overflowing words rather than fixed-width overflowing ones. This would eliminate the infamous overflow problem as well as made many things much simpler.

While it is not possible to change architecture now from fixed-width to arbitrary-width words, it is still possible to introduce precompiled smart contracts for efficient big-integer operations.

3. Stack Too Deep

Usually, EVM opcodes take arguments from the top of the stack and put the result back there. However, there are opcodes such as DUP1, DUP2, …, DUP16, and SWAP1, SWAP2, …, SWAP16 that allow accessing deeper stack elements. However, this random stack access is limited to only 16 topmost elements.

This limitation makes Solidity output infamous “Stack Too Deep” error when the value to access is too deep in the stack. It is up to the Solidity compiler how to arrange values in the stack, thus it is impossible to predict when this error will pop up next time. A common recommendation is just not to use too many local variables and function arguments, which is ridiculous. This makes the correctness of a Solidity program depend on optimization techniques used by a compiler.

It is possible to fix this problem by introducing generic DUP and SWAP opcodes that would take stack slot depth from the stack as a normal parameter.

4. Separated Memory Spaces

EVM memory is split into several spaces, being accessed via differnet opcodes. These spaces are: i) normal memory, accessed via MLOAD, MSTORE, and MSTORE8; ii) stack, accessed via DUP<n>, SWAP<n>, and many other opcodes, iii) call data, accessed via CALLDATALOAD, CALLDATASIZE, and CALLDATACOPY; iv) return data, accessed via RETURNDATASIZE and RETURNDATACOPY; v) byte code, accessed via CODESIZE and CODECOPY.

As long as all these memories use different opcodes, it is not possible to have a generic C-style pointers in EVM, that could point to any data, regardless of what kind of memory it is stored in. Solidity tries to address this by introducing different flavors of pointers, like “memory” and “calldata”, but it still doesn’t support “stack”, “code”, or“returndata” pointers. Also, such approach doesn’t solve the problem completely, as these pointers are not convertible to each other. If a function accepts “memory” pointer, but we need to pass it some data stored as literal in the code, or just returned to us by an external call and still residing in return data, or passed to our contract from outside and sitting in calldata, or just stored in a local variable on stack, then we need to first allocate memory and copy our data into this region, which is suboptimal.

5. CREATE2

CREATE2 opcode was introduced recently, and was supposed to allow a contract to reserve addresses for future child contracts, but only create these child contracts when needed.

The idea was that such child contract’s byte code has to be known in advance in order to reserve an address, but these child contracts become immutable even before being created. Unfortunately, together with already existing CREATE and SELFDESTRUCT opcodes, this new CREATE2 opcode made it possible to replace byte code of a deployed contract with arbitrary new byte code, while preserving contract’s address. This breaks the Ethereum principle of contract’s immutability.

In some cases it is even cheaper to store and update data in a byte code of a contract, rather then in contract’s storage.

6. Silent Arithmetic Errors

By arithmetic errors here we mean situations when operation produces mathematically incorrect result, i.e. overflows, underflows, and divisions by zero that return zero in EVM.

Mainstream hardware architectures offer some ways to know whether operation returned mathematically correct result, or not, but EVM doesn’t provide such functionality. This makes it hard and gas-expensive to do math safely.

The problem could be solved by introducing a new opcode that will obtain status flags of the last executed opcode.

7. Immutable Byte Code

Contract’s byte code is immutable. This is one of the core Ethereum principles, unfortunately, broken by SELFDESTRUCT opcode, and even more broken by CREATE2 + CREATE + SELFDESTRUCT opcodes combo. But at least for contracts that don’t use SELFDESTRUCT byte code stored on-chain is immutable. This means that one may study the byte code before interacting with the contract, and be sure that the code will not change between it was studied and was interacted with.

However, in some cases, it would be convenient for a contract to modify its own byte code in memory at run time, without saving these modifications on-chain. Such modifications would not break the immutability principle, as code stored in blockchain will remain unchanged, but will make it possible for contracts to modify their code or even generate new code on the fly based on the input, thus reducing gas cost.

Currently, EVN forbids this, but it is still possible to new opcodes to make this possible.

8. Lack of Extensibility

There are two common ways how new functionality is being added to ENV: by introducing new opcodes and introducing new precompiled contracts. There are no strict rules on what method to use for each particular case. Keccak256 hash is implemented as an opcode (SHA3), while SHA256 and HIP160 hash functions are implemented as precompiled contracts. Both ways require hard forks and both ways are not 100% backward compatible, as they may affect the behavior of already deployed contracts.

Also, low-level opcodes, that operate purely within EMV, i.e. affect only stack, memory, and instruction pointer, are intermixed with high-level opcodes that deal with blockchain state.

EVM should have some proper way of adding new features such as a special SYS opcode used to perform a system call, i.e. call some external functionality. It also should clearly separate low-level opcodes that are independent of blockchain state structure, and high-level system calls that interact with that state.

9. Too Wide Words

While limited words width problem was already mentioned, the separate problem is that this width limit is … too wide. 256-bit words are much wider than 64-bit words natively supported by mainstream hardware. This makes operations with such words expensive, even is actual values are small. Dealing with booleans, chars, array indexes, and other small numbers consumes quite much gas: namely the same as would be consumed in case the number were large.

One way to fix this would be to have opcodes that operate on lower 64 or even 32 bits of the arguments.

10. No Access To Other Contract’s Storage

In Ethereum, the contract’s storage is public information, but not for other contracts. When a contract didn’t provide a getter function for some valuable information kept in the contract’s storage, one may still read this information off-chain. Just need to figure out the storage slot address. But other contracts cannot do this. Currently, contract authors try to declare almost every storage variable as public, just in case its value will ever be needed in other contracts, as it is not possible to make the variable public after the contract was deployed.

The problem could be easily solved by introducing EXTSLOAD opcode, that reads from the storage of another contract.

The problems described above is what I collected developing and auditing Ethereum smart contracts for about 4 years. These problems are not fatal and could be worked around, however, most of them could be fixed quite easily and at least some of them definitely worth fixed.

If you know something that should be added to this list, let me know.

Also, Read

Get Best Software Deals Directly In Your Inbox

--

--