An array's length should be cached to save gas in for-loops
Reading array length at each iteration of the loop consumes more gas than necessary.
In the best case scenario (length read on a memory variable), caching the array length in the stack saves around 3 gas per iteration.
In the worst case scenario (external calls at each iteration), the amount of gas wasted can be massive.
Here, I suggest storing the array's length in a variable before the for-loop, and use this new variable instead:
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:471: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:508: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:515: for (uint256 j = 0; j < externalPositions.length; j++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:550: for (uint256 i = 0; i < _components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:590: for (uint256 i = 0; i < _components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:656: for (uint256 i = 0; i < issuanceHooks.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:666: for (uint256 i = 0; i < issuanceHooks.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:238: for(uint256 i = 0; i < modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:254: for(uint256 i = 0; i < modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:176: for (uint256 i = 0; i < _modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:181: for (uint256 j = 0; j < _components.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:485: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:502: for (uint256 j = 0; j < externalModules.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:527: for (uint256 i = 0; i < externalModules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:604: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:614: for (uint256 j = 0; j < externalModules.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:636: for (uint256 i = 0; i < components.length; i++) {
++i costs less gas compared to i++ or i += 1 (same for --i vs i-- or i -= 1)
Pre-increments and pre-decrements are cheaper.
For a uint256 i variable, the following is true with the Optimizer enabled at 10k:
Increment:
i += 1 is the most expensive form
i++ costs 6 gas less than i += 1
++i costs 5 gas less than i++ (11 gas less than i += 1)
Decrement:
i -= 1 is the most expensive form
i-- costs 11 gas less than i -= 1
--i costs 5 gas less than i-- (16 gas less than i -= 1)
Note that post-increments (or post-decrements) return the old value before incrementing or decrementing, hence the name post-increment:
uint i = 1;
uint j = 2;
require(j == i++, "This will be false as i is incremented after the comparison");
However, pre-increments (or pre-decrements) return the new value:
uint i = 1;
uint j = 2;
require(j == ++i, "This will be true as i is incremented before the comparison");
In the pre-increment case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2.
Affected code:
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:471: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:508: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:515: for (uint256 j = 0; j < externalPositions.length; j++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:550: for (uint256 i = 0; i < _components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:590: for (uint256 i = 0; i < _components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:656: for (uint256 i = 0; i < issuanceHooks.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:666: for (uint256 i = 0; i < issuanceHooks.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:689: for (uint256 i = 0; i < modulesLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:693: for (uint256 i = 0; i < modulesLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:238: for(uint256 i = 0; i < modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:254: for(uint256 i = 0; i < modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:393: for(uint256 i = 0; i < positionsLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:605: for(uint256 i = 0; i < positionsLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:610: numFCashPositions++;
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:618: for(uint256 i = 0; i < positionsLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:623: j++;
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:176: for (uint256 i = 0; i < _modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:181: for (uint256 j = 0; j < _components.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:485: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:498: positionCount++;
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:502: for (uint256 j = 0; j < externalModules.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:513: positionCount++;
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:527: for (uint256 i = 0; i < externalModules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:604: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:614: for (uint256 j = 0; j < externalModules.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:636: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:641: positionCount++;
Consider using pre-increments and pre-decrements where they are relevant (meaning: not where post-increments/decrements logic are relevant).
abi.encode() is less efficient than abi.encodePacked()
Changing abi.encode function to abi.encodePacked can save gas since the abi.encode function pads extra null bytes at the end of the call data, which is unnecessary. Also, in general, abi.encodePacked is more gas-efficient (see Solidity-Encode-Gas-Comparison).
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
As an example: for (uint256 i = 0; i < numIterations; ++i) { should be replaced with for (uint256 i; i < numIterations; ++i) {
Affected code:
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:471: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:508: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:511: int256 cumulativeDebt = 0;
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:515: for (uint256 j = 0; j < externalPositions.length; j++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:550: for (uint256 i = 0; i < _components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:590: for (uint256 i = 0; i < _components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:656: for (uint256 i = 0; i < issuanceHooks.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:666: for (uint256 i = 0; i < issuanceHooks.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:689: for (uint256 i = 0; i < modulesLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:693: for (uint256 i = 0; i < modulesLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:238: for(uint256 i = 0; i < modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:254: for(uint256 i = 0; i < modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:393: for(uint256 i = 0; i < positionsLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:519: uint32 minImpliedRate = 0;
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:605: for(uint256 i = 0; i < positionsLength; i++) {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:618: for(uint256 i = 0; i < positionsLength; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:176: for (uint256 i = 0; i < _modules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:181: for (uint256 j = 0; j < _components.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:483: uint256 positionCount = 0;
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:485: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:502: for (uint256 j = 0; j < externalModules.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:527: for (uint256 i = 0; i < externalModules.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:604: for (uint256 i = 0; i < components.length; i++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:614: for (uint256 j = 0; j < externalModules.length; j++) {
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:636: for (uint256 i = 0; i < components.length; i++) {
I suggest removing explicit initializations for default values.
Upgrade pragma to at least 0.8.4
Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.
The advantages here are:
Low level inliner (>= 0.8.2): Cheaper runtime gas (especially relevant when the contract has small functions).
Optimizer improvements in packed structs (>= 0.8.3)
Custom errors (>= 0.8.4): cheaper deployment cost and runtime cost. Note: the runtime cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors.
Reduce the size of error messages (Long revert Strings)
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Revert strings > 32 bytes:
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:220: require(_newFeeRecipient != address(0), "Fee Recipient must be non-zero address.");
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:317: require(_managerIssueFee <= _maxManagerFee, "Issue fee can't exceed maximum fee");
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:318: require(_managerRedeemFee <= _maxManagerFee, "Redeem fee can't exceed maximum fee");
index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol:336: require(issuanceSettings[ISetToken(msg.sender)].moduleIssuanceHooks.length == 0, "Registered modules must be removed.");
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:169: require(_setToken.isComponent(address(_sendToken)), "Send token must be an index component");
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:199: require(_setToken.isComponent(address(wrappedfCash)), "FCash to redeem must be an index component");
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:378: require(wrappedfCashAddress.isContract(), "WrappedfCash not deployed for given parameters");
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:573: require(_paymentToken == assetToken, "Token is neither asset nor underlying token");
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:565: revert("Real to Virtual unit conversion invalid");
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:570: revert("Virtual to Real unit conversion invalid");
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:676: "Module must be enabled on controller"
index-coop-notional-trade-module/contracts/protocol/SetToken.sol:686: require(msg.sender == locker, "When locked, only the locker can call");
I suggest shortening the revert strings to fit in 32 bytes.
Use Custom Errors instead of Revert Strings to save Gas
Solidity 0.8.4 introduced custom errors. They are more gas efficient than revert strings, when it comes to deploy cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for gas savings.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
I suggest replacing all revert strings with custom errors in the following files:
Functions guaranteed to revert when called by normal users can be marked payable
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:279: function updateAllowedSetToken(ISetToken _setToken, bool _status) external onlyOwner {
index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol:289: function updateAnySetAllowed(bool _anySetAllowed) external onlyOwner {
Table of Contents:
++i
costs less gas compared toi++
ori += 1
(same for--i
vsi--
ori -= 1
)abi.encode()
is less efficient thanabi.encodePacked()
payable
An array's length should be cached to save gas in for-loops
Reading array length at each iteration of the loop consumes more gas than necessary.
In the best case scenario (length read on a memory variable), caching the array length in the stack saves around 3 gas per iteration. In the worst case scenario (external calls at each iteration), the amount of gas wasted can be massive.
Here, I suggest storing the array's length in a variable before the for-loop, and use this new variable instead:
++i
costs less gas compared toi++
ori += 1
(same for--i
vsi--
ori -= 1
)Pre-increments and pre-decrements are cheaper.
For a
uint256 i
variable, the following is true with the Optimizer enabled at 10k:Increment:
i += 1
is the most expensive formi++
costs 6 gas less thani += 1
++i
costs 5 gas less thani++
(11 gas less thani += 1
)Decrement:
i -= 1
is the most expensive formi--
costs 11 gas less thani -= 1
--i
costs 5 gas less thani--
(16 gas less thani -= 1
)Note that post-increments (or post-decrements) return the old value before incrementing or decrementing, hence the name post-increment:
However, pre-increments (or pre-decrements) return the new value:
In the pre-increment case, the compiler has to create a temporary variable (when used) for returning
1
instead of2
.Affected code:
Consider using pre-increments and pre-decrements where they are relevant (meaning: not where post-increments/decrements logic are relevant).
abi.encode()
is less efficient thanabi.encodePacked()
Changing
abi.encode
function toabi.encodePacked
can save gas since theabi.encode
function pads extra null bytes at the end of the call data, which is unnecessary. Also, in general,abi.encodePacked
is more gas-efficient (see Solidity-Encode-Gas-Comparison).Consider using
abi.encodePacked()
here:No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (
0
foruint
,false
forbool
,address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.As an example:
for (uint256 i = 0; i < numIterations; ++i) {
should be replaced withfor (uint256 i; i < numIterations; ++i) {
Affected code:
I suggest removing explicit initializations for default values.
Upgrade pragma to at least 0.8.4
Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.
The advantages here are:
Consider upgrading pragma to at least 0.8.4:
Reduce the size of error messages (Long revert Strings)
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Revert strings > 32 bytes:
I suggest shortening the revert strings to fit in 32 bytes.
Use Custom Errors instead of Revert Strings to save Gas
Solidity 0.8.4 introduced custom errors. They are more gas efficient than revert strings, when it comes to deploy cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for gas savings.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Custom errors are defined using the
error
statement, which can be used inside and outside of contracts (including interfaces and libraries).I suggest replacing all revert strings with custom errors in the following files:
Functions guaranteed to revert when called by normal users can be marked
payable
If a function modifier such as
onlyOwner
is used, the function will revert if a normal user tries to pay the function. Marking the function aspayable
will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.