---
description: State writes that bypass declared protection
---

## Protected variables

**Severity**: `High`
**ID**: `protected-vars`

Flags externally callable functions that can write a state variable without invoking the function
or modifier named by its `@custom:security write-protection` annotation.

### What it does

A state variable can declare a required protection with an exact function or modifier signature:

```solidity
/// @custom:security write-protection="onlyOwner()"
address owner;
```

The lint follows modifiers, library calls, and resolved internal call paths from public, external,
fallback, and receive entry points. If an entry point writes the variable directly or through a
reachable helper, the required function or modifier must run on every path reaching that write.
Guards are tracked through branches, short-circuit and ternary expressions, loops, returns, and
reachable modifier-placeholder continuations, so a call on only one branch or after the write does
not satisfy the annotation. This includes writes through storage references, returned storage
locations, collection `push`/`pop` operations, and resolvable inline-assembly storage slots.

Inline assembly tracks storage-pointer `.slot` retargeting and resolved Yul return locations.
Persistent `sstore` operations count as writes; transient `tstore` uses a separate namespace and
does not imply that the annotated state variable changed.

Overloads are matched by their exact signature. Inherited variables, entry points, functions, and
modifiers are resolved in the most-derived contract, including virtual dispatch. External calls
such as `this.onlyOwner()` do not satisfy an internal write-protection requirement. An unresolved
signature or malformed `write-protection` value is treated as unsatisfied so an invalid annotation
cannot silently disable the lint.

The rule checks protection at each write occurrence. A protected write on one path does not hide an
unprotected write on another, and a modifier that exits before its placeholder does not make an
unreachable function body appear writable.

### Why is this bad?

Writing security-sensitive state without its declared access check can let an untrusted caller
change ownership, authorization, or other protected configuration.

### Example

#### Bad

```solidity
contract Registry {
    /// @custom:security write-protection="onlyOwner()"
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function setOwner(address newOwner) external {
        owner = newOwner;
    }
}
```

#### Good

```solidity
contract Registry {
    /// @custom:security write-protection="onlyOwner()"
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function setOwner(address newOwner) external onlyOwner {
        owner = newOwner;
    }
}
```
