Solidity provides us with special functions called modifiers that allow us to extend our standard functions with reusable code in a declarative way. Well, what does this mean? Let’s take a look at an example:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract Modifiers {
    address public owner;
    uint public value;
    
    constructor() {
        owner = msg.sender;
    }

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

    function changeValue(uint _value) public onlyOwner() {
        value = _value;
    }
}

This is a very common example for using a modifier: Restricting the access to critical functions to certain users. Here, we have a public state variable (value) and we only want to allow the owner of the contract to change that value.

In the constructor, we define the owner of the contract as the address of the account that is deploying the contract (msg.sender).

Then, we create our special function “onlyOwner” by using the “modifier” keyword. In the function body, we define, what our modifier should do. Here, we require (by using the “require” keyword) that the account address that is calling this function/modifier equals our state variable “owner, which we set to the address of the deployer account.

Just a reminder: “msg.sender” allows you to access the address that called the current function.

So, if the address of the caller is different from the “owner” address, the “require” statement will fail and return the provided error message (You are not authorized). The reminder of the function body will not be executed and if any state variables would have already been changed (which is not the case in our example) those changes would be reverted.

What does the following mean: _; ?

If the require statement in the modifier passes, the underscore is replaced with the code of the function that uses the modifier and that code will be executed.

Using modifiers on smart contract functions…

Now, let’s use our modifier on a function. We mentioned above that we can use modifiers in a declarative way, and that’s exactly what we are doing in our example. We are adding the “onlyOwner” modifier next to the public keyword in the declaration of our “changeValue” function.

When someone calls the “changeValue” function, the code defined in the “onlyOwner” modifier will be executed first. If the specified condition passes ‘(require…), we hit the line with the underscore, which simply means: execute the code of our function. And, we change the value of our state variable with the provided value (_value).

Our example is very simple, there is only one function and defining a modifier for that single function does not provide any benefit for our code. However, if we are writing a more complex smart contract with several function that should only be accessible by the owner of the contract, then it makes sense to use a modifier.

Modifiers are typically used to check specific conditions before executing a function and they provide reusable code for other functions, which reduces the amount of code we have to write. Also, it makes our code more readable and easier to maintain.

Let’s take a look at a slightly more complex example:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract Modifiers {
    address public owner;
    uint public electionEndTime;
    bool public electionEnded = false;

    constructor(uint votingTime) {  
        owner = msg.sender;     
        electionEndTime = block.timestamp + votingTime;
    }

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

    modifier onlyAfter(uint time) {
        require(block.timestamp > time, "Too early!");
        _;
    }

    function endElection() public onlyOwner() onlyAfter(electionEndTime) {
        electionEnded = true;
    }
}

Here, we define a second modifier: “onlyAfter” that allows us to verify if a specific delay has passed. We also have a function “endElection” that allows the owner of the contract to end the election, but only, if the “votingTime” specified in the constructor has passed.

The special variable “block.timestamp” provides us with the current timestamp in seconds since unix epoch (1/1/1970). This means, the “votingTime” timespan provided in the constructor needs to be specified in seconds. If the votingTime should be one week, we can write: votingTime = 60 * 60 * 24 * 7;

You will also have realized that we are specifying both modifiers in the declaration of the “endElection” function. You can specify several modifiers in your function declarations. The modifiers need to be separated by a single whitespace character and they are executed in the specified order.

And, as in our example, modifiers can take one or more arguments. Here, we set the “electionEndTime” state variable in the constructor and we are using that constant value for our “onlyAfter” modifier.

Finally, modifiers can also be overridden in derived contracts. You have to use the “virtual” keyword in the base contract and the “override” keyword in the derived contract.