Immutable Violation
Detects modifications to storage variables that should be immutable, including constructor-set values modified by public functions and view-named functions that write to storage.
Immutable Violation
Overview
Remediation Guide: How to Fix Immutable Violation
The immutable violation detector identifies storage variables that are written during contract construction and then modified by other functions, violating the expectation that certain values remain constant after deployment. At the bytecode level, Solidity’s immutable keyword is not explicitly encoded — the detector uses heuristics to identify constructor-initialized slots that are subsequently overwritten.
The detector also flags functions whose names suggest read-only behavior (matching get, view, balance, total patterns) but contain SSTORE instructions, indicating a mislabeled or incorrectly implemented function.
Why This Is an Issue
Immutability guarantees are a cornerstone of smart contract security. When a variable set during construction (such as a trusted oracle address, bridge endpoint, or token contract) can be modified later, an attacker who gains write access to that slot can redirect the contract’s behavior entirely.
The Nomad Bridge exploit ($190M, August 2022) involved an initialization flaw where a critical variable was set to a value that effectively disabled message verification. Curve Finance pool factory vulnerabilities similarly involved immutable address changes.
How to Resolve
// Before: Vulnerable — critical address stored in regular storage
contract VulnerableBridge {
address public validator; // Mutable storage variable
constructor(address _validator) {
validator = _validator;
}
// BUG: validator can be changed after deployment
function setValidator(address _new) external {
validator = _new;
}
}
// After: Fixed — use immutable keyword
contract SafeBridge {
address public immutable validator; // Cannot be changed after construction
constructor(address _validator) {
validator = _validator;
}
// No setter function — validator is permanently fixed
}
Examples
Vulnerable Code
contract VulnerableOracle {
address public priceFeed;
address public owner;
constructor(address _feed) {
priceFeed = _feed;
owner = msg.sender;
}
// Anyone could call this if onlyOwner is missing or compromised
function updateFeed(address _newFeed) external {
priceFeed = _newFeed; // Immutability violation — feed should not change
}
// Misleading name — this function writes to storage
function getBalance() external returns (uint256) {
lastQueried = block.timestamp; // SSTORE in a "get" function
return address(this).balance;
}
}
Fixed Code
contract SafeOracle {
address public immutable priceFeed;
address public immutable owner;
constructor(address _feed) {
priceFeed = _feed;
owner = msg.sender;
}
// View function — correctly marked, no state changes
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
Sample Sigvex Output
{
"detector_id": "immutable-violation",
"severity": "critical",
"confidence": 0.80,
"description": "Storage slot 0x1 is written in the constructor and modified by function updateFeed(address). This variable appears to be intended as immutable.",
"location": { "function": "updateFeed(address)", "offset": 128 }
}
Detection Methodology
- Constructor analysis: Identifies storage slots written during initialization (the creation-time code path).
- Cross-function write tracking: For each constructor-written slot, checks whether any runtime function also writes to the same slot.
- View-function heuristic: Flags functions whose names match read-only patterns (
get*,view*,balance*,total*) but containSSTOREinstructions. A list of known-mutating standard library functions (e.g.,renounceOwnership,transferOwnership) is excluded. - Write-once detection: Identifies storage slots that are written exactly once in the constructor and never again — if a runtime function writes to such a slot, it is flagged with high confidence.
Limitations
False positives:
- Contracts that intentionally allow one-time migration of immutable-like values (e.g., a
migrate()function with a guard) may be flagged. - Proxy patterns where the constructor sets initial values and a separate
initialize()function is intended to be called once may trigger findings.
False negatives:
- Variables that are set through complex initialization patterns (factory deployment, CREATE2) may not be recognized as constructor-initialized.
- Storage slots accessed through computed offsets (dynamic mappings) are not tracked for immutability.
Related Detectors
- Access Control — detects missing authorization on state-changing functions
- UUPS Initialization — detects uninitialized proxy patterns
- Storage Collision — detects proxy storage slot conflicts