📋 What Is It?
Access control is the set of rules deciding who may invoke which state-changing operation. An access-control vulnerability exists when a privileged function - one that moves funds, mints tokens, changes ownership, upgrades logic, pauses, or destroys the contract - can be called by an account that should never have been permitted. A deployed contract is public, immutable, and permissionlessly callable: there is no firewall and no login to fail. If a treasury-draining function lacks an authorization check, the check simply does not exist, and bots scanning the mempool exploit it within a block.
⚠️ Common Exploits
- Missing Modifier: Sensitive function has no
onlyOwner/role check at all - tx.origin Auth: Phishing contract acts with the owner's authority
- Open Initializer: Unguarded
initialize()lets anyone become owner - Unguarded Upgrade: Public
setImplementation/ rawdelegatecall - Unprotected selfdestruct: Anyone erases the contract and sweeps its balance
- Inverted Modifier:
!=instead of==lets everyone but the owner through - Weak Ownership Transfer: Public or single-step setter enables takeover/lockout
- Default Visibility: Legacy code leaves a helper externally callable
🔴 Attack Flow
↓
2. List every external, state-changing function
↓
3. Spot one with no modifier or a broken guard
↓
4. Directly call withdraw / mint / seize ownership
↓
5. IMPACT: Treasury drained, supply inflated, protocol taken over!
❌ Vulnerable Code
✅ Secure Code
✓ Prevention Checklist
- Explicit modifier on EVERY privileged function
- Authenticate with
msg.sender, nevertx.origin - Use OpenZeppelin
Ownable/AccessControl - Guard initializers with
initializer - Call
_disableInitializers()on implementations - Restrict upgrades via
_authorizeUpgrade - Never expose a public
delegatecalltarget - Two-step ownership transfer; reject zero address
- Remove or governance-gate
selfdestruct - Declare explicit function visibility
- Least privilege + role separation (multisig/timelock)
- Negative tests: unauthorized callers MUST revert
🔍 Detection & Tools
Analysis Tools:
Libraries & Patterns:
How to Test:
- Run
slither .- detectors: suicidal, tx-origin, unprotected-upgrade - Run
mythril analyzefor reachable privileged ops - Enumerate every external function; map its intended caller
- Write a reverting test for each function from a stranger address
- Confirm the implementation cannot be initialized directly
🌍 Real-World
- Parity Multisig Freeze (2017): An unprotected initialization function on a shared library, reached via
delegatecall, let an account take ownership and callselfdestruct- bricking every wallet that depended on the library. - Uninitialized-Proxy Takeover class: Implementation contracts left uninitialized let an attacker call
initialize, seize ownership of the logic, and hijack the upgrade path or destroy it. - Unprotected Privileged Function class: A
mint/withdraw/setOwnershipped without its intended modifier - the most frequently reported access-control finding; searchers detect and exploit it automatically. - tx.origin phishing: A contract the owner is lured into calling inherits the owner's
tx.originand passes the check.
📌 Quick Tips
- DO NOT leave any privileged function unguarded
- DO NOT authorize with
tx.origin - DO NOT expose a public
selfdestructor upgrade setter - DO use audited
Ownable/AccessControl - DO lock initializers and disable them on the impl
- DO transfer ownership in two steps
- DO run Slither/Mythril and negative tests in CI