Definition: Access control vulnerabilities occur when sensitive functions (mint, burn, transfer, withdraw) lack proper permission checks. Missing or misconfigured access controls have caused 30%+ of DeFi exploits. Common issues: missing onlyOwner modifiers, incorrect role checks, or unrestricted administrative functions. Firepan detects both missing access controls and misconfigured permissions in ERC-20 tokens and DeFi protocols.
Poly Network lost $611M in 2021—not because of reentrancy or oracle manipulation, but because the cross-chain bridge owner function was unprotected. Anyone could call setManager() and become the new owner. Worse, nobody audited it post-launch. Access control vulnerabilities are silent killers: often invisible in static analysis, but catastrophic when exploited. This guide shows how to detect them, prevent them, and monitor continuously.
Standard ERC-20 tokens have two access control models:
Model 1: Centralized Owner (Ownable)
contract MyToken is ERC20, Ownable {
function mint(address to, uint amount) public onlyOwner {
_mint(to, amount);
}
function burn(address from, uint amount) public onlyOwner {
_burn(from, amount);
}
}
Single owner controls all administrative functions. Risk: if owner key is compromised, entire contract is compromised.
Model 2: Role-Based Access Control (RBAC)
contract MyToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
function mint(address to, uint amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
function burn(address from, uint amount) public onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
}
Multiple roles, each with specific permissions. Risk: misconfiguration can grant excessive permissions or create privilege escalation paths.
Vulnerability 1: Missing Access Control
contract VulnerableToken is ERC20 {
function mint(address to, uint amount) public { // No access control!
_mint(to, amount);
}
function setOwner(address newOwner) public { // No access control!
owner = newOwner;
}
}
Anyone can mint unlimited tokens or become owner. Instant total loss.
Vulnerability 2: Incorrect Modifier Order
contract Token is ERC20 {
function transfer(address to, uint amount) public returns (bool) {
require(msg.sender == owner); // Can be bypassed via delegatecall
_transfer(msg.sender, to, amount);
return true;
}
}
If the contract is delegated-into, the owner check might not apply. Use modifiers instead.
Vulnerability 3: Role Granted to Wrong Address
contract Token is ERC20, AccessControl {
function setupRoles() public onlyOwner {
// Typo: grants role to zero address instead of intended address
grantRole(MINTER_ROLE, address(0)); // BUG
}
}
Off-by-one errors in role setup can create unintended access. Especially common in multi-sig setups.
Vulnerability 4: Privilege Escalation via Role Modification
contract Token is AccessControl {
function renounceRole(bytes32 role, address account) public override {
// No checks—anyone can renounce any role for anyone
_revokeRole(role, account);
}
}
Without proper checks, roles can be revoked or transferred maliciously. The protocol loses the ability to pause or mint.
Vulnerability 5: Unprotected Governance Parameter Changes
contract StakingPool {
uint public penaltyFee = 10; // 10%
function setPenaltyFee(uint newFee) public { // No access control
penaltyFee = newFee;
}
}
Unprotected governance parameters can be changed to 100% instantly, breaking the protocol.
Firepan detects access control vulnerabilities using:
1. Function Signature Analysis Flags functions that modify state (mint, burn, transfer, setOwner, etc.) without access control modifiers.
2. Call Graph Analysis Maps which functions can call which, detecting privilege escalation paths.
3. Role Permission Inference Verifies that role grants are consistent (roles aren't granted to unintended addresses).
4. Smart Contract Patterns Recognizes vulnerable patterns:
onlyOwner but aren't5. Historical Analysis Compares role assignments over time. If roles change unexpectedly, Firepan flags it.
Example Firepan Detection:
HIGH: Missing access control on mint()
Location: Token.sol:42
Function: mint(address to, uint amount)
Issue: Function can be called by anyone. No onlyOwner or role check.
Risk: Anyone can mint unlimited tokens, causing total loss.
Recommendation:
Add: modifier onlyRole(MINTER_ROLE) {require(hasRole(MINTER_ROLE, msg.sender));_;}
Or: Use OpenZeppelin's Ownable + onlyOwner modifier
Best Practice 1: Use OpenZeppelin AccessControl
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Token is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
}
function mint(address to, uint amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
function grantMinterRole(address account) public onlyRole(ADMIN_ROLE) {
grantRole(MINTER_ROLE, account);
}
}
OpenZeppelin's AccessControl is battle-tested and handles role inheritance correctly.
Best Practice 2: Separate Roles for Different Functions
// Good: fine-grained roles
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
function mint(...) public onlyRole(MINTER_ROLE) { ... }
function burn(...) public onlyRole(BURNER_ROLE) { ... }
function pause() public onlyRole(PAUSER_ROLE) { ... }
Principle of least privilege: each role should have minimal necessary permissions.
Best Practice 3: Use Multi-Sig for Critical Role Changes
function grantRole(bytes32 role, address account)
public
override
onlyRole(getRoleAdmin(role))
{
// Requires timelock or multi-sig for DEFAULT_ADMIN_ROLE changes
if (role == DEFAULT_ADMIN_ROLE) {
require(msg.sender == multiSigWallet, "only multisig");
}
super.grantRole(role, account);
}
Critical role changes should require multi-sig approval, not single owner.
Best Practice 4: Immutable Critical Parameters
contract Token is ERC20 {
address public immutable owner; // Cannot be changed after deployment
constructor(address _owner) {
owner = _owner;
}
}
If the owner doesn't need to change, make it immutable. Removes an entire class of attack.
Multi-Chain Access Control Cross-chain protocols need role synchronization across chains. If one chain's access control is compromised, attackers can exploit others.
Timelock for Governance Administrative changes should be timelocked (24–48 hour delay) so users can withdraw if they disagree:
contract GovernedToken is ERC20 {
uint constant TIMELOCK = 2 days;
uint public nextOwnerChange;
address public pendingOwner;
function proposeOwnerChange(address newOwner) public onlyOwner {
pendingOwner = newOwner;
nextOwnerChange = block.timestamp + TIMELOCK;
}
function executeOwnerChange() public {
require(block.timestamp >= nextOwnerChange);
owner = pendingOwner;
}
}
Q: Is Ownable or AccessControl better?
A: AccessControl is better for complex protocols with multiple roles. Ownable is simpler for single-owner tokens. For protocols protecting >$10M, use AccessControl with role-based permissions.
Q: How do I audit role assignments?
A: Log all role changes (OpenZeppelin's AccessControl emits events). Review role assignments quarterly. Use Firepan to detect unexpected role changes automatically.
Q: Should critical functions be timelocked?
A: Yes. Owner function changes, parameter updates, and role grants should all have a 24–48 hour timelock. This gives users time to withdraw if they distrust the change.
Q: Can I change the owner after deployment?
A: If necessary, require multi-sig approval and a timelock. Better: make the owner immutable or transfer to a DAO. Single-address owner is a systemic risk.
Q: How does Firepan detect access control issues?
A: Firepan analyzes function permissions, role assignments, and governance parameter changes. It detects missing modifiers, incorrect role grants, and privilege escalation paths. Start scanning at https://app.firepan.com/
Access control is foundational. Missing or misconfigured permissions are behind many of the largest DeFi exploits. Use OpenZeppelin, role-based access, multi-sig for critical changes, and continuous monitoring. Get it right from day one.
Start scanning at https://app.firepan.com/
Firepan
12,453 contracts secured. 2,851 vulnerabilities blocked. 236 exploits prevented. Run a free surface scan — results in minutes, no credit card required.
Run Free Scan →