So the fact the _; special symbol, that is a placeholder for the modified function, is after the computation has an impact on execution gas since the computation could be reverted due to the require() condition after the computation and storage operation done in the modifier has consumed some gas and it is important to observe the modified function require() does not depend on the result of this computation
To check this point let's create a simple example in Solidity
Let's take the following code
Contract1.sol
pragma solidity >=0.7.0 <0.9.0;
contract Test {
modifier test1 {
// Storage opeation just to consume some gas
number = 1;
_;
}
uint256 number;
function store(uint256 num) public test1 {
require(num > 0);
number = num;
}
}
This is similar to the analysed contract case since the modifier puts _; after some computation has been performed
Using Remix, let's compile and run this contract calling store(0) so that the TX gets reverted due to the require() check and let's check the execition gas
execution cost 20437 gas
Now let's slightly modify the contract as follows
Contract2.sol
pragma solidity >=0.7.0 <0.9.0;
contract Test {
modifier test1 {
// Storage opeation just to consume some gas
_;
number = 1;
}
uint256 number;
function store(uint256 num) public test1 {
require(num > 0);
number = num;
}
}
Let's compile and deploy the contract then calling store(0) again and check the gas
execution cost 423 gas
The execution gas is way lower
Let's dig even deeper at EVM Opcode level: on the left we have Contract1.sol and on the right we have Contract2.sol
The main difference is in the relative position of 3 sets of opcodes
Set 1
PUSH 1 1
PUSH 0 number
DUP2 number = 1
SWAP1 number = 1
SSTORE number = 1
POP number = 1
JUMPDEST require(num > 0)
DUP1 num
PUSH 0 number
DUP2 number = num
SWAP1 number = num
SSTORE number = num
POP number = num
this is related to number = num
On the left so in Contract1.sol we see the tag12 section contains both Set1 and Set2 while tag16 contains Set3
The Set1 is executed before the Set2 so if the JUMPI leads to REVERT then the previous expensive SSTORE needs to be reverted
On the right so in Contract2.sol we see the tag12 contains the Set2 only that is executed before tag16 that contains Set1 and Set3 so if the REVERT is reached then it was certainly executed before any SSTORE operation
Overview
Let's analyse the Issue I have opened here regarding gas efficiency
Let's take this function, but the same also applied to other functions following the same pattern
https://github.com/yaxis-project/metavault/blob/main/contracts/token/Rewards.sol#L195
It uses the
updateReward()
modifier that is defined as followshttps://github.com/yaxis-project/metavault/blob/main/contracts/token/Rewards.sol#L150
So the fact the
_;
special symbol, that is a placeholder for the modified function, is after the computation has an impact on execution gas since the computation could be reverted due to therequire()
condition after the computation and storage operation done in the modifier has consumed some gas and it is important to observe the modified functionrequire()
does not depend on the result of this computationTo check this point let's create a simple example in Solidity
Let's take the following code
Contract1.sol
This is similar to the analysed contract case since the modifier puts
_;
after some computation has been performedUsing Remix, let's compile and run this contract calling
store(0)
so that the TX gets reverted due to therequire()
check and let's check the execition gasNow let's slightly modify the contract as follows
Contract2.sol
Let's compile and deploy the contract then calling
store(0)
again and check the gasThe execution gas is way lower
Let's dig even deeper at EVM Opcode level: on the left we have
Contract1.sol
and on the right we haveContract2.sol
The main difference is in the relative position of 3 sets of opcodes
Set 1
this is related to
number = 1
andSet 2
this is related to
require(num > 0)
Set 3
this is related to
number = num
On the left so in
Contract1.sol
we see thetag12
section contains both Set1 and Set2 whiletag16
contains Set3 The Set1 is executed before the Set2 so if theJUMPI
leads toREVERT
then the previous expensiveSSTORE
needs to be revertedOn the right so in
Contract2.sol
we see thetag12
contains the Set2 only that is executed beforetag16
that contains Set1 and Set3 so if theREVERT
is reached then it was certainly executed before anySSTORE
operation