10 EVM Design Mistakes
And how not to repeat them again
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)
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
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
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
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
MSTORE8; ii) stack, accessed via
SWAP<n>, and many other opcodes, iii) call data, accessed via
CALLDATACOPY; iv) return data, accessed via
RETURNDATACOPY; v) byte code, accessed via
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.
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
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
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.
- The Best Crypto Trading Bot
- Deribit Review | Options, Fees, APIs and Testnet
- FTX Crypto Exchange Review
- Bybit Exchange Review
- The Best Bitcoin Hardware wallet
- Crypto Copy Trading Platforms
- The Best Crypto Tax Software
- Best Crypto Trading Platforms
- Best Crypto Lending Platforms
- Ledger Nano S vs Trezor one vs Trezor T vs Ledger Nano X
- BlockFi vs Celsius vs Hodlnaut
- Bitsgap review — A Crypto Trading Bot That Makes Easy Money
- Quadency Review- A Crypto Trading Bot Made For Professionals
- PrimeXBT Review | Leverage Trading, Fee and Covesting
- HaasOnline Review and Get a 10% discount
- Ellipal Titan Review
- SecuX Stone Review
- BlockFi Review | Earn up to 8.6% interests on your Crypto
- Best Crypto APIs for Developers
- Best Blockchain Analysis Tools
- Crypto arbitrage guide: How to make money as a beginner
- Top Bitcoin Node Providers
- Best Crypto Charting Tool
- What are the best books to learn about Bitcoin?