Open code423n4 opened 2 years ago
Redundant variable initialization
The optimizer seems to take care of this scenario now - I tested all the changes together and gas went up & down, best savings ~22 gas.
Usage of unchecked can be added Prefix increment or decrement variables costs less gas Redundant return statements Cache array's length when iterating through it
There is some savings to be had here.
Increment loop index in the end of the loop body
I'm not sure this tactic is worth the impact to readability, unless combining with unchecked increments.
Use > 0 instead of != 0
I got some mixed results when testing these changes - ~/- ~25 gas. Very small impact and does not appear to be a consistent win.
The rest of the findings do appear to be valid considerations and each could provide small savings.
Gas Optimizations
Redundant variable initialization
Variables in solidity are initialized to their default value, and initializing them to the same value actually costs more gas. So for example, using
uint i;
instead ofuint i = 0;
can reduce the gas cost. This optimization also works in yul, wherelet x
is cheaper thanlet x := 0
.There are multiple places where this issue can be seen, mostly in loops:
Usage of unchecked can be added
Unchecked can be used in some places where we know for sure that the calculations won't overflow or underflow.
There are multiple places where this issue can be seen:
FulfillmentApplier
We know that if we entered the if section then
considerationItem.amount > execution.item.amount
so we can use unchecked when calculatingconsiderationItem.amount - execution.item.amount
, and if we entered the else section thenconsiderationItem.amount <= execution.item.amount
so we can use unchecked when calculatingexecution.item.amount - considerationItem.amount
.OrderCombiner
We know that
maximumFulfilled > 0
if we reached this line, somaximumFulfilled--;
won't underflow.OrderFulfiller
The calculations here uses the time limits of an order, which are checked before (
orderParameters.startTime <= block.timestamp < orderParameters.endTime
), so these calculations won't underflow.ReferenceOrderFulfiller (I know it's not deployed but still worth mentioning, it is also optimized using unchecked in the regular contract)
We know that
amount <= etherRemaining
, so the calculation ofetherRemaining -= amount
won't underflow.Prefix increment or decrement variables costs less gas
The operation
i++
costs more gas than++i
, and they are equivalent if the return value of them is not used after the operation. The same is correct fori--
and--i
. So whenever we can use++i
or--i
instead ofi++
ori--
, we should do so.We can see it in multiple places in the code:
Redundant return statements
There's no need to use a
return
statement when you declare the return values as variables.This can be seen in multiple places:
Cache array's length when iterating through it
When iterating through an array, we can cache the length of the array and use it to avoid accessing the
length
in every iteration.This can be done in multiple places:
Increment loop index in the end of the loop body
Incrementing the loop index in the end of the loop body instead of in the loop declaration costs less gas.
For example, the following code:
will cost more gas then this code:
This can be done in multiple places:
Use
> 0
instead of!= 0
Comparing
uint
s to zero using> 0
is more efficient than using!= 0
, so for example evaluating the expressionx > 0
will cost more gas than evaluating the expressionx != 0
.We can apply this in multiple places in the code:
Cache array elements
When accessing a struct field or array element or calculating an expression multiple times, it is better to cache it to avoid calculating the same thing multiple times (whether it is the address of the element or the expression itself).
This can be done in multiple places in the code:
orderHashes[i]
can be cached to avoid accessing it twice in every iteration.item.amount
can be cached instead of being accessed twice.filledNumerator + numerator
can be cached in order avoid calculating it twice._orderStatus[orderHash]
instead of accessing it 4 times.Save the current index instead of calculating it in every iteration
In line 493 and in line 519 of the
OrderCombiner
contract, the current index in the executions array is calculated. To avoid this calculation, we can save the current index instead of saving the number of filtered executions, and increment it whenever we use this index.In addition, this index variable can be used to set the actual length of the array in line 530, instead of calculating it. This will also save using an
mload
.This is how it currently looks:
And this is how it will look after applying this optimization:
Create two versions of the
_aggregateAvailable
functionCreate two versions of the
_aggregateAvailable
function, a function for the OFFER side and a function for the CONSIDERATION side. This will reduce the arguments number by one, and will save evaluating the if statement in the_aggregateAvailable
function (which checks which side is given).Loop optimization in
BasicOrderFulfiller
The
_transferERC20AndFinalize
function of theBasicOrderFulfiller
contract has a loop which checks in every iteration whether thefromOfferer
boolean variable which is given as an argument to the function is true or false. To prevent checking the same condition in every iteration, we can check this condition before the loop, and then execute the loop code with the right code for every case.The current code looks like this:
But after applying the optimization it will look like this:
Redundant assignment
In the
_applyFulfillment
function of theFulfillmentApplier
contract there is an assignment which occurs after an if statement, but is redundant for one case in this if, so it can be done only if we enter the second case of this if (theelse
case).As you can see here:
If we enter the if case, then
considerationItem.amount = execution.item.amount;
is executed and there's no point in executingexecution.item.amount = considerationItem.amount;
too. So it can be moved to the end of the else case like this:Avoid updating the array's length in every iteration
The update of the array's length is not necessary in every iteration of the loop, it can be done once in the end of the loop.
The accumulator can accumulate more than it does currently
In the current implementation, if one of the transfer functions receives zero conduit key, which doesn't require any conduit's action and any accumulation, it first triggers the accumulator and then transfers the tokens, and it is redundant because the accumulator can have potentially more to accumulate after this transfer which didn't use it at all.
The accumulator needs to be triggered only if a different non-zero conduit key is received, or the end of the transfers is reached.
We can see the call to
_triggerIfArmedAndNotAccumulatable
in the_transferERC20
function here even if the conduit key is zero. Instead, we can modify the code this way:Avoid calculating the same expression twice
Instead of calculating
i + 1
to update the array's length in the_validateOrdersAndPrepareToFulfill
function of theOrderCombiner
contract, we can modify the code to increment i before updating the length, and avoiding calculatingi + 1
twice (once for the array's length, and one for incrementing the loop index).The current code looks like this:
But after applying the optimization, it'll something like this: