I had a lot of fun writing about how the constant keyword actually works so I decided to unravel how the immutable keyword works!

I recommend reading how the constant keyword actually works before starting this article.

I’m going to use the same tool (Soler) that I built to understand these EVM nuances and get a better grip on what we’re actually building on top of.

Understanding immutable

In Solidity, variables marked as immutable can be assigned a value at construction time. The value can be changed at any time before deployment and then it becomes permanent.

One additional restriction is that immutable variables can only be assigned to inside expressions for which there is no possibility of being executed after creation. This excludes all modifier definitions and functions other than constructors.

Unlike constant variables, which are hardcoded into the bytecode at compile time, immutable variables are set during contract deployment and then “locked”. This makes them useful for values that need to be determined at deployment time (e.g., an address or a configuration parameter) while still saving gas compared to regular storage variables.

This is our example contract Immutable.sol:

contract ImmutableTest {
	uint256 public immutable zk = 2;
}

Bytecode Overview

The bytecode consists of two parts:

  • Creation Code (constructor): Executed once during deployment to initialize the contract and return the runtime code.
  • Runtime Code: The code stored on-chain and executed during transactions.

With solc --bin <contract.sol> we can check the bytecode for both: with and without immutable.

Without the immutable keyword:

608060405260025f553480156012575f5ffd5b5060ac80601e5f395ff3fe6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063c91c1f5714602a575b5f5ffd5b60306044565b604051603b9190605f565b60405180910390f35b5f5481565b5f819050919050565b6059816049565b82525050565b5f60208201905060705f8301846052565b9291505056fea264697066735822122022c9e4f3762ad7d36040f0648ad69d86c6eda803f584905fb8ae6ebb6c9ec9af64736f6c634300081c0033

With the immutable keyword:

60a060405260026080908152503480156016575f5ffd5b5060805160cb61002c5f395f6046015260cb5ff3fe6080604052348015600e575f5ffd5b50600436106026575f3560e01c8063c91c1f5714602a575b5f5ffd5b60306044565b604051603b9190607e565b60405180910390f35b7f000000000000000000000000000000000000000000000000000000000000000081565b5f819050919050565b6078816068565b82525050565b5f602082019050608f5f8301846071565b9291505056fea2646970667358221220efd2afa7b5564e10f6b261be2eab6245d3cf5a7a1ab56ac470d238bab6394f3e64736f6c634300081c0033

At first glance, we can see that the bytecode with immutable is visibly larger than the bytecode without it. This may seem contradictory with the idea that immutable makes us save a lot of gas, but we’ll see that it’s an investment.

Inside the EVM

Let’s extract the opcodes displayed on the image:

  • Without immutable:
0x0005:  PUSH1 0x02   // Push the value 2 (zk)onto the stack
0x0007:  PUSH0        	// Push the value 0 onto the stack
0x0008:  SSTORE      // Store 2 in storage slot 0

This code stores 2 in storage slot 0 during deployment, making zk a regular storage variable.

zk is then retrieved using SLOAD in the getter (created since it’s a public variable):

0x0063: PUSH0        // Push slot 0x00
0x0064: SLOAD        // Load value from storage

It costs ~2100 gas per SLOAD (there’s a nuance here, check SLOAD at evm.codes) which is quite expensive.

  • With immutable:
0x0005:  PUSH1 0x02    // Push 2 (zk) onto the stack
0x0007:  PUSH1 0x80    // Push 0x80 onto the stack
(...)
0x000b:  MSTORE       // Store 0x02 at memory location 0x80

Then, at runtime, the getter directly loads the value using PUSH32:

PUSH32 0x0...2

It costs ~3 gas per PUSH32 which is really cheap compared to 2100 gas for SLOAD.

Conclusions: Instead of using SSTORE to write to storage, the constructor uses MSTORE to temporarily store the value in memory during deployment. The runtime bytecode is then modified to include a PUSH32 instruction that retrieves the value without needing storage.

As we can see, although immutable variables save gas at runtime, we have to make an investment at deployment. This is because additional MSTORE operations are required during deployment, and PUSH32 instructions take up more space in the final bytecode. However, the savings in execution gas make this a worthwhile trade-off.

Cool thing

Another cool thing we can see right at the start are these instructions:

Without ImmutableWith Immutable
0x0000PUSH1 0x80PUSH1 0xA0
0x0001PUSH1 0x40PUSH1 0x40
0x0002MSTOREMSTORE

Read about Solidity’s memory layout The address 0x40 is the FMPA (Free Memory Pointer Address) and points to the NFMA (Next Free Memory Address) which is where Solidity can write to.

On the left side (without immutable), it will point to SNFMA (Starting Next Free Memory Address - 0x80) which is where free memory usually starts.

On the right side (with immutable), the NFMA isn’t the SNFMA, but rather, 0xA0 instead! Which is exactly 32 bytes of difference from the usual SNFMA. With immutable variables, the constructor uses this memory to compute values, and the NFMA shifts as memory is allocated.

If instead of 1 immutable variable, we had 2 immutable variables, the NFMA (Next Free Memory Address) would be 0xc0… and so on.

How the EVM Enforces Immutability

You may be asking: how does the EVM enforce that an immutable variable can only be written to, one time?

The EVM enforces immutability by not associating an immutable variable with storage at all. When a contract with immutable variables is deployed, the constructor runs once, during deployment, to put the value into memory. Instead of storing the value in a storage slot (SSTORE). This ensures that the immutable variable exists in memory only during deployment and is not stored in a modifiable state.

Unlike constant variables, which are hardcoded into the bytecode at compile time, immutable variables are temporarily stored in memory during deployment. This means:

  • The value is not present in the contract’s raw bytecode before deployment. It is declared, but its value is only assigned at deployment.

  • The constructor modifies the bytecode to include the immutable value at a specific offset before storing the final contract on-chain

Conclusion

This article made me understand that immutable bridges constant and regular variables, being a key part of Solidity for smart contract developers who want to optimize their code!