High severity

Uninitialized state variable

Solidity never leaves storage undefined. Every state variable you declare but never assign still has a value: the zero value for its type — 0 for a number, false for a bool, and address(0) for an address. Nothing warns you, so a variable that was supposed to be set in a constructor or a setter but was forgotten reads back as zero at runtime, and the contract keeps running as if that were a real value. When the forgotten variable is a payout destination, the contract sends ether to address(0), where it is burned and unrecoverable. When it is an owner or an admin, the access check compares against address(0), so either no one can ever pass it or — worse, in code that treats a zero owner as “ownership renounced” or “not yet claimed” — the wrong party slips through. The variable looks declared and used, the code compiles cleanly, and the gap only shows up on-chain with real funds.

Vulnerable pattern

A state variable that is read but never written
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Payout {
    // Meant to be the treasury address, but nothing ever assigns it,
    // so it stays at address(0) for the life of the contract.
    address payable treasury;

    // Anyone can fund the contract.
    receive() external payable {}

    // Sends the balance to `treasury` -- which is address(0).
    // The ether is burned, not paid out, and cannot be recovered.
    function sweep() external {
        treasury.transfer(address(this).balance);
    }
}

The fix

Give the variable a value before anything reads it, and make the compiler help you. Assign it in the constructor (or at the point of declaration for a genuine constant), validate the input so a zero address can never be stored by accident, and reach for OpenZeppelin’s Ownable or AccessControl rather than hand-rolling an owner address that is easy to leave unset. If a variable is deliberately meant to start at zero, assign it 0 explicitly so a reader can tell the zero was intended rather than forgotten. The corrected contract sets the treasury once, at construction, and rejects a zero address outright.

The same contract, corrected
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Payout {
    address payable public immutable treasury;

    constructor(address payable treasury_) {
        require(treasury_ != address(0), "treasury is zero");
        treasury = treasury_;
    }

    receive() external payable {}

    function sweep() external {
        treasury.transfer(address(this).balance);
    }
}

A note on scope

The detector reports a state variable only when it is read somewhere and written nowhere — not merely unassigned at declaration. A write counts whether it happens in the contract’s own functions, in a modifier, or by being passed as a storage argument into an internal or library call that assigns it, so the common pattern of a helper or library that initialises storage on the contract’s behalf does not trip it. It also analyses only “leaf” contracts — those that no other contract inherits — so a base contract whose variable is set only by a child is judged as part of that child, where the write is visible, rather than being flagged in isolation. What it cannot see is initialisation that happens outside the Solidity it is given: a value written only by an upgradeable proxy’s initializer, by assembly, or by a delegatecall into shared storage. That last case is exactly where pactlint’s triage steps in below.

Scan your contract for this

pactlint flags uninitialized-state and other recurring DeFi bug classes in Solidity source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon or Solana with x402, no signup; the first few scans each day are free.

How to scan →

An uninitialized state variable is detected by Slither’s stock uninitialized-state detector, rated high impact at high confidence, which pactlint surfaces at high severity when a state variable is read but assigned nowhere the analyser can see. pactlint does run worker-side triage on this class: it drops a finding to low only in the narrow case where a contract that inherits this one delegates its fallback to an extension or implementation that shares this storage and may write the variable — a proxy or extension pattern where the assignment lives in code Slither did not analyse. Any other genuinely unwritten variable stays High. Run it in CI with the pactlint GitHub Action. Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.