State Variable Shadowing: Solidity's Silent Inheritance Killer

Two variables with the same name across a parent and child contract aren't a syntax error — they're a logic bomb. State variable shadowing has silently broken access control, fee logic, and upgradeability in production contracts. Solidity 0.6+ warns you, but only if you're paying attention. Here's…

State variable shadowing (SWC-119) occurs in Solidity when a derived contract declares a state variable with the same name as one in its parent contract. The child's variable hides the parent's, but both occupy separate storage slots. Functions inherited from the parent continue to read and write the parent's variable, while functions in the child operate on the shadow — leading to desynchronized state, broken access control, and subtle logic bugs.
··7 min read

The Bug in One Sentence

When a child contract redeclares a state variable that already exists in a parent, Solidity gives you two storage slots, two sources of truth, and one very confused codebase.

This is SWC-119. It's not exploitable on its own — it's a *foundation* bug that quietly corrupts whatever logic depends on the shadowed variable. Access control, fee accounting, paused flags, owner addresses: all prime targets.

A Minimal Vulnerable Example

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0; // shadowing compiles silently here

contract Base {
address public owner;

constructor() public {
owner = msg.sender;
}

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

contract Vault is Base {
address public owner; // shadows Base.owner

constructor() public {
owner = msg.sender; // sets Vault.owner only
}

function withdraw() external onlyOwner {
// onlyOwner reads Base.owner — which is address(0)
// because Base's constructor wrote to Base.owner,
// but Vault never inherited that write path correctly
// under certain constructor orderings.
payable(msg.sender).transfer(address(this).balance);
}
}
```

Look closely. `Vault` declares its own `owner`. The `onlyOwner` modifier is *defined in `Base`*, so when it runs it reads `Base.owner`. But every external caller checking `Vault.owner()` via the public getter sees the shadow. Two different answers to "who owns this vault?" depending on which function you ask.

Under Solidity 0.5.x this compiles without complaint. In 0.6.0+ it's a compile error — *if* the parent variable is non-private. Mark it `private` or change visibility patterns and the warning evaporates. Inherited contracts from upgradeable patterns (OpenZeppelin's `Initializable`, `ContextUpgradeable`) have produced exactly this footgun when developers redefine `_owner`, `_paused`, or `__gap`.

Why The Storage Layout Matters

Each contract in the inheritance chain contributes its state variables to a single storage layout in linearization order (C3). Shadowed variables don't merge — they each consume a slot:

Slot 0: `Base.owner`
- Slot 1: `Vault.owner`

Now imagine this in an **upgradeable proxy**. You deploy V1 with `Base.owner` at slot 0. V2 adds a child contract that redeclares `owner`. Storage slot 0 still holds the original value, but new code reads slot 1 — uninitialized, `address(0)`. Anyone can claim ownership via a public setter, or worse, ownership checks silently pass for nobody.

This is precisely the bug class that wrecks upgradeable contracts and the reason OpenZeppelin ships `__gap` arrays in every upgradeable parent.

The Fix

There are three correct moves, in order of preference:

1. Don't redeclare. Use the inherited variable.

```solidity
pragma solidity ^0.8.20;

contract Base {
address public owner;
constructor() { owner = msg.sender; }
modifier onlyOwner() { require(msg.sender == owner); _; }
}

contract Vault is Base {
// No shadow. owner is inherited and shared.
function withdraw() external onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}
}
```

2. If you need a different variable, give it a different name.

```solidity
contract Vault is Base {
address public operator; // distinct role, distinct name
}
```

3. Use `virtual`/`override` for functions, never duplicate state.

Solidity has explicit overriding for functions. There is no equivalent for state variables — and that's deliberate. If you find yourself wanting to "override" a state variable, you want a setter, an internal function, or a different design.

Real-World Incidents

Shadowing rarely makes headlines on its own — it's the *enabler* of the eventual exploit, so post-mortems usually name the symptom ("access control bypass," "incorrect fee calculation") rather than the root cause.

A few documented cases worth knowing:

**Multiple audit reports from Trail of Bits and ConsenSys Diligence** flag shadowed `_owner` and `_paused` variables in client codebases pre-deployment. The fix is trivial; the cost of missing it is total compromise.
- **Tokens forked from OpenZeppelin templates** have shipped with redeclared `_balances` or `_totalSupply` in custom child contracts, producing tokens whose `totalSupply()` view returns one number and whose actual transferable supply is another. Several rugpulls have used this *deliberately* — the contract reads honest on Etherscan while the real state lives in a shadow.
- **Upgradeable proxy migrations** have bricked themselves when V2 introduced a parent with overlapping variable names, shifting the layout. Audius's 2022 governance exploit (~$6M) wasn't shadowing per se, but it lived in the same family: a delegated call hitting a storage slot the caller didn't expect.

Slither's `shadowing-state`, `shadowing-abstract`, and `shadowing-local` detectors catch all three flavors in seconds. If you're not running Slither in CI, you're auditing your contracts with hope.

Detection Checklist

1. Run `slither . --detect shadowing-state,shadowing-abstract,shadowing-local`.
2. Compile with `solc` 0.8.x — many shadowing patterns are now hard errors.
3. For upgradeable contracts, diff storage layouts between versions with `forge inspect <Contract> storageLayout` or OpenZeppelin's Upgrades plugin.
4. Grep for any state variable name that appears in more than one contract file in your inheritance tree.
5. Get a second pair of eyes. Free first pass via our [AI-powered audit](https://www.cryptohawking.com/audit), or for production contracts holding real value, a [manual audit](https://www.cryptohawking.com/audit/manual) catches the inheritance-graph bugs that pattern matchers miss.

The Broader Lesson

Shadowing is a special case of a general principle: **Solidity's inheritance and storage models are coupled, and the compiler will not save you from semantic mistakes that are syntactically legal.** Linearization order, storage slot assignment, modifier resolution — these all depend on the inheritance graph you wrote. If two contracts in that graph disagree about what a name means, the EVM picks a winner you didn't choose.

Name your variables uniquely. Run Slither. Read the storage layout. And when in doubt, ask whether a redeclaration is buying you anything that an internal getter wouldn't.

FAQ

Does Solidity 0.8 prevent state variable shadowing entirely?

No. Solidity 0.6+ throws a compile error when a child redeclares a non-private state variable from a parent — but only for direct shadowing of visible variables. It does not catch shadowing across abstract contracts in some edge cases, shadowing of private variables (which technically isn't shadowing since private variables aren't inherited but still occupy adjacent storage), or storage-layout collisions in upgradeable proxies. You still need Slither and a storage-layout diff for upgradeable systems.

Is shadowing a problem in non-upgradeable contracts?

Yes, though the failure mode is different. In a non-upgradeable contract, shadowing creates two storage slots that drift apart depending on which function writes to them. Inherited modifiers and functions read the parent's slot; child functions read the child's. This produces broken access control, incorrect view functions, and accounting bugs. The contract isn't bricked the way an upgradeable one might be, but its behavior is unpredictable and exploitable.

How does shadowing differ from function overriding?

Function overriding is explicit: you mark the parent function `virtual` and the child `override`, and Solidity replaces the dispatch entry. There is exactly one implementation at runtime. State variable shadowing is implicit and additive: both variables exist, both occupy storage, and which one gets read depends on which contract's code is executing. Solidity provides no `override` keyword for state variables because the language design treats redeclaration as a mistake, not a feature.

Can I shadow a variable on purpose for a legitimate reason?

Almost never. The legitimate use cases people propose — versioning, role differentiation, namespace separation — are all better solved by unique names, mappings, or structs. If you genuinely need parallel state per inheritance level, use distinct names like `_baseOwner` and `_vaultOwner`. The cost of an unambiguous name is zero; the cost of a shadow is a future incident report.

What tools detect state variable shadowing?

Slither is the gold standard with three detectors: `shadowing-state`, `shadowing-abstract`, and `shadowing-local`. Mythril and Securify also flag some cases. The Solidity compiler itself catches direct shadowing of visible variables since 0.6.0. For upgradeable contracts, OpenZeppelin's Upgrades plugin checks storage-layout compatibility between versions, which catches shadowing-induced slot drift. Running Slither in CI on every PR is the lowest-effort, highest-value step you can take against this bug class.

One Solidity tip + 1 case study per month

State Variable Shadowing: Solidity's Silent Inheritance Killer | Crypto Hawking