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 { ... }.
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.
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.
// 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.
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.