I had a lot of fun writing about [how the constant keyword actually works](https://figtracer.com/Public/-+My+Articles+-/Web3/How+does+the+constant+keyword+actually+work%3F) so I decided to unravel how the *immutable* keyword works! > I recommend reading [how the constant keyword actually works](https://figtracer.com/Public/-+My+Articles+-/Web3/How+does+the+constant+keyword+actually+work%3F) 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](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 Immutable | With Immutable | | ------ | ----------------- | -------------- | | 0x0000 | PUSH1 0x80 | PUSH1 0xA0 | | 0x0001 | PUSH1 0x40 | PUSH1 0x40 | | 0x0002 | MSTORE | MSTORE | [Read about Solidity's memory layout](https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html) 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!