Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gas Optimizations #19

Closed
0xMilenov opened this issue Feb 6, 2024 · 1 comment
Closed

Gas Optimizations #19

0xMilenov opened this issue Feb 6, 2024 · 1 comment

Comments

@0xMilenov
Copy link
Contributor

0xMilenov commented Feb 6, 2024

Issue Instances Gas Savings
[G-01] Missing validation check in redeemReward 2 6
[G-02] Optimize gas by using != 0 over > 0 in require() for unsigned integers 9 54
[G-03] a = a + b consumes less gas than a += b for state variables 7 112
[G-04] Use assembly to validate msg.sender 7 84
[G-05] Store array length outside loops for efficiency 3 9
[G-06] Using assembly to check for zero can save gas 7 49
[G-07] Revert strings are less gas-efficient than custom errors 14 700
[G-08] Using a double if statement instead of a logical AND(&&) 1 30
[G-09] Use shift right/left instead of division/multiplication if possible 2 44
[G-10] Upgrade to a newer Solidity version. 21 N/A
[G-11] Increments/decrements can be unchecked in for-loops 5 125
[G-12] Separate require() conditions for gas efficiency 2 6

[G-01] Missing validation check in redeemReward

Context StabilityPool

Impact Users with a zero deposit are allowed to execute the function, potentially causing unnecessary gas expenditure.

Description

function redeemReward() external {
    Snapshots memory snapshots = depositSnapshots[msg.sender];
    uint256 contributorDeposit = deposits[msg.sender];

    uint256 compoundedDeposit = _getCompoundedDepositFromSnapshots(contributorDeposit, snapshots);
    _redeemReward();
    _updateDepositAndSnapshots(msg.sender, compoundedDeposit);
}

Recomendation. Add validation check.

function redeemReward() external {
    Snapshots memory snapshots = depositSnapshots[msg.sender];
    uint256 contributorDeposit = deposits[msg.sender];
+  require(contributorDeposit > 0, "deposit-is-0")

    uint256 compoundedDeposit = _getCompoundedDepositFromSnapshots(contributorDeposit, snapshots);
    _redeemReward();
    _updateDepositAndSnapshots(msg.sender, compoundedDeposit);
}

[G-02] Optimize gas by using != 0 over > 0 in require() for unsigned integers

For contracts using Solidity versions up to 0.8.13, there's a slight gas optimization to be had when using != 0 instead of > 0 in require() checks on unsigned integers.

When the optimizer is enabled, the != 0 check consumes 6 gas units less. Although it might appear that > 0 is more gas-efficient, this holds true only without the optimizer and outside of require statements.

For contracts where every gas unit matters, consider this optimization.

Reference: Tweet by @gzeon.

Ensure the Optimizer is enabled for maximal benefit.

File: ~All files

     require(_amount  > 0, "amount-is-0");
+   require(_amount  != 0, "amount-is-0");

[G-03] a = a + b consumes less gas than a += b for state variables, except for arrays and mappings

File: ~All files / Example with AuctionManager contract

}144:       _totalCollateralValue += _collateralValue;
}180:       _totalCollateralValue += _collateralValue;
+         _totalCollateralValue = _totalCollateralValue + _collateralValue;
+         _totalCollateralValue = _totalCollateralValue + _collateralValue;

144, 180

[G-04] Use assembly to validate msg.sender

We can use assembly to efficiently validate msg.sender with the least amount of opcodes necessary. For more details check the following report here

File: contracts/AuctionManager.sol

}156:      require(msg.sender == address(liquidationRouter), "not-allowed");

156

File: contracts/LiquidationRouter.sol

}84:       require(msg.sender == stabilityPool, "not-allowed");
}89:       require(msg.sender == lastResortLiquidation, "not-last-resort-liquidation");

84, 89

[G-05] Store array length outside loops for efficiency

When array length isn't cached, the Solidity compiler repeatedly reads it in every loop iteration.

For storage arrays, this means an added sload operation costing 100 extra gas for each subsequent iteration.

For memory arrays, it's an additional mload operation costing 3 extra gas post the first iteration.

