Access Control Reviewer
1. Purpose
Systematically validate access control in Solidity contracts. Access control failures are the #1 vulnerability category in 2025, responsible for $1.6B+ in losses.
2. Vulnerability Patterns
ETH-006: Missing Access Control (CRITICAL)
State-changing function without any authorization check.
// VULNERABLE
function setPrice(uint newPrice) external {
price = newPrice; // Anyone can call!
}
// SECURE
function setPrice(uint newPrice) external onlyOwner {
price = newPrice;
}
ETH-007: tx.origin Authentication (CRITICAL)
// VULNERABLE — phishing attack via intermediate contract require(tx.origin == owner); // SECURE require(msg.sender == owner);
ETH-008: Unprotected selfdestruct (CRITICAL)
// VULNERABLE
function destroy() external {
selfdestruct(payable(msg.sender));
}
// SECURE
function destroy() external onlyOwner {
selfdestruct(payable(owner));
}
ETH-010: Uninitialized Proxy (CRITICAL)
Implementation contract not initialized, allowing anyone to take ownership.
ETH-012: Centralization Risk (MEDIUM)
Single admin can change critical parameters without timelock or multisig.
ETH-086: Broken tx.origin == msg.sender Assumption (CRITICAL) — EIP-7702
Post-Pectra, tx.origin == msg.sender no longer guarantees caller is an EOA. EOAs can have code via EIP-7702 delegation.
// VULNERABLE — broken after EIP-7702 (Pectra upgrade) require(tx.origin == msg.sender, "No contracts"); // EOA with delegated code passes this check but executes arbitrary logic! // SECURE — use ERC-165 or explicit whitelist // Or: accept that EOAs can have code and design accordingly
ETH-088: EIP-7702 Cross-Chain Authorization Replay (CRITICAL)
EIP-7702 authorization tuples can be replayed on other chains if chain ID is not validated.
// VULNERABLE — no chain ID in authorization validation
function setDelegation(address delegate, bytes calldata sig) external {
// Missing: chainId check in signature verification
}
// SECURE — always include chain ID in EIP-7702 authorization
// The protocol enforces this, but custom implementations may not
ETH-091: Paymaster Exploitation (CRITICAL) — ERC-4337
Paymaster sponsors gas but doesn't validate actual execution cost or effects.
// VULNERABLE — paymaster pays for any UserOp
function validatePaymasterUserOp(UserOperation calldata userOp, ...)
external returns (bytes memory context, uint256 validationData) {
return ("", 0); // Approves everything!
}
// SECURE — validate user, op limits, and track spending
function validatePaymasterUserOp(UserOperation calldata userOp, ...)
external returns (bytes memory context, uint256 validationData) {
require(approvedUsers[userOp.sender], "Not approved");
require(userOp.callGasLimit <= MAX_GAS, "Gas too high");
require(dailySpend[userOp.sender] + cost <= DAILY_LIMIT, "Limit exceeded");
// ...
}
ETH-093: Validation-Execution Phase Confusion (CRITICAL) — ERC-4337
Validation phase has side effects that affect execution phase, violating ERC-4337 separation.
3. Audit Workflow
Step 1: Map All State-changing Functions
rg "function.*(external|public)" contracts/ | grep -v "view\|pure"
Step 2: Check Access Control
For each state-changing function, verify:
- • Has
onlyOwner,onlyRole, or custom access modifier - • Uses
msg.sendernottx.origin - • Does NOT rely on
tx.origin == msg.senderto detect EOA (EIP-7702 breaks this) - • Initializer has
initializermodifier - • Upgrade functions have proper authorization
- • ERC-4337 UserOp validation doesn't have side effects
Step 3: Map Privilege Hierarchy
Owner / Admin ├── Can: pause, unpause, upgrade, setFee ├── Should have: timelock, multisig │ Operator / Manager ├── Can: harvest, rebalance │ User └── Can: deposit, withdraw, claim
Step 4: Check Proxy Patterns
rg "initializer|_disableInitializers|__gap" contracts/ rg "upgradeTo|upgradeToAndCall|_authorizeUpgrade" contracts/
Step 5: Check EIP-7702 / ERC-4337 Patterns
rg "tx\.origin.*==.*msg\.sender|msg\.sender.*==.*tx\.origin" contracts/ # ETH-086 rg "extcodesize|isContract" contracts/ # ETH-089 — unreliable post-7702 rg "IPaymaster|validatePaymasterUserOp|UserOperation" contracts/ # ETH-091 rg "validateUserOp|IAccount" contracts/ # ETH-093
4. OpenZeppelin Patterns
Ownable
import "@openzeppelin/contracts/access/Ownable.sol"; // Single owner, transferable
AccessControl (RBAC)
import "@openzeppelin/contracts/access/AccessControl.sol";
// Role-based, granular permissions
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
Ownable2Step
import "@openzeppelin/contracts/access/Ownable2Step.sol"; // Two-step ownership transfer (safer)
5. Quick Checklist
- • All state-changing functions have access control
- • No
tx.originfor authentication - • No
tx.origin == msg.senderto detect EOA (broken by EIP-7702) - • No
extcodesize == 0to detect EOA (broken by EIP-7702) - • No unprotected
selfdestruct - • Proxy implementations are initialized or disabled
- • Upgrade functions are properly authorized
- • Critical operations have timelock
- • Admin keys use multisig
- • Two-step ownership transfer used
- • ERC-4337 paymaster validates user and limits (ETH-091)
- • ERC-4337 validation phase has no side effects (ETH-093)