The Effects of Ethereum's Upgrades on Smart Contracts

Category CyberSecurity

The effects of Ethereum's upgrades on smart contracts

Rougly every year, Ethereum undergoes an upgrade. This is a coordinated hard fork that contains new features. Most of these upgrades impact the blockchain layer, focusing on things like scaling and consensus hardening. However, these hard forks also often impact the application layer, this is when new features are exposed to smart contracts. Not all of these new features are backwards compatible and certain assumptions may break. As such, it is crucial to be aware of them when creating immutable smart contracts.

We'll go through the recent Ethereum hard forks, starting from The Merge, and explore their impact on the smart contract layer, with a focus on security. At the end we take a look forward and discuss potential future upgrades.

Paris (The Merge)

Paris, or 'The Merge' was the big fork where Ethereum transitioned from proof-of-work to proof-of-stake. This all happened seamlessly as the transition was carefully designed to have very little impact on smart contracts and their users, however, there were still some small changes:

  • EIP-4399: DIFFICULTY becomes PREVRANDAO. On the PoW chain the DIFFICULTY opcode was often used as a source of (bad) randomness. However, with the switch to PoS, difficulty no longer has a meaning as no more mining is being done. This results in the DIFFICULTY opcode being useless. To maintain compatibility with smart contracts using DIFFICULTY for randomness, it was decided to rename DIFFICULTY to PREVRANDAO and make it return the previous' block RANDAO mix. To implement this, Solidity deprecated block.difficulty and introduced block.prevrandao in version 0.8.18. Note: under the hood the same opcode (0x44) is still called, so block.difficulty in pre 0.8.18 Solidity will produce the same value as block.prevrandao in version 0.8.18 and higher.

    Just like DIFFICULTY, PREVRANDAO is not a source of true randomness. It can be predicted and biased to certain extents, so great care must be taken when using it as such. Refer to the EIP for a more accurate and complete description of PREVRANDAO's security assumptions.

    Additionally, not all networks implement PREVRANDAO equally. For example, Optimism lets the sequencer set the value of PREVRANDAO every block, giving weaker security guarantees. On Arbitrum PREVRANDAO will always return '1', which should obviously not be used for randomness.

  • The timestamp of a block is more deterministic. Because mining is probabilistic, the time between blocks on the PoW chain was not constant. However, on average a block was produced every 13 seconds. In contrast, the beacon chain has a slot (i.e. an opportunity to produce a block) exactly every 12 seconds. As such, after the merge block.timestamp may only increase with multiples of 12. This may impact contracts that rely on block.timestamp. Of course, this is only the case for Ethereum mainnet, other networks have different block times.

Shapella

Shapella's big feature was withdrawals, but it also introduced other goodies such as PUSH0:

  • EIP 3855: PUSH0 is a new opcode that pushes '0' to the stack. Even though you will not interact directly with PUSH0 when writing or reviewing smart contracts in pure Solidity, it is still important to be aware of its existence and usage. Starting from version 0.8.20 Solidity will use PUSH0 by default, meaning that any compiled contracts may contain PUSH0. When deploying the compiled contract it is important to check that the target network supports PUSH0, otherwise execution will revert when a PUSH0 instruction is encountered.

Fortunately, Solidity generates a PUSH0 instruction in the deployment code. This means that if the target network does not support PUSH0 a revert occurs at deployment time, not at a later stage, when funds could potentially be frozen.

If your target network does not support PUSH0 it is recommended to either lower the Solidity version to 0.8.19 or earlier, or drop the target EVM version to Paris or earlier.

Dencun

Dencun implemented danksharding alongside several smaller features:

  • EIP 4844: Danksharding brought two new opcodes and one new precompile. BLOBBASEFEE and BLOBHASH are exposed in Solidity through blobbasefee() and blobhash() respectively, starting from version 0.8.24. As you would expect, blobbasefee() returns the base fee for blobs in this block. blobhash() returns the versioned hash of the KZG commitment of the blob with the given index in this transaction. If the given index does not exist then zero bytes are returned, similar to blockhash()'s behaviour.

The new 'point evaluation' precompile allows you to verify a point in a blob. In addition, the precompile returns the number of elements in the blob and the BLS modulus. Currently, Solidity offers no way to call this precompile at a high level, as such a low level call to the precompile address (0x0a) must be used. When doing so, ensure that the precompile has been deployed on the target network, else the call will succeed by default.

  • EIP 5656: MCOPY is a new opcode introduced to optimize certain operations, similar to PUSH0. Solidity will use it by default starting from version 0.8.25. As such, ensure the target network supports it, else execution will revert when MCOPY is encountered. In contrast to PUSH0, MCOPY is not used in Solidity's deployment code by default. This means that deployment may succeed but other operations may fail, which could lead to frozen funds.

  • EIP 1153: TSTORE/TLOAD are new opcodes giving access to transient storage. These are currently only accessible with inline assembly in Solidity. When using tstore() or tload(), ensure the target network supports them. Additionally, it is important to understand the lifetimes in play: transient storage is only cleared at the end of a transaction. When using transient storage for re-entrancy locks, this means that you still need to manually clear them at the end of a call. Otherwise, a user will not be able to have multiple interactions with your contract in a transaction.

  • EIP 6780: SELFDESTRUCT is modified such that if it is not executed in the same transaction as the contract was created in then the contract is not deleted, but all ETH balance is still transferred. If SELFDESTRUCT is executed in the same transaction as the contract was created in, then behaviour remains the same and the contract is deleted in addition to all ETH balance being transferred. This change may break contracts currently relying on SELFDESTRUCT, and is important to be aware of when creating a contract that uses SELFDESTRUCT. Regardless, SELFDESTRUCT remains deprecated and should not be used.

Future

These features are not live yet, but may be implemented soon. As such, it is useful to be aware of them.

  • EIP 3074 or 7702 enhance the UX of EOA's by giving them smart contract capabilities (i.e. the ability to execute code). This breaks the assumption that if msg.sender equals tx.origin then the caller must be an EOA and not a smart contract. As such, this check can no longer be relied upon.

  • EOF is a big update that adds structure to EVM bytecode. Most of these changes should be handled by Solidity under the hood. However, some aspects are still visible to developers:

    • Gas observability is removed. This means that in EOF contracts, gas() is not available and it is not possible to specify a gas limit for a call.
    • Similarly, code observability is removed. Meaning that it is not possible to copy a contract's code into memory, nor get the size or hash of a contracts code from EOF contracts. Observing the code of an EOF contract from a legacy contract is still possible but with an important caveat: it works as if the EOF contract's code is EF00, for example EXTCODEHASH of an EOF contract returns keccak256(0xEF00).
    • delegatecalls from EOF code to legacy code are not allowed and will revert. The opposite direction, from legacy code to EOF code is allowed.

    If your application relies on this functionality you will still be able to use legacy (non-EOF) contracts since these maintain their current behaviour.

    Make sure to check out the EOF spec for more details. You'll want to have a snack on hand though, it's a big one!


Ethereum's upgrades often bring exciting new features and possibilities. Unfortunately, they sometimes change behaviour and break assumptions, often in non-obvious ways. It is important to be aware of these, both as a smart contract developer and as a security engineer to create robust and secure applications.