BlockchainSolidity8 min readUpdated

Modifiers in Solidity: Reusable Function Wrappers

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Modifiers in Solidity: Reusable Function Wrappers

Section 01 · Definition

What is a modifier in Solidity?

A modifier is Solidity's way of writing a precondition once and applying it to many functions. It is the cleanest way to express access control and pausability.

Quick answer

What is a modifier? A modifier is a small piece of code that wraps a function. The body of the wrapped function is injected at the underscore (_) inside the modifier. Code before the underscore runs as a precondition; code after runs as a postcondition. Apply a modifier by writing its name after the function visibility — for example, function setFee(uint256 newFee) public onlyOwner { ... }.

solidity
address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "not owner");
    _;
}

function setFee(uint256 newFee) public onlyOwner {
    fee = newFee;
}

Two functions, twenty different ones, two hundred — they can all share the same onlyOwner gate without copy-pasting the require. That is the value of the keyword: one source of truth for the precondition, applied wherever it belongs.

Section 02 · Execution flow

What the underscore does

The underscore is a placeholder for the wrapped function body. Code before runs first, the body runs in the middle, code after runs last.

Execution flow diagram showing modifier preconditions, the underscore body injection point, and optional postconditions.
The underscore is where Solidity inserts the wrapped function. Anything before it is a precondition, anything after is a postcondition.
solidity
uint256 public callCount;

modifier track() {
    // 1. before
    callCount += 1;

    _;            // 2. wrapped function body runs here

    // 3. after
    emit Tracked(msg.sender, callCount);
}

function doWork() external track {
    // body — runs at the underscore
    work();
}

When multiple modifiers are stacked on a function, each one starts to run, hands off to the next at its underscore, and finishes once the inner work returns. The order is exactly left-to-right: function f() onlyOwner whenNotPaused noReentrant { ... } runs onlyOwner first, then whenNotPaused, then noReentrant, then the body, then the postconditions in reverse.

Section 03 · Access control

The patterns you will use most

Three modifier patterns that show up in nearly every audited contract: owner check, role check, and pause guard.

solidity
// 1. Single owner
modifier onlyOwner() {
    if (msg.sender != owner) revert NotOwner();
    _;
}

// 2. Role-based access (one role per check)
mapping(bytes32 => mapping(address => bool)) private _roles;

modifier onlyRole(bytes32 role) {
    if (!_roles[role][msg.sender]) revert AccessDenied(role, msg.sender);
    _;
}

// 3. Pause switch
bool public paused;

modifier whenNotPaused() {
    if (paused) revert Paused();
    _;
}

// Usage
function setFee(uint256 v) external onlyOwner whenNotPaused { fee = v; }
function mint(address to, uint256 amt) external onlyRole(MINTER_ROLE) { _mint(to, amt); }

Inside the check, prefer revert CustomError() over require(..., "message"). Custom errors are cheaper at deploy time and at runtime. The pattern uses bool flags and mappings to track who has access.

Don't write your own from scratch

OpenZeppelin's Ownable, AccessControl, Pausable, and ReentrancyGuard ship audited modifiers that solve every common case. Inheriting them is shorter, safer, and easier for an auditor to recognise. Write your own modifier only when no library version fits, and even then study the OpenZeppelin one first.

Section 04 · Reentrancy guard

The other modifier you cannot skip

Any function that sends ether or tokens through a low-level call should be protected by a reentrancy guard. The modifier shape is the standard way to do it.

solidity
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED     = 2;
uint256 private _status = _NOT_ENTERED;

modifier nonReentrant() {
    require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
    _status = _ENTERED;
    _;
    _status = _NOT_ENTERED;
}

function withdraw(uint256 amount) external nonReentrant {
    require(balances[msg.sender] >= amount, "insufficient");
    balances[msg.sender] -= amount;
    (bool ok, ) = payable(msg.sender).call{value: amount}("");
    require(ok, "transfer failed");
}

The modifier sets a flag before the body runs and clears it after. Any callback that tries to re-enter sees the flag set and reverts. This is exactly OpenZeppelin's ReentrancyGuard, and the same pattern blocks the entire class of attacks that drained the original DAO.

Section 06 · FAQ

Frequently asked questions

What is the underscore for in a Solidity modifier?

The underscore is a placeholder for the body of the wrapped function. When the modifier runs, code before the underscore executes first, then the function body is inserted at the underscore, then any code after the underscore runs. A modifier without an underscore would block the function from ever running.

Can a modifier modify state?

Technically yes — the underscore is just an injection point and you can write any code around it. In practice, keep modifiers limited to checks (require / revert). State changes belong in the function body so the contract is easier to audit and reason about.

What order do multiple modifiers run in?

Strictly left-to-right. function f() A B C { ... } runs A's pre-code, then B's pre-code, then C's pre-code, then the function body, then C's post-code, then B's post-code, then A's post-code. Order matters for security — put the cheapest or most decisive check first.

Should I use onlyOwner from OpenZeppelin or write my own?

Use OpenZeppelin's. It handles two-step ownership transfer to avoid the bug where you set the owner to the wrong address and lock yourself out. The Ownable contract is short, audited, and recognised on sight by any reviewer.

Can a modifier take arguments?

Yes. modifier onlyRole(bytes32 role) { ... } takes a role parameter so the same modifier can enforce different roles on different functions: function mint() onlyRole(MINTER_ROLE) { ... }. The arguments must be available at the call site, not state-dependent.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting serviceSee ChainTrust case study

Related service

Agentic AI Consulting

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →