Access Control in ERC-20 Smart Contracts: Detecting & Preventing Exploits

Firepan Security TeamApril 1, 2026

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.

Introduction

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.

Access Control Patterns in ERC-20

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.

Common Access Control Vulnerabilities

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.

Detection: How Firepan Finds Access Control Issues

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:

  • Functions that should be onlyOwner but aren't
  • Incorrect use of AccessControl
  • Race conditions in role assignment

5. 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

Prevention: Best Practices

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.

Access Control in DeFi: Special Considerations

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;
    }
}

Frequently Asked Questions

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/

Conclusion

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

Scan Your Contracts Now

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 →