Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sidebar/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const docs = [
{ text: "erc20-unchecked-transfer", link: "/forge/linting/erc20-unchecked-transfer" },
{ text: "incorrect-exp", link: "/forge/linting/incorrect-exp" },
{ text: "incorrect-shift", link: "/forge/linting/incorrect-shift" },
{ text: "protected-vars", link: "/forge/linting/protected-vars" },
{ text: "reentrancy-eth", link: "/forge/linting/reentrancy-eth" },
{ text: "rtlo", link: "/forge/linting/rtlo" },
{ text: "unchecked-call", link: "/forge/linting/unchecked-call" },
Expand Down
1 change: 1 addition & 0 deletions src/pages/forge/linting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ navigation on the right to browse by severity.
- [`erc20-unchecked-transfer`](/forge/linting/erc20-unchecked-transfer) — Flags calls to ERC20 `transfer` and `transferFrom` where the boolean return value is ignored.
- [`incorrect-exp`](/forge/linting/incorrect-exp) — Flags `^` (bitwise xor) used between integer literals where `**` (exponentiation) was almost certainly intended.
- [`incorrect-shift`](/forge/linting/incorrect-shift) — Flags shift operations where a literal appears on the left and a non-literal on the right, which is almost always the wrong operand order.
- [`protected-vars`](/forge/linting/protected-vars) — Flags externally callable functions that can write an annotated state variable without invoking its declared protection.
- [`reentrancy-eth`](/forge/linting/reentrancy-eth) — Flags uncapped ETH-transferring low-level `call` operations when state read before the call is written after the call.
- [`rtlo`](/forge/linting/rtlo) — Flags the presence of Unicode bidirectional override characters in source code, which can be used to hide malicious behavior ("Trojan Source", [CVE-2021-42574](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574)).
- [`unchecked-call`](/forge/linting/unchecked-call) — Flags low-level calls (`call`, `delegatecall`, `staticcall`, `callcode`) whose `success` return value is ignored.
Expand Down
79 changes: 79 additions & 0 deletions src/pages/forge/linting/protected-vars.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
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 also be reachable from that entry point.
This includes writes through storage references, returned storage locations, collection
`push`/`pop` operations, and resolvable inline-assembly storage slots.

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.

Like Slither's annotation semantics, this rule is reachability-based rather than order- or
path-sensitive: the annotation identifies a required internal call, not a proof that the call
dominates every write.

### 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;
}
}
```