The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
56: if (owner != msg.sender) { //@audit gas: SLOAD (owner), this is important here as it impacts both the happy and sad paths
62: revert CustomErrors.EXECUTION_NOT_AUTHORIZED(owner, msg.sender, target, selector); //@audit gas: SLOAD (owner)
72: address owner_ = owner; //@audit gas: SLOAD (owner)
82: if (owner_ != owner) { //@audit gas: SLOAD (owner)
83: revert CustomErrors.OWNER_CHANGED(owner_, owner); //@audit gas: SLOAD (owner)
2. Multiple accesses of a mapping/array should use a local variable cache
Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times saves ~42 gas per access due to not having to perform the same offset calculation every time.
3. Use of the memory keyword when storage should be used
When copying a state struct in memory, there are as many SLOADs and MSTOREs as there are slots. When reading the whole struct multiple times is not needed, it's better to actually only read the relevant field(s). When only some of the fields are read several times, these particular values should be cached instead of the whole state struct.
Consider using a storage pointer instead of memory location here:
132: if (collateralBalanceAfter > flashloanRepayAmount) {
133: token.safeIncreaseAllowance(address(core), collateralBalanceAfter - flashloanRepayAmount); //@audit should be unchecked due to L132
7. <array>.length should not be looked up in every loop of a for-loop
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, consider storing the array's length in a variable before the for-loop, and use this new variable instead:
proxy/MIMOProxy.sol:132: for (uint256 i = 0; i < targets.length; i++) {
8. ++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:
proxy/MIMOProxy.sol:132: for (uint256 i = 0; i < targets.length; i++) {
Consider using pre-increments and pre-decrements where they are relevant (meaning: not where post-increments/decrements logic are relevant).
9. 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.
Consider wrapping with an unchecked block here (around 25 gas saved per instance):
proxy/MIMOProxy.sol:132: for (uint256 i = 0; i < targets.length; i++) {
The change would be:
- for (uint256 i; i < numIterations; i++) {
+ for (uint256 i; i < numIterations;) {
// ...
+ unchecked { ++i; }
}
The same can be applied with decrements (which should use break when i == 0).
The risk of overflow is non-existent for uint256 here.
10. It costs more gas to initialize variables with their default value than letting the default value be applied
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 (around 3 gas per instance).
Affected code:
proxy/MIMOProxy.sol:132: for (uint256 i = 0; i < targets.length; i++) {
Consider removing explicit initializations for default values.
11. Upgrade pragma
Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.
The advantages here are:
Contract existence checks (>= 0.8.10): external calls skip contract existence checks if the external call has a return value
The original warden who proved these type of findings is 0xKitsune. Clone the repo 0xKitsune/gas-lab, copy/paste the contract examples and run forge test --gas-report to replicate the gas reports with the optimizer turned on and set to 10000 runs.
12.1. Use assembly for math (add, sub, mul, div)
Use assembly for math instead of Solidity. You can check for overflow/underflow in assembly to ensure safety. If using Solidity versions < 0.8.0 and you are using Safemath, you can gain significant gas savings by using assembly to calculate values and checking for overflow/underflow.
POC Contract:
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
Contract2 c2;
Contract3 c3;
Contract4 c4;
Contract5 c5;
Contract6 c6;
Contract7 c7;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
c2 = new Contract2();
c3 = new Contract3();
c4 = new Contract4();
c5 = new Contract5();
c6 = new Contract6();
c7 = new Contract7();
}
function testGas() public {
c0.addTest(34598345, 100);
c1.addAssemblyTest(34598345, 100);
c2.subTest(34598345, 100);
c3.subAssemblyTest(34598345, 100);
c4.mulTest(34598345, 100);
c5.mulAssemblyTest(34598345, 100);
c6.divTest(34598345, 100);
c7.divAssemblyTest(34598345, 100);
}
}
contract Contract0 {
//addition in Solidity
function addTest(uint256 a, uint256 b) public pure {
uint256 c = a + b;
}
}
contract Contract1 {
//addition in assembly
function addAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := add(a, b)
if lt(c, a) {
mstore(0x00, "overflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract2 {
//subtraction in Solidity
function subTest(uint256 a, uint256 b) public pure {
uint256 c = a - b;
}
}
contract Contract3 {
//subtraction in assembly
function subAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := sub(a, b)
if gt(c, a) {
mstore(0x00, "underflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract4 {
//multiplication in Solidity
function mulTest(uint256 a, uint256 b) public pure {
uint256 c = a * b;
}
}
contract Contract5 {
//multiplication in assembly
function mulAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := mul(a, b)
if lt(c, a) {
mstore(0x00, "overflow")
revert(0x00, 0x20)
}
}
}
}
contract Contract6 {
//division in Solidity
function divTest(uint256 a, uint256 b) public pure {
uint256 c = a * b;
}
}
contract Contract7 {
//division in assembly
function divAssemblyTest(uint256 a, uint256 b) public pure {
assembly {
let c := div(a, b)
if gt(c, a) {
mstore(0x00, "underflow")
revert(0x00, 0x20)
}
}
}
}
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public view {
c0.ownerNotZero(address(this));
c1.assemblyOwnerNotZero(address(this));
}
}
contract Contract0 {
function ownerNotZero(address _addr) public pure {
require(_addr != address(0), "zero address)");
}
}
contract Contract1 {
function assemblyOwnerNotZero(address _addr) public pure {
assembly {
if iszero(_addr) {
mstore(0x00, "zero address")
revert(0x00, 0x20)
}
}
}
}
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.
POC Contract:
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testFailGas() public {
c0.stringErrorMessage();
c1.customErrorMessage();
}
}
contract Contract0 {
function stringErrorMessage() public {
bool check = false;
require(check, "error message");
}
}
contract Contract1 {
error CustomError();
function customErrorMessage() public {
bool check = false;
if (!check) {
revert CustomError();
}
}
}
Overview
Table of Contents:
memory
keyword whenstorage
should be usedimmutable
variable<array>.length
should not be looked up in every loop of afor-loop
++i
costs less gas compared toi++
ori += 1
(same for--i
vsi--
ori -= 1
)1. Caching storage values in memory
The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
2. Multiple accesses of a mapping/array should use a local variable cache
Caching a mapping's value in a local
storage
orcalldata
variable when the value is accessed multiple times saves ~42 gas per access due to not having to perform the same offset calculation every time.Affected code:
MIMOEmptyVault.sol#executeOperation()
:amounts[0]
MIMOLeverage.sol#executeOperation()
:amounts[0]
MIMORebalance.sol#executeOperation()
:amounts[0]
MIMOAutoRebalance.sol#executeOperation()
:amounts[0]
MIMOManagedRebalance.sol#executeOperation()
:amounts[0]
3. Use of the
memory
keyword whenstorage
should be usedWhen copying a state struct in memory, there are as many SLOADs and MSTOREs as there are slots. When reading the whole struct multiple times is not needed, it's better to actually only read the relevant field(s). When only some of the fields are read several times, these particular values should be cached instead of the whole state struct.
Consider using a
storage
pointer instead ofmemory
location here:4. Unnecessary memory operations with an
immutable
variableimmutable
variables aren't storage variable, their instances get replaced in the code with their value. This caching operation is unnecessary here:5. The result of a function call should be cached rather than re-calling the function
External calls are expensive. Consider using the already existing cached value for the following:
6. Unchecking arithmetics operations that can't underflow/overflow
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an
unchecked
block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmeticConsider wrapping with an
unchecked
block here (around 25 gas saved per instance):7.
<array>.length
should not be looked up in every loop of afor-loop
Note : This is describing an optimization that the sponsor already chose to ignore, to be thorough: https://github.com/code-423n4/2022-08-mimo/tree/main/docs/#for-loop-syntax
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, consider storing the array's length in a variable before the for-loop, and use this new variable instead:
8.
++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).
9. Increments/decrements can be unchecked in for-loops
Note : This is describing an optimization that the sponsor already chose to ignore, to be thorough: https://github.com/code-423n4/2022-08-mimo/tree/main/docs/#for-loop-syntax
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
Consider wrapping with an
unchecked
block here (around 25 gas saved per instance):The change would be:
The same can be applied with decrements (which should use
break
wheni == 0
).The risk of overflow is non-existent for
uint256
here.10. It costs more gas to initialize variables with their default value than letting the default value be applied
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 (around 3 gas per instance).Affected code:
Consider removing explicit initializations for default values.
11. Upgrade pragma
Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.
The advantages here are:
Consider upgrading here :
12. Optimizations with assembly
The original warden who proved these type of findings is 0xKitsune. Clone the repo 0xKitsune/gas-lab, copy/paste the contract examples and run
forge test --gas-report
to replicate the gas reports with the optimizer turned on and set to 10000 runs.12.1. Use assembly for math (add, sub, mul, div)
Use assembly for math instead of Solidity. You can check for overflow/underflow in assembly to ensure safety. If using Solidity versions < 0.8.0 and you are using Safemath, you can gain significant gas savings by using assembly to calculate values and checking for overflow/underflow.
POC Contract:
POC Gas Report:
Affected code:
12.2. Use assembly to check for address(0)
POC Gas Report:
Affected code:
12.3. Use assembly to write storage values
POC Gas Report:
Affected code:
13. Use Custom Errors instead of Revert Strings to save Gas
Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas
Additionally, custom errors can be used inside and outside of contracts (including interfaces and libraries).
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
POC Contract:
POC Gas Report:
Consider replacing all revert strings with custom errors in the solution.