High severity
State variable shadowing
A child contract declares a state variable with the same name as one it already inherits from a parent. Solidity allows this: the two names refer to two different storage slots, one in the parent and one in the child. Code written in the parent — a modifier, a getter, an accounting update — keeps reading and writing the parent's slot, while the child's own code touches the child's slot. The two drift apart. The classic case is an access-control variable: the parent's onlyOwner modifier checks the parent's owner, but the child's constructor sets the child's owner, so the parent's slot is never assigned and stays the zero address. The guard then compares against zero and locks everyone out, or — worse in some layouts — lets an unintended account through. The bug is quiet because every line looks correct in isolation; it only shows up when a write in one contract fails to change what a read in the other sees.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Base {
address owner; // Base's slot
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
}
contract Vault is Base {
address owner; // shadows Base.owner: a SECOND slot
constructor() {
owner = msg.sender; // sets Vault's slot, not Base's
}
// onlyOwner reads Base.owner, which was never assigned and is still
// address(0). The check is broken: the real deployer is rejected, and
// the modifier now gates on the zero address instead of the owner.
function withdraw() external onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}
}The fix
Declare each state variable once, in a single contract in the hierarchy, and never re-declare an inherited name. Remove the child's duplicate and let it use the parent's variable; if the parent's visibility is too narrow, widen it to internal rather than shadowing it. Where a child genuinely needs its own value, give it a different name so the distinction is explicit. This also matters for upgradeable contracts: a shadowed or re-ordered variable shifts the storage layout and can corrupt existing state across an upgrade, so keep one canonical layout and append new variables at the end.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Base {
address internal owner; // the only `owner` slot
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
}
contract Vault is Base {
constructor() {
owner = msg.sender; // writes the inherited slot the modifier reads
}
function withdraw() external onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}
}Scan your contract for this
pactlint flags shadowing-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 or Polygon with x402,
no signup; the first few scans each day are free.
A state variable that shadows an inherited one is detected by Slither's stock shadowing-state detector,
rated high impact at high confidence, which pactlint surfaces at high severity — it keeps Slither's own
impact-and-confidence rating, with no extra downgrade or triage. (A local variable shadowing an outer name,
shadowing-local, is a separate, style-level check pactlint treats as noise.) Run it in CI with the
pactlint GitHub Action.
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.