--- name: immutable-constants description: Proper use of immutable and constant variables for gas savings and correctness. Use when deploying contracts with fixed configuration, optimizing gas, or deciding between constant, immutable, and storage variables. --- # Immutable & Constant Variables ## When to Use Each | Type | Set At | Stored In | SLOAD Cost | Use Case | |------|--------|-----------|------------|----------| | `constant` | Compile time | Bytecode (inlined) | 0 gas | Known at write time: math constants, hashes, selectors | | `immutable` | Deploy time (constructor) | Bytecode (appended) | 0 gas | Known at deploy: addresses, chain IDs, config | | Storage | Runtime | Storage slot | 2,100 cold / 100 warm | Must change after deployment | ## Constant Variables Must be known at compile time. Value is inlined everywhere it's used. ```solidity contract Constants { // Numeric constants uint256 public constant MAX_BPS = 10_000; uint256 public constant WAD = 1e18; uint256 public constant RAY = 1e27; uint256 public constant PRECISION = 1e18; // Hash constants — computed at compile time bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); // EIP-712 type hashes bytes32 public constant PERMIT_TYPEHASH = keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); // EIP-1967 slots bytes32 internal constant _IMPLEMENTATION_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); // Selector constants bytes4 public constant TRANSFER_SELECTOR = IERC20.transfer.selector; } ``` ### What Can Be Constant - Literal values (`uint256`, `int256`, `bool`, `address`, `bytes1`-`bytes32`) - `keccak256()` of literal values - Expressions evaluated at compile time - **Cannot** be `constant`: values depending on runtime state, `block.timestamp`, `msg.sender` ## Immutable Variables Set once in the constructor, stored in bytecode. Cannot be changed after deployment. ```solidity contract ImmutableConfig { address public immutable WETH; address public immutable FACTORY; address public immutable ORACLE; uint256 public immutable DEPLOYMENT_CHAIN_ID; uint256 public immutable CREATED_AT; uint8 public immutable UNDERLYING_DECIMALS; constructor( address weth, address factory, address oracle, address underlying ) { if (weth == address(0)) revert ZeroAddress(); if (factory == address(0)) revert ZeroAddress(); if (oracle == address(0)) revert ZeroAddress(); WETH = weth; FACTORY = factory; ORACLE = oracle; DEPLOYMENT_CHAIN_ID = block.chainid; CREATED_AT = block.timestamp; UNDERLYING_DECIMALS = IERC20Metadata(underlying).decimals(); } } ``` ### Immutable Rules - Must be assigned in the constructor (or inline for simple expressions) - Cannot be assigned in any other function - Cannot be used in upgradeable contracts with initializers (no constructor runs) - Value types only: `address`, `uint*`, `int*`, `bool`, `bytes1`-`bytes32` - **Cannot** be `immutable`: `string`, `bytes`, arrays, structs, mappings ## Gas Comparison ```solidity contract GasComparison { // Storage: 2,100 gas cold SLOAD, 100 gas warm SLOAD address public storageVar; // Immutable: 0 gas — loaded from bytecode via PUSH32 address public immutable immutableVar; // Constant: 0 gas — inlined at every usage site uint256 public constant CONSTANT_VAR = 42; constructor(address _addr) { storageVar = _addr; immutableVar = _addr; } // Reading storageVar: ~2,100 gas (cold) or ~100 gas (warm) // Reading immutableVar: ~3 gas (PUSH32) // Reading CONSTANT_VAR: ~3 gas (PUSH32, inlined) } ``` ### Real-World Savings For a function that reads an address 3 times: - Storage: 2,100 + 100 + 100 = **2,300 gas** - Immutable: 3 + 3 + 3 = **9 gas** (savings: 2,291 gas) ## Bytes32 vs String for Constants ```solidity // BAD: string constant — still uses dynamic encoding string public constant NAME = "MyProtocol"; // GOOD: bytes32 constant — fixed size, cheaper to read and compare bytes32 public constant NAME = "MyProtocol"; // Converting bytes32 to string (offchain or in rare onchain cases) function nameAsString() external pure returns (string memory) { return string(abi.encodePacked(NAME)); } ``` ## Naming Conventions ```solidity // Constants: UPPER_SNAKE_CASE uint256 public constant MAX_SUPPLY = 1_000_000e18; // Immutables: UPPER_SNAKE_CASE (they're functionally constant post-deploy) address public immutable TREASURY; // Storage variables: camelCase uint256 public totalDeposited; ``` ## Immutables in Upgradeable Contracts Immutables are stored in bytecode, not storage. In upgradeable proxies, the proxy reads the **implementation's** bytecode. This means immutables can be used in implementations — they're set in the implementation's constructor (which runs at deployment, not through the proxy). ```solidity contract VaultImplementation is Initializable { // These are set when the implementation contract is deployed address public immutable WETH; address public immutable FACTORY; constructor(address weth, address factory) { WETH = weth; FACTORY = factory; _disableInitializers(); } function initialize(address admin) external initializer { // Storage variables set here (via proxy's storage) __Ownable_init(admin); } } ``` ## Checklist - [ ] All values known at compile time are `constant` - [ ] All values known at deploy time (but not compile time) are `immutable` - [ ] Only values that change post-deployment use storage - [ ] Constructor validates all immutable values (zero-address checks) - [ ] UPPER_SNAKE_CASE naming for both `constant` and `immutable` - [ ] `bytes32` preferred over `string` for short constant strings - [ ] Immutables in upgradeable contracts are in the implementation constructor