What immutable means in Solidity

An immutable variable can be assigned only once, either at declaration or inside the constructor. After deployment, its value cannot change.

Unlike constant, an immutable value is not required to be known at compile time. That makes it ideal for values that depend on the deployment environment, such as:

  • token addresses
  • fee recipients
  • oracle contracts
  • admin or owner addresses
  • chain-specific configuration

At runtime, Solidity replaces reads of immutable values with code that loads the embedded value from the deployed bytecode. This is cheaper than reading from storage and avoids an extra state slot.

immutable vs constant vs storage

Featureconstantimmutablestorage
Assigned at compile timeYesNoNo
Assigned in constructorNoYesYes
Can change after deploymentNoNoYes
Stored in contract state slotsNoNoYes
Runtime read costLowestVery lowHigher
Best forFixed literalsDeployment-time configMutable state

A useful rule of thumb:

  • use constant for values known before compilation
  • use immutable for values known only at deployment
  • use storage for values that must evolve

Why immutables are useful in real contracts

Storage reads are one of the most common recurring costs in Solidity. If a contract repeatedly needs the same address or parameter, storing it in a state slot means every access incurs an SLOAD.

With immutable, the value is embedded into runtime code, so reads are much cheaper. This matters in:

  • token contracts that frequently reference a treasury or fee collector
  • vaults that need a fixed asset, oracle, or router address
  • access-controlled systems with a single admin or governance contract
  • factory-deployed instances where each deployment has different configuration

The benefit is not only gas efficiency. Immutables also improve clarity by making it explicit that a value is part of deployment configuration, not mutable business state.


Basic syntax and constructor assignment

Here is a minimal example showing how to define and use immutables.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract FeeCollector {
    address public immutable treasury;
    uint256 public immutable feeBps;

    constructor(address treasury_, uint256 feeBps_) {
        require(treasury_ != address(0), "zero treasury");
        require(feeBps_ <= 1_000, "fee too high"); // max 10%

        treasury = treasury_;
        feeBps = feeBps_;
    }

    function quoteFee(uint256 amount) public view returns (uint256) {
        return (amount * feeBps) / 10_000;
    }
}

Key points

  • immutable variables are often declared public so Solidity generates a getter.
  • The constructor should validate inputs before assignment.
  • Once deployed, treasury and feeBps cannot be changed.

This pattern is especially useful when deploying the same contract to multiple environments with different addresses.


Where immutables save gas

A storage variable requires a persistent slot in contract state. Reading it costs an SLOAD, which is significantly more expensive than reading a value embedded in bytecode.

Consider a contract that checks a trusted router address on every swap or deposit. If the router is immutable, each access is cheaper than a storage read. That difference becomes meaningful in frequently called functions.

Practical examples of good immutable candidates

  • owner in contracts with a fixed admin model
  • token in wrappers or staking contracts
  • router in integration contracts
  • oracle in price-sensitive logic
  • feeRecipient in protocol fee collection

Poor candidates for immutable

Avoid immutables for values that may need governance updates, emergency rotation, or migrations. Examples include:

  • upgrade implementation addresses
  • rotating signers
  • dynamic fee rates
  • whitelists
  • balances and counters

If a value may need to change, it should usually be stored in state and protected by access control.


Common patterns for deployment-time configuration

1. Fixed external contract addresses

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract StakingVault {
    IERC20 public immutable stakingToken;

    constructor(IERC20 stakingToken_) {
        require(address(stakingToken_) != address(0), "zero token");
        stakingToken = stakingToken_;
    }

    function deposit(uint256 amount) external {
        require(stakingToken.transferFrom(msg.sender, address(this), amount), "transfer failed");
    }
}

This is a strong fit because the token address is fixed for the lifetime of the vault.

2. Immutable role addresses

address public immutable governance;

constructor(address governance_) {
    require(governance_ != address(0), "zero governance");
    governance = governance_;
}

This is useful when the contract is intentionally non-upgradeable and governance is expected to remain constant.

3. Immutable numeric parameters

uint256 public immutable minDeposit;
uint256 public immutable maxSlippageBps;

These are good for protocol parameters that are set once at deployment and never changed.


Constructor validation best practices

Because immutables cannot be changed later, constructor validation is critical. A bad deployment can permanently lock in an invalid address or parameter.

Validate these cases explicitly

  • zero addresses
  • out-of-range percentages
  • mismatched decimals or units
  • unsupported chain-specific values
  • inconsistent parameter combinations

For example:

constructor(address treasury_, uint256 feeBps_) {
    require(treasury_ != address(0), "zero treasury");
    require(feeBps_ <= 500, "fee too high");
    treasury = treasury_;
    feeBps = feeBps_;
}

If the contract depends on another contract, consider checking that it is deployed and compatible. For instance, if you expect a router or oracle, verify its interface behavior in deployment scripts or tests.


Immutables in inheritance hierarchies

Immutables work well in base contracts, but you should be careful about constructor ordering and initialization flow.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

abstract contract OwnableBase {
    address public immutable owner;

    constructor(address owner_) {
        require(owner_ != address(0), "zero owner");
        owner = owner_;
    }
}

contract ManagedVault is OwnableBase {
    constructor(address owner_) OwnableBase(owner_) {}

    function onlyOwnerAction() external view {
        require(msg.sender == owner, "not owner");
    }
}

Best practices for inheritance

  • assign immutables in the most appropriate constructor layer
  • keep validation close to the assignment
  • avoid duplicate configuration across parent and child contracts
  • document which values are fixed at deployment

If multiple contracts need the same immutable value, prefer passing it through constructors rather than duplicating storage or relying on global assumptions.


Immutables and upgradeable contracts

Immutables are generally a poor fit for proxy-based upgradeable systems.

Why? Because the constructor runs only on the implementation contract, not on the proxy instance. Since proxy state lives in the proxy, constructor-assigned immutables do not behave like normal runtime state for upgradeable deployments.

Practical guidance

  • Use immutables in non-upgradeable contracts.
  • For proxy patterns, prefer initializer functions and storage variables.
  • If you need deployment-time configuration in an upgradeable system, store it explicitly in state during initialization.
PatternRecommended?Reason
Direct deployment, no proxyYesConstructor can set immutables normally
Transparent/UUPS proxyNoConstructor logic does not initialize proxy state
Factory deploying independent instancesYesEach instance can receive its own immutable config

If your architecture may evolve toward upgradeability later, avoid immutables for values that might need to be reconfigured through future upgrades.


Reading immutables in functions

Immutable values can be accessed like normal state variables.

function treasuryAddress() external view returns (address) {
    return treasury;
}

You can also use them directly in internal logic:

function isTrusted(address candidate) internal view returns (bool) {
    return candidate == treasury;
}

Because they are effectively read-only, immutables are safe to use in view and pure-adjacent logic where state mutation is not expected.


Security and design considerations

1. Treat constructor inputs as part of the trust boundary

An immutable is only as trustworthy as the deployment process. If deployment scripts or CI pipelines can be manipulated, an attacker may lock in malicious values permanently.

Protect deployment with:

  • reviewed deployment scripts
  • explicit parameter logging
  • environment separation
  • multisig-controlled deployment flows where appropriate

2. Do not use immutables for values that require rotation

If a protocol may need to replace a signer, treasury, oracle, or router, use mutable storage and an access-controlled setter instead.

3. Be careful with assumptions about external contracts

An immutable address does not guarantee the target contract remains valid forever. If the target is upgradeable or externally controlled, your contract may still depend on behavior that changes over time.

4. Prefer clear naming

Use names that communicate deployment-time finality:

  • immutable treasury
  • immutable asset
  • immutable governance
  • immutable oracle

This makes audits and maintenance easier.


When to choose immutable

Use immutable when all of the following are true:

  • the value is known at deployment
  • the value will not need to change
  • the contract will be deployed directly, not behind a proxy
  • the value is read often enough that storage savings matter
  • the constructor can validate the input safely

If any of these are false, reconsider whether storage is the better choice.

Decision checklist

  • Is the value fixed for the contract lifetime? → immutable
  • Is the value known at compile time? → constant
  • Must the value be updated later? → storage
  • Is the contract upgradeable through a proxy? → usually storage

Summary

Immutable variables are one of Solidity’s most practical advanced features. They let you capture deployment-time configuration without paying ongoing storage costs, while still keeping the code expressive and easy to audit.

They are especially effective for addresses and parameters that are fixed after deployment, such as treasury accounts, token contracts, routers, and fee settings. The main discipline is to validate constructor inputs carefully and avoid using immutables in systems that need future reconfiguration or proxy-based upgrades.

Learn more with useful resources