NicolaBernini / BlockchainAnalysis

Analysis of Blockchain related Stuff
3 stars 2 forks source link

Gas Efficiency Analysis 20210602 #14

Open NicolaBernini opened 3 years ago

NicolaBernini commented 3 years ago

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

function stake(uint256 amount) public override updateReward(msg.sender) 
{        
        require(amount > 0, "Cannot stake 0"); 
        super.stake(amount);        
        emit Staked(msg.sender, amount);    
}

https://github.com/yaxis-project/metavault/blob/main/contracts/token/Rewards.sol#L195

It uses the updateReward() modifier that is defined as follows

modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) 
        {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;    
}

https://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 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

image

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

this is related to number = 1 and

Set 2

      PUSH 0            0
      DUP2          num
      GT            num > 0
      PUSH [tag] 16         require(num > 0)
      JUMPI             require(num > 0)
      PUSH 0            require(num > 0)
      DUP1          require(num > 0)
      REVERT            require(num > 0)

this is related to require(num > 0)

Set 3

      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