Modifiers¶
Modifiers are reusable code that can run before and/or after a function.
Basic Modifiers¶
Defining a Modifier¶
contract Ownable {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // Continue with function body
}
constructor() {
owner = msg.sender;
}
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
}
The Placeholder _¶
The _ represents where the function body executes:
modifier beforeAndAfter() {
// Code here runs BEFORE function
doSomethingBefore();
_; // Function body executes here
// Code here runs AFTER function
doSomethingAfter();
}
Modifiers with Parameters¶
modifier minAmount(uint256 minimum) {
require(msg.value >= minimum, "Amount too low");
_;
}
modifier validAddress(address addr) {
require(addr != address(0), "Invalid address");
_;
}
modifier onlyRole(bytes32 role) {
require(hasRole(role, msg.sender), "Missing role");
_;
}
// Using parameterized modifiers
function deposit() public payable minAmount(1 ether) {
balances[msg.sender] += msg.value;
}
function transfer(address to, uint256 amount)
public
validAddress(to)
{
// ...
}
Common Modifier Patterns¶
Access Control¶
contract AccessControl {
address public owner;
mapping(address => bool) public admins;
mapping(address => bool) public operators;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
modifier onlyOperator() {
require(operators[msg.sender], "Not operator");
_;
}
modifier onlyAdminOrOwner() {
require(
msg.sender == owner || admins[msg.sender],
"Not authorized"
);
_;
}
}
Reentrancy Guard¶
contract ReentrancyGuard {
bool private locked;
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
// Transfer SOL...
}
}
Pausable¶
contract Pausable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
modifier whenPaused() {
require(paused, "Contract not paused");
_;
}
function pause() public onlyOwner whenNotPaused {
paused = true;
}
function unpause() public onlyOwner whenPaused {
paused = false;
}
function transfer(address to, uint256 amount)
public
whenNotPaused
{
// Only works when not paused
}
}
State Validation¶
contract Auction {
enum State { Created, Active, Ended }
State public state;
modifier inState(State expected) {
require(state == expected, "Invalid state");
_;
}
modifier notInState(State forbidden) {
require(state != forbidden, "Invalid state");
_;
}
function startAuction() public onlyOwner inState(State.Created) {
state = State.Active;
}
function bid() public payable inState(State.Active) {
// Process bid
}
function endAuction() public inState(State.Active) {
state = State.Ended;
}
}
Time-Based¶
contract TimeLock {
uint256 public unlockTime;
modifier onlyAfter(uint256 time) {
require(block.timestamp >= time, "Too early");
_;
}
modifier onlyBefore(uint256 time) {
require(block.timestamp < time, "Too late");
_;
}
function withdraw() public onlyAfter(unlockTime) {
// Can only withdraw after unlock time
}
function deposit() public payable onlyBefore(unlockTime) {
// Can only deposit before unlock time
}
}
Multiple Modifiers¶
Modifiers execute in order, left to right:
function sensitiveAction()
public
onlyOwner // First: check ownership
whenNotPaused // Second: check not paused
nonReentrant // Third: set reentrancy lock
{
// Function body runs last
}
Execution flow: 1. onlyOwner checks owner, reaches _ 2. whenNotPaused checks pause state, reaches _ 3. nonReentrant sets lock, reaches _ 4. Function body executes 5. nonReentrant post-code runs (unlock) 6. Control returns up the chain
Modifier Inheritance¶
contract Base {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
contract Derived is Base {
// Can use inherited modifier
function adminFunction() public onlyOwner {
// ...
}
// Can override modifier
modifier onlyOwner() override {
require(msg.sender == owner, "Derived: Not owner");
_;
}
}
Virtual Modifiers¶
contract Base {
modifier check() virtual {
require(condition(), "Check failed");
_;
}
function condition() internal view virtual returns (bool) {
return true;
}
}
contract Derived is Base {
modifier check() override {
require(condition(), "Derived check failed");
_;
}
function condition() internal view override returns (bool) {
return someOtherCondition;
}
}
Advanced Patterns¶
Modifier with Return Value Check¶
modifier checkReturn() {
_;
// Check something after function executes
require(lastOperationSuccessful, "Operation failed");
}
Combining with Events¶
modifier logged(string memory action) {
emit ActionStarted(msg.sender, action);
_;
emit ActionCompleted(msg.sender, action);
}
function importantAction() public logged("importantAction") {
// ...
}
Rate Limiting¶
contract RateLimited {
mapping(address => uint256) public lastAction;
uint256 public cooldown = 1 hours;
modifier rateLimited() {
require(
block.timestamp >= lastAction[msg.sender] + cooldown,
"Rate limited"
);
_;
lastAction[msg.sender] = block.timestamp;
}
function limitedAction() public rateLimited {
// Can only be called once per cooldown period
}
}
Best Practices¶
1. Keep Modifiers Simple¶
// Good - single responsibility
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// Avoid - too complex
modifier complexCheck() {
require(msg.sender == owner, "Not owner");
require(!paused, "Paused");
require(balance > 0, "No balance");
// Too many checks in one modifier
_;
}
2. Use Descriptive Names¶
// Good
modifier onlyOwner() { }
modifier whenNotPaused() { }
modifier validAddress(address addr) { }
// Avoid
modifier check1() { }
modifier mod() { }
3. Order Modifiers Logically¶
// Good - logical order
function action() public onlyOwner whenNotPaused nonReentrant {
// Access -> State -> Safety
}
4. Document Modifier Behavior¶
/// @notice Restricts function to contract owner
/// @dev Reverts with "Not owner" if caller is not owner
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
5. Be Careful with State Changes in Modifiers¶
// Caution - modifies state
modifier countCalls() {
callCount++; // State change in modifier
_;
}
// Usually better to be explicit in function
function action() public {
callCount++; // Clear state change
// ...
}
Next Steps¶
- Interfaces - Contract interfaces
- Inheritance - Contract inheritance
- Events & Errors - Error handling