# Access Control Pitfalls — Beyond onlyOwner
Most developers start with onlyOwner. It works, but it is a single point of failure. If the owner key is compromised, everything is lost.
The Problem with Single-Owner Patterns
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function withdraw(uint256 amount) external onlyOwner {
payable(owner).transfer(amount);
}
If the owner key leaks, an attacker can drain funds AND pause the contract.
Role-Based Access Control
OpenZeppelin AccessControl separates permissions into roles:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Treasury is AccessControl {
bytes32 public constant WITHDRAWER = keccak256("WITHDRAWER");
bytes32 public constant PAUSER = keccak256("PAUSER");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function withdraw(uint256 amount) external onlyRole(WITHDRAWER) {
payable(msg.sender).transfer(amount);
}
}
Different keys can have different permissions. Compromise of one role does not compromise all functions.
Common Pitfalls
1. Missing Access Control
// VULNERABLE — anyone can call
function setPrice(uint256 newPrice) external {
price = newPrice;
}
2. Not Revoking Deployer Privileges
After setup, renounce unnecessary roles:
_grantRole(DEFAULT_ADMIN_ROLE, multisig);
_revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);
3. No Timelock on Sensitive Operations
Use timelocks so users can exit before admin actions take effect. A 48h delay gives users time to react.