DEV Community

Heemin Kim
Heemin Kim

Posted on • Originally published at contract-scanner.raccoonworld.xyz

How Access Control Mistakes Led to $1.4B in Losses

How Access Control Mistakes Led to $1.4B in Losses

As of 2025, the #1 loss category in smart contract security incidents is access control vulnerabilities. Three landmark cases alone — Poly Network, Ronin Bridge, and Nomad Bridge — account for over $1.4B in combined losses. These are not complex mathematical exploits — they stem from simply failing to verify "who can call this function."

What Is an Access Control Vulnerability?

Admin-only functions (mint, pause, upgrade, transfer ownership, etc.) that lack proper access controls, allowing anyone to call them.

Vulnerable Code

contract VulnerableToken {
    address public owner;

    // ⚠️ No onlyOwner check — anyone can mint
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }

    // ⚠️ Initializer is unprotected
    function initialize(address _owner) external {
        owner = _owner;
    }
}
Enter fullscreen mode Exit fullscreen mode

Case 1: Poly Network (2021, ~$611M)

The cross-chain relay's keeper change function was insufficiently protected, allowing the attacker to register themselves as a keeper and withdraw funds from all chains.

Root cause: verifyHeaderAndExecuteTx() allowed arbitrary contract calls through cross-chain message validation.

Source: Rekt News — Poly Network

Case 2: Ronin Bridge (2022, ~$625M)

Five out of nine validator private keys for Axie Infinity's Ronin Bridge were compromised. This exceeded the multisig threshold, enabling bridge fund withdrawal.

Root cause: Too few validators, some keys stored on shared infrastructure. Vulnerable to social engineering.

Source: Rekt News — Ronin

Case 3: Nomad Bridge (2022, ~$190M)

During an upgrade, the trusted root was initialized to 0x00. All messages were automatically treated as valid, allowing anyone to withdraw bridge funds.

Root cause: Incorrect parameters in the initialize() call set the Merkle root to zero.

Source: Rekt News — Nomad

Defense 1: Ownable Pattern

import "@openzeppelin/contracts/access/Ownable.sol";

contract SafeToken is Ownable {
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Defense 2: Role-Based Access Control (RBAC)

import "@openzeppelin/contracts/access/AccessControl.sol";

contract SafeToken is AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Defense 3: Initializer Protection

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract SafeProxy is Initializable {
    function initialize(address _owner) external initializer {
        // initializer modifier prevents re-calling
        _transferOwnership(_owner);
    }
}
Enter fullscreen mode Exit fullscreen mode

Defense 4: Timelock

Add a delay before admin function execution, giving the community time to detect and respond to malicious changes.

import "@openzeppelin/contracts/governance/TimelockController.sol";
Enter fullscreen mode Exit fullscreen mode

Checklist

  • [ ] Do all admin/owner functions have access control modifiers?
  • [ ] Does initialize() use the initializer modifier?
  • [ ] Is the upgrade function (upgradeTo) protected?
  • [ ] Is multi-sig or a timelock applied?
  • [ ] Are private keys stored in a distributed manner?

Detecting These Issues with ContractScan

Slither's unprotected-upgrade and Semgrep's missing-access-control rules automatically detect missing access controls.
ContractScan Free Scan

Top comments (0)