iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Access Control

Access control on smart contracts decides who can mint, pause, upgrade, or change parameters. OpenZeppelins Ownable, AccessControl, and AccessManager cover most cases. Pair with a multisig + timelock for production owners; pair with role-based admin chains for multi-team protocols.

Ownable, AccessControl, multisig + timelock

EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// ============================================================
// 1) Ownable — simplest: single owner can do privileged actions
// ============================================================
import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol';

contract Treasury is Ownable {
    constructor(address admin) Ownable(admin) {}

    function withdraw(address payable to, uint256 amount) external onlyOwner {
        to.transfer(amount);
    }
}

// ============================================================
// 2) AccessControl — role-based admin with optional admin-of-admin
// ============================================================
import { AccessControl } from '@openzeppelin/contracts/access/AccessControl.sol';

contract Token is AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256('MINTER_ROLE');
    bytes32 public constant PAUSER_ROLE = keccak256('PAUSER_ROLE');

    constructor(address admin) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(MINTER_ROLE, admin);
        _grantRole(PAUSER_ROLE, admin);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        // _mint(to, amount);   // from ERC20
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        // _pause();             // from Pausable
    }
}

// DEFAULT_ADMIN_ROLE can grant + revoke any role by default.
// Lock that role to a multisig in production:
//   token.grantRole(DEFAULT_ADMIN_ROLE, multisig);
//   token.revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);

// ============================================================
// 3) Multisig + Timelock — the production owner pattern
// ============================================================
// Gnosis Safe (multisig): N-of-M signers approve any tx.
// TimelockController:     queued tx waits T seconds before execution.
//
// Architecture:
//   token.owner = Timelock
//   Timelock proposers = MultisigA
//   Timelock executors = MultisigB (or address(0) = anyone)
//
// Workflow:
//   1) Multisig A queues a tx in Timelock (e.g. pause())
//   2) Wait min delay (e.g. 24-48h)
//   3) Anyone can execute() the queued tx
//
// Effect:
//   - No single signer can rug
//   - A compromised admin key has at least 24h before damage
//   - Users have time to exit if a malicious proposal is queued

import { TimelockController } from '@openzeppelin/contracts/governance/TimelockController.sol';

contract MyTimelock is TimelockController {
    constructor(uint256 minDelay, address[] memory proposers, address[] memory executors)
        TimelockController(minDelay, proposers, executors, address(0)) {}
}

// ============================================================
// 4) AccessManager — single contract managing roles across many targets
// ============================================================
// OZ AccessManager (v5+) centralises role decisions for an entire protocol.
// Each protected function declares a 'role'; AccessManager decides who has it.
// Useful when you have 10+ contracts that need consistent role policy.

import { AccessManager } from '@openzeppelin/contracts/access/manager/AccessManager.sol';

// AccessManager exposes:
//   grantRole(roleId, account, executionDelay)
//   restrict + execute helpers
//   per-target function -> role mappings

// ============================================================
// 5) Tests (Foundry) — verify access control
// ============================================================
import 'forge-std/Test.sol';

contract AccessTest is Test {
    Token token;
    address admin = makeAddr('admin');
    address user  = makeAddr('user');

    function setUp() public {
        vm.prank(admin);
        token = new Token(admin);
    }

    function test_only_minter_can_mint() public {
        vm.prank(admin);
        token.mint(user, 100);  // ok

        vm.prank(user);
        vm.expectRevert();
        token.mint(user, 100);
    }

    function test_admin_can_grant_minter() public {
        bytes32 role = token.MINTER_ROLE();
        vm.prank(admin);
        token.grantRole(role, user);
        vm.prank(user);
        token.mint(user, 100);
    }
}

// ============================================================
// 6) Decision matrix
// ============================================================
// Single owner, simple contract                     -> Ownable + multisig
// Many privileged operations, distinct teams         -> AccessControl
// Time-delayed admin actions, user trust             -> + Timelock
// Many contracts sharing policy                       -> AccessManager
// Decentralised governance                            -> Governor + Timelock + token voting

// ============================================================
// 7) Pitfalls
// ============================================================
// - DEFAULT_ADMIN_ROLE left on the deployer EOA -> a leaked key compromises everything
// - No timelock on admin actions -> rug risk
// - Forgetting to revoke the deployer role after grant_role(multisig)
// - Mixing Ownable + AccessControl unpredictably
// - Hardcoded role bytes32 instead of keccak256('NAME') -> typos compile

Why it matters

A production smart-contract owner is a multisig behind a timelock — not an EOA you control. The combo means no single compromised key can move funds or change parameters instantly, and users get time to exit on a malicious proposal. Get the deploy script right once and the protocol becomes structurally harder to rug.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// OpenZeppelin AccessControl gives you role-based gating.
import "@openzeppelin/contracts/access/AccessControl.sol";
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
function mint(address to, uint256 id) external onlyRole(MINTER_ROLE) { _mint(to, id); }
Try it Yourself »

Discussion

Loading…