top of page
  • Writer's pictureShane Duncan

Solidity Functions — Everything You Need to Know About Modifiers

function (<parameter types>) {internal|external|public|private} [pure|constant|view|payable] [(modifiers)] [returns (<return types>)]


Solidity is a programming language specifically designed for writing smart contracts on the Ethereum blockchain. It provides a range of features to ensure that contracts can execute safely and efficiently. In this third function installment, we’ll be discussing function modifiers.

Function Modifiers in Solidity

Function modifiers are a way to modify the behavior of a function in Solidity. Modifiers are similar to regular functions in that they have a name, may have parameters, and contain code that is executed when called. Unlike functions, they cannot be called directly. Modifiers can add additional checks or constraints to a function before or after it executes. This can be useful for adding security features to a contract or ensuring that certain conditions are met before or after executing a function.

Here’s an example of a function modifier in Solidity:

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

In this example, we define a modifier called onlyOwner(). Note the use of the modifier keyword. This modifier checks that the msg.sender (i.e. the address that called the function) is the contract’s owner. If the check passes, the function continues to execute using the placeholder. What this means is that the calling function’s logic will be executed at the position of the . If the require check fails, the function is aborted and an exception is thrown.

At times, it may be necessary to perform certain actions after a function’s logic has been executed. Modifiers are very useful in this case. In this case, there is an added check to ensure that the msg.sender still has a balance after the modified function’s logic has executed.

modifier onlyOwnerAndFunded() {
    require(msg.sender == owner);
    _;
    require(balanceOf(msg.sender) > 0);
}

Wherever the is included in the modifier is where the modified function’s logic will be executed. The does not have to be the last line in the function and it may be repeated as necessary. This means that the logic of the function may be also repeated so it is very important to use caution with the _. In this example, the function logic will be repeated.

modifier doubleLogicModifier() {
    require(msg.sender == owner);
    _;
    _;
}

It is still acceptable to use additional require statements in the modified function. They do not all have to be in the modifier. In this example, the onlyOwner modifier is applied to the withdraw function. This ensures that only the owner of the contract can withdraw funds. The require statement inside the function adds an additional check only to this function to ensure that the requested amount is less than or equal to the balance of the contract.

function withdraw(uint amount) public onlyOwner {
    require(amount <= balance);
    balance -= amount;
    msg.sender.transfer(amount);
}

Modifiers can not be added to other modifiers but multiple modifiers can be applied to functions. This is very useful for validation and/or permission checks and helps to increase code modularity while decreasing redundancy. The modifiers will be executed in the order they are listed so in the following example, onlyOwner will be executed first. At first glance, this would seem problematic because one would assume that the function logic is executed each time the is encountered. However, Solidity is smart enough to know this would not be a desired result. Instead, if multiple modifiers are present, the code from the subsequent modifier is injected in the first modifier’s position. So the execution sequence is: onlyOwner, onlyEOA, and then the withdraw logic.

function withdraw(uint amount) public onlyOwner onlyEOA {
    require(amount <= balance);
    balance -= amount;
    msg.sender.transfer(amount);
}
modifier onlyOwner() {
    require(msg.sender == owner);
    _;}
modifier onlyEOA() {
    require(!msg.sender.code.length > 0);
    _;
}

Modifiers may also accept parameters. For instance, if we wanted to make the function require that the amount be less than or equal to balance, we could add:

function withdraw(uint amount) public onlyLessThanBalance(amount) {
	balance -= amount;
    msg.sender.transfer(amount);
}
modifier onlyLessThanBalance(uint256 amount) {
    require(amount <= balance);
    _;
}

Common Misconception

Several sources claim that modifiers cannot change storage variables. This may have been the case with older versions of Solidity but is no longer true. Modifiers can modify state variables (variables defined outside of function definitions) but cannot modify the calling function’s input parameters or return values. The Solidity Docs say, “Modifiers cannot implicitly access or change the arguments and return values of functions they modify. Their values can only be passed to them explicitly at the point of invocation.” However, modifying storage is allowed:

contract TestContract {
uint256 withdraws;
function withdraw(uint amount) public lessThanBalanceWithAdd(amount) {
    require(amount <= balance);
    balance -= amount;
    msg.sender.transfer(amount);
}
modifier lessThanBalanceWithAdd(uint256 amount) {
    require(amount <= balance);
    _;
    withdraws +=1;
}
}

Use Cases

Modifiers are commonly used for permissions, validation, fees, and reentrancy prevention. They can be used to effectively gate functions, with one of the most popular being OpenZeppelin’s onlyOwner found in Ownable.sol. Another common use can be to ensure that a proper amount of ETH is sent to functions.

modifier requireETH (uint fee) {
    if(msg.value < fee) revert NotEnoughETH();
    _;
}

Preventing reentrancy can be very helpful and OpenZeppelin provides a modifier, nonReentrant, to accomplish this. It uses pre and post function checks.

modifier nonReentrant() {
  _nonReentrantBefore();
  _;
  _nonReentrantAfter();
}

function _nonReentrantBefore() private {
  // On the first call to nonReentrant, _status will be NOT_ENTERED
  if (_status == ENTERED) {
     revert ReentrancyGuardReentrantCall();
  }
  // Any calls to nonReentrant after this point will fail
  _status = ENTERED;
}
function _nonReentrantAfter() private {
  // By storing the original value once again, a refund is triggered (see  https://eips.ethereum.org/EIPS/eip-2200)
  _status = NOT_ENTERED;
}

Gas Implications

Each time a modifier is used, the compiler creates a new function at compile time to cover it. If a modifier is used by many different functions, this can result in increased deployment costs and even push large contracts over the 24 kb limit. When a modifier is heavily used, it might be more economical to perform the same logic in a standard function or, in the very least, have the modifier call a standard function.

function withdraw(uint amount) public onlyBalanceLessThan100 {
    require(amount <= balance);
    balance -= amount;
    msg.sender.transfer(amount);
}
modifier onlyBalanceLessThan100() {
    checkBalanceLessThan100(); // call a function to perform the logic
    _;
}
// function containing modifier logic
function checkBalanceLessThan100() private {
uint256 balance;
for (uint256 i; i < tokenList.length; ) {
  balance += tokenList.balanceOf(msg.sender);
}
require(balance < 100);
}

Or you can forgo the modifier and use the check function directly which reduces the compiler created redundant functions.

function withdraw(uint amount) public {
    checkBalanceLessThan100(); // call check function directly
    require(amount <= balance);
    balance -= amount;
    msg.sender.transfer(amount);
}
// check function
function checkBalanceLessThan100() private {
uint256 balance;
for (uint256 i; i < tokenList.length; ) {
  balance += tokenList.balanceOf(msg.sender);
}
require(balance < 100);
}

Conclusion

Function modifiers are a powerful tool to increase modularity and reduce redundancy. They are useful for many common needs in Solidity but, like most things in life, they have a limit. Knowing when and how to use them helps code readability and gas efficiency.

Up Next

In the upcoming and final installment, I’ll be discussing special functions.


Comentarios


bottom of page