File: contracts/LiquidationRouter.sol

}125:      for (uint256 i = 0; i <  collateralSet.length(); i++) {

125

File: contracts/StabilityPool.sol

}276:       for (uint128 i = 0; i <  _snapshots.tokenToSArray.length; i++) {
}285:       for (uint128 i = 0; i <  nextTokensToSum_cached.length; i++) {
}424:       for (uint128 i = 0; i <  currentTokenToSArray.length; i++) {
}552:       for (uint128 j = 0; j <  _snapshots.tokenToSArray.length; j++) {
}561:       for (uint128 j = 0; j <  nextTokensToSum_cached.length; j++) {
}758:       for (uint256 i = 0; i <  _depositorCollateralGains.length; i++) {

276, 285, 424, 552, 561, 758

File: contracts/Vault.sol

}177:       for (uint256 i = 0; i <  collateralSet.length(); i++) {
}385:       for (uint256 i = 0; i <  collateralSet.length(); i++) {
}408:       for (uint256 i = 0; i <  collateralSet.length(); i++) {

177, 385, 408

[G-06] Using assembly to check for zero can save gas

Using assembly to check for zero can save gas by allowing more direct access to the evm and reducing some of the overhead associated with high-level operations in solidity.

File: ~All files / Example with BONQMath.sol

}67:       if (_minutes == 0) {
}77:       if (n % 2 == 0) {

67, 77

[G-07] Revert strings are less gas-efficient than custom errors

From Solidity v0.8.4 onward, custom errors have been introduced.

These errors provide a savings of approximately 50 gas each instance they're triggered, as they circumvent the need to allocate and keep the revert string.

Omitting these strings also conserves gas during deployment.

Furthermore, custom errors are versatile, applicable both inside and outside contracts, including in interfaces and libraries.

As stated in the Solidity blog:

With the introduction of Solidity v0.8.4, a streamlined and gas-efficient method has been provided to elucidate to users the reasons behind an operation's failure through custom errors.

Prior to this, although strings could be used to detail failure reasons (e.g., revert('Insufficient funds.');), they were notably costlier, especially in terms of deployment, and incorporating dynamic information into them was challenging.

It's advisable to transition all revert strings to custom errors in your solution, especially focusing on those that appear multiple times.

File: ~All files

        require(...

[G-08] Using a double if statement instead of a logical AND(&&)

Using a double if statement instead of a logical AND (&&) can provide similar short-circuiting behavior whereas double if is slightly more gas efficient.

File: contracts/utils/linked-address-list.sol

}46:         if (_before && (_reference == address(0x0) || _reference == _list._first)) {
}53:         } else if (!_before && (_reference == address(0x0) || _reference == _list._last)) {
}91:         if (_element == _list._last && _element == _list._first) {

46, 53, 91

[G-09] Use shift right/left instead of division/multiplication if possible

While the DIV / MUL opcode uses 5 gas, the SHR / SHL opcode only uses 3 gas. Furthermore, beware that Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting. Eventually, overflow checks are never performed for shift operations as they are done for arithmetic operations. Instead, the result is always truncated, so the calculation can be unchecked in Solidity version 0.8+

  • Use >> 1 instead of / 2
  • Use >> 2 instead of / 4
  • Use << 3 instead of * 8
  • ...
  • Use >> 5 instead of / 2^5 == / 32
  • Use << 6 instead of * 2^6 == * 64

TL;DR:

  • Shifting left by N is like multiplying by 2^N (Each bits to the left is an increased power of 2)
  • Shifting right by N is like dividing by 2^N (Each bits to the right is a decreased power of 2)

Saves around 2 gas + 20 for unchecked per instance

File: contracts/utils/BONQMath.sol

}47:         decProd = (prod_xy + (DECIMAL_PRECISION / 2)) / DECIMAL_PRECISION;
}79:         n = n / 2;
}84:         n = (n - 1) / 2;

47, 79, 84

File: contracts/utils/constants.sol

}21:     uint256 public constant PERCENT_05 = PERCENT / 2; // Represents 0.5%

21

[G-10] Upgrade to a newer Solidity version.

Using a more recent version of Solidity offers various benefits, including enhanced functionality, bug fixes, and potential gas savings. It ensures your smart contracts are more secure and compatible with the latest tools and libraries

File: ~All files

}2: pragma solidity ^0.8.4;

[G-11] Increments/decrements can be unchecked in for-loops

In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.

ethereum/solidity#10695

The change would be:

- for (uint256 i; i <  numIterations; i++) {
+ for (uint256 i; i <  numIterations;) {
 // ...  
+   unchecked { ++i; }
}  

These save around 25 gas saved per instance.

The same can be applied with decrements (which should use break when i == 0).

The risk of overflow is non-existent for uint256.

File: ~All files / Example with AuctionManager.sol

}170:         for (uint256 i = 0; i <  _collateralsLength; i++) {
}259:         for (uint256 i = 0; i <  _collateralsLength; i++) {
}299:         for (uint256 i = 0; i <  _collateralsLength; i++) {

170, 259, 299

[G-12] Separate require() conditions for gas efficiency

It's beneficial to separate conditions in require() statements rather than using the && operator.

As outlined in this discussed issue, while there might be a slightly higher deployment gas cost initially, the overall gas savings in runtime calls makes this approach more cost-effective in the long run.

Implementing this change can result in a saving of approximately 3 gas per instance.

File: contracts/AuctionManager.sol

}232:      require(!_auction.auctionEnded && block.timestamp < = _auction.auctionEndTime, "auction-ended");

232

File: contracts/TokenToPriceFeed.sol

}92:       require(_mlr  >= 100 && _mlr < = _mcr, "MLR <  100 or MLR  > MCR");

92

@0xMilenov
Copy link
Contributor Author

0xMilenov commented Feb 6, 2024

@antoniopel @CristianRicharte6
I wanted to contribute to the protocol and make all these Gas Optimisations issues.
All are free of charge to show you my respect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants