> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.getfoundry.sh/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

## Uninitialized State Variables

**Severity**: `Med`
**ID**: `uninitialized-state`

Flags state variables that are read anywhere in a contract's inheritance chain but never
assigned. Because Solidity zero-initialises all storage, such a variable silently returns
its type's zero value (`0`, `address(0)`, `false`, etc.), which almost always indicates a
missing initialisation step, for example, forgetting to set `owner` in the constructor.

### What it does

For each non-constant, non-immutable state variable across the full C3-linearised inheritance
chain, the lint checks whether it is ever written, via an inline initialiser at the
declaration site, any assignment (including compound assignments such as `+=`), `delete`,
pre/post increment/decrement, or `push`/`pop` on a dynamic array, anywhere in any function
or constructor in the hierarchy, including modifier call arguments and base-constructor
arguments. Writes through a local `storage` pointer (`Foo storage f = bar; f.x = 1;`) count
as writes to every state variable the pointer may reference. If the variable is read (in a
function body, state-variable initialiser, modifier argument, or compiler-synthesised public
getter) but never written by any of the above, it is flagged.

**Assembly bail-out**: if any function body in the inheritance chain contains inline assembly
(which Solar lowers to an opaque AST node), the lint skips the entire contract conservatively
to avoid false positives from untracked storage writes.

**Known limitations**:

* *Storage aliases*: pointer tracking is flow-insensitive, so a pointer that is reassigned
  (`f = bar; f = baz; f.x = 1;`) counts as a write to both `bar` and `baz`. A pointer
  initialized from a function return value has no known target, so writes through it are
  not attributed to any state variable.
* *Storage-parameter calls (partial)*: the lint detects when a state variable or a local
  storage pointer is passed as a storage reference to a bare, qualified, or `super` internal
  call and treats it as a write. Calls through a member expression where the receiver is
  itself a state variable remain undetected.
* *Member calls*: any member call whose receiver is a state variable or a local storage
  pointer to one (e.g. `oracle.latestAnswer()`, `token.balanceOf(address)`) suppresses the
  warning for that variable. Without full call-graph resolution the lint conservatively treats the receiver
  as potentially mutated, to avoid false positives from `push`/`pop` and library-dispatch
  patterns (`using Lib for T`). Read-only interface calls on uninitialized variables will
  therefore not be flagged.

### Why is this bad?

A variable that is always read as its zero default is almost certainly a logic bug. Common
consequences include:

* Ownership checks that permanently pass or fail (`owner` is always `address(0)`).
* Token balances that always read as zero regardless of deposits.
* Flags and counters that never reflect actual contract state.

The Solidity compiler does not warn about this pattern because reading an uninitialized
storage variable is syntactically valid.

### Example

#### Bad

```solidity
contract Escrow {
    address public owner; // never set, always address(0)

    function withdraw() external {
        require(msg.sender == owner, "not owner"); // always fails
        payable(owner).transfer(address(this).balance);
    }
}
```

#### Good

```solidity
contract Escrow {
    address public owner;

    constructor(address _owner) {
        owner = _owner; // initialized in constructor
    }

    function withdraw() external {
        require(msg.sender == owner, "not owner");
        payable(owner).transfer(address(this).balance);
    }
}
```
