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 #71

Open
code423n4 opened this issue Jun 2, 2022 · 1 comment
Open

Gas Optimizations #71

code423n4 opened this issue Jun 2, 2022 · 1 comment
Labels
bug Something isn't working G (Gas Optimization)

Comments

@code423n4
Copy link
Contributor

Table of Contents:

Cheap Contract Deployment Through Clones

See @audit tag:

contracts/conduit/ConduitController.sol:
   91:         new Conduit{ salt: conduitKey }(); //@audit gas: deployment can cost less through clones

There's a way to save a significant amount of gas on deployment using Clones: https://www.youtube.com/watch?v=3Mw-pMmJ7TA .

This is a solution that was adopted, as an example, by Porter Finance. They realized that deploying using clones was 10x cheaper:

I suggest applying a similar pattern, here with a cloneDeterministic method to mimic the current create2

ConduitController.sol#createConduit(): Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

To help the optimizer, declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array.

The effect can be quite significant.

As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.

Affected code (check the @audit tags):

File: ConduitController.sol
- 94:         _conduits[conduit].owner = initialOwner;  //@audit gas: similar to L130, should declare "ConduitProperties storage _conduitProperties = _conduits[conduit];" and use it to set owner
+ 93:         ConduitProperties storage _conduitProperties = _conduits[conduit];
+ 94:         _conduitProperties.owner = initialOwner;
95: 
96:         // Set conduit key used to deploy the conduit to enable reverse lookup.
- 97:         _conduits[conduit].key = conduitKey;//@audit gas: should use suggested storage variable _conduitProperties to set key
+ 97:        _conduitProperties.key = conduitKey;

Notice that this optimization already exists in the solution:

File: ConduitController.sol
129:         // Retrieve storage region where channels for the conduit are tracked.
130:         ConduitProperties storage conduitProperties = _conduits[conduit];

ConduitController.sol#acceptOwnership(): Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

This optimization is similar to the one explained here.

Instead of repeatedly fetching the storage region, consider declaring and using a storage variable here (see @audit tags):

File: ConduitController.sol
232:     function acceptOwnership(address conduit) external override {
...
- 237:         if (msg.sender != _conduits[conduit].potentialOwner) { //@audit gas: similar to L130, should declare "ConduitProperties storage _conduitProperties = _conduits[conduit];" and use it to get potentialOwner
+ 236:         ConduitProperties storage _conduitProperties = _conduits[conduit];
+ 237:         if (msg.sender != _conduitProperties.potentialOwner) {
...
- 246:         delete _conduits[conduit].potentialOwner;//@audit gas: should use suggested storage variable _conduitProperties to delete potentialOwner
+ 246:         delete _conduitProperties.potentialOwner;
...
249:         emit OwnershipTransferred(
250:             conduit,
- 251:             _conduits[conduit].owner, //@audit gas: should use suggested storage variable _conduitProperties to get owner
+ 251:             _conduitProperties.owner,
252:             msg.sender
253:         );
...
- 256:         _conduits[conduit].owner = msg.sender; //@audit gas: should use suggested storage variable _conduitProperties to set owner
+ 256:         _conduitProperties.owner = msg.sender;

OrderValidator.sol#_validateBasicOrderAndUpdateStatus(): Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

This optimization is similar to the one explained here.

Instead of repeatedly fetching the storage region, consider declaring and using a storage variable here (see @audit tags):

File: OrderValidator.sol
48:     function _validateBasicOrderAndUpdateStatus(
...
- 70:         _orderStatus[orderHash].isValidated = true; //@audit gas: should declare "OrderStatus storage _orderStatusStorage = _orderStatus[orderHash];" and use it to set isValidated
+ 69:         OrderStatus storage _orderStatusStorage = _orderStatus[orderHash];
+ 70:         _orderStatusStorage.isValidated = true;
...
- 72:         _orderStatus[orderHash].numerator = 1;//@audit gas: should use suggested storage variable _orderStatusStorage to set numerator
- 73:         _orderStatus[orderHash].denominator = 1;//@audit gas: should use suggested storage variable _orderStatusStorage to set denominator
+ 72:         _orderStatusStorage.numerator = 1;
+ 73:         _orderStatusStorage.denominator = 1;
74:     }

OrderValidator.sol#_validateBasicOrderAndUpdateStatus(): avoid an unnecessary SSTORE by not writing a default value

The following line is not needed, as it's writing to storage a default value:

File: OrderValidator.sol
71:         _orderStatus[orderHash].isCancelled = false;//@audit gas: SSTORE not needed, already false by default

Consider removing this line completely.

OrderCombiner.sol: ++totalFilteredExecutions costs less gas compared to totalFilteredExecutions += 1

For a uint256 i variable, the following is true with the Optimizer enabled at 10k:

  • 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)

Consider replacing totalFilteredExecutions += 1 with ++totalFilteredExecutions here:

lib/OrderCombiner.sol:490:                    totalFilteredExecutions += 1;
lib/OrderCombiner.sol:515:                    totalFilteredExecutions += 1;
lib/OrderCombiner.sol:768:                    totalFilteredExecutions += 1;

OrderCombiner.sol: --maximumFulfilled costs less gas compared to maximumFulfilled--

For a uint256 i variable, the following is true with the Optimizer enabled at 10k:

  • 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)

Consider replacing maximumFulfilled-- with --maximumFulfilled here:

lib/OrderCombiner.sol:229:                maximumFulfilled--;

FulfillmentApplier.sol#_applyFulfillment(): 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-arithmetic

I suggest wrapping with an unchecked block here (see @audit tags for more details):

File: FulfillmentApplier.sol
090:         if (considerationItem.amount > execution.item.amount) {
...
097:             advancedOrders[targetComponent.orderIndex]
098:                 .parameters
099:                 .consideration[targetComponent.itemIndex]
100:                 .startAmount = considerationItem.amount - execution.item.amount; //@audit gas: should be unchecked due to L90
...
104:         } else {
...
109:             advancedOrders[targetComponent.orderIndex]
110:                 .parameters
111:                 .offer[targetComponent.itemIndex]
112:                 .startAmount = execution.item.amount - considerationItem.amount; //@audit gas: should be unchecked due to L90
113:         }

/reference: Unchecking arithmetics operations that can't underflow/overflow

This is similar to the optimization above, except that here, contracts under /reference are just readable versions of the real contracts to be deployed.

The following lines should be unchecked, please check that this is the case in their corresponding assembly code:

  • reference/lib/ReferenceBasicOrderFulfiller.sol:
  842              if (additionalRecipientAmount > etherRemaining) {
  843                  revert InsufficientEtherSupplied();
  844              }
  ...
  853:             etherRemaining -= additionalRecipientAmount; //@audit gas: should be unchecked due to L842-L843
  865          if (etherRemaining > amount) {
  866              // Transfer remaining Ether to the caller.
  867:             _transferEth(payable(msg.sender), etherRemaining - amount); //@audit gas: should be unchecked due to L865
  868          }
  • reference/lib/ReferenceFulfillmentApplier.sol:
   99          // If total consideration amount exceeds the offer amount...
  100          if (considerationItem.amount > execution.item.amount) {
  ...
  107              ordersToExecute[targetComponent.orderIndex]
  108                  .receivedItems[targetComponent.itemIndex]
  109:                 .amount = considerationItem.amount - execution.item.amount; //@audit gas: should be unchecked due to L100
  ...
  113          } else {
  ...
  118              ordersToExecute[targetComponent.orderIndex]
  119                  .spentItems[targetComponent.itemIndex]
  120:                 .amount = execution.item.amount - considerationItem.amount; //@audit gas: should be unchecked due to L100
  121          }
  189          // If no available order was located...
  190          if (nextComponentIndex == 0) {
  191              // Return with an empty execution element that will be filtered.
  192              // prettier-ignore
  193              return Execution(
  194                  ReceivedItem(
  195                      ItemType.NATIVE,
  196                      address(0),
  197                      0,
  198                      0,
  199                      payable(address(0))
  200                  ),
  201                  address(0),
  202                  bytes32(0)
  203              );
  204          }
  205  
  206          // If the fulfillment components are offer components...
  207          if (side == Side.OFFER) {
  208              // Return execution for aggregated items provided by offerer.
  209              // prettier-ignore
  210              return _aggregateValidFulfillmentOfferItems(
  211                  ordersToExecute,
  212                  fulfillmentComponents,
  213:                 nextComponentIndex - 1 //@audit gas: should be unchecked due to L190
  214              );
  215          } else {
  216              // Otherwise, fulfillment components are consideration
  217              // components. Return execution for aggregated items provided by
  218              // the fulfiller.
  219              // prettier-ignore
  220              return _aggregateConsiderationItems(
  221                  ordersToExecute,
  222                  fulfillmentComponents,
  223:                 nextComponentIndex - 1, //@audit gas: should be unchecked due to L190
  224                  fulfillerConduitKey
  225              );
  226          }
  • reference/lib/ReferenceOrderCombiner.sol:
  629                  if (item.amount > etherRemaining) {
  630                      revert InsufficientEtherSupplied();
  631                  }
  632  
  633                  // Reduce ether remaining by amount.
  634:                 etherRemaining -= item.amount; //@audit gas: should be unchecked due to L629
  • reference/lib/ReferenceOrderFulfiller.sol:
  220                      if (amount > etherRemaining) {
  221                          revert InsufficientEtherSupplied();
  222                      }
  223                      // Reduce ether remaining by amount.
  224:                     etherRemaining -= amount; //@audit gas: should be unchecked due to L220
  273                      if (amount > etherRemaining) {
  274                          revert InsufficientEtherSupplied();
  275                      }
  276                      // Reduce ether remaining by amount.
  277:                     etherRemaining -= amount; //@audit gas: should be unchecked due to L273
  • reference/lib/ReferenceOrderValidator.sol:
  220              if (filledNumerator + numerator > denominator) {
  221                  // Reduce current numerator so it + supplied = denominator.
  222:                 numerator = denominator - filledNumerator; //@audit gas: should be unchecked due to L220
  223              }

OR conditions cost less than their equivalent AND conditions ("NOT(something is false)" costs less than "everything is true")

Remember that the equivalent of (a && b) is !(!a || !b)

Even with the 10k Optimizer enabled: OR conditions cost less than their equivalent AND conditions.

POC

  • Compare in Remix this example contract's 2 diffs (or any test-contract of your choice, as experimentation always show the same results):
pragma solidity 0.8.13;

contract Test {
    bool isOpen;
    bool channelPreviouslyOpen;
    
    function boolTest() external view returns (uint) {
-       if (isOpen && !channelPreviouslyOpen) { 
+       if (!(!isOpen || channelPreviouslyOpen)) { 
            return 1;
-       } else if (!isOpen && channelPreviouslyOpen) {
+       } else if (!(isOpen || !channelPreviouslyOpen)) {
            return 2;
        }
    }

    function setBools(bool _isOpen, bool _channelPreviouslyOpen) external {
        isOpen = _isOpen;
        channelPreviouslyOpen= _channelPreviouslyOpen;
    }
}
  • Notice that, even with the 10k Optimizer, the red diff version costs 8719 gas, while the green diff version costs 8707 gas, effectively saving 12 gas.

Affected code

Added together, it's possible to save a significant amount of gas by replacing the && conditions by their || equivalent in the solution.

  • ConduitController.sol#updateChannel()

Use !(!isOpen || channelPreviouslyOpen) instead of isOpen && !channelPreviouslyOpen and use !(isOpen || !channelPreviouslyOpen) instead of !isOpen && channelPreviouslyOpen:

File: ConduitController.sol
- 141:         if (isOpen && !channelPreviouslyOpen) {
+ 141:         if (!(!isOpen || channelPreviouslyOpen))
...
- 149:         } else if (!isOpen && channelPreviouslyOpen) {
+ 149:         } else if (!(isOpen || !channelPreviouslyOpen))
  • OrderValidator.sol#_validateOrderAndUpdateStatus()

Use !(!(numerator < denominator) || !_doesNotSupportPartialFills(orderParameters.orderType)) instead of numerator < denominator && _doesNotSupportPartialFills(orderParameters.orderType:

contracts/lib/OrderValidator.sol:
  142          if (
-  143:             numerator < denominator &&
-  144              _doesNotSupportPartialFills(orderParameters.orderType)
+  143:             !(!(numerator < denominator) ||
+  144              !_doesNotSupportPartialFills(orderParameters.orderType))
  145          ) {
  • OrderValidator.sol#_cancel()

Use !(msg.sender == offerer || msg.sender == zone) instead of msg.sender != offerer && msg.sender != zone here:

-  280:                 if (msg.sender != offerer && msg.sender != zone) {
+  280:                 if (!(msg.sender == offerer || msg.sender == zone))
  • SignatureVerification.sol#_assertValidSignature()

Use !(v == 27 || v == 28) instead of v != 27 && v != 28:

contracts/lib/SignatureVerification.sol:
-  78:             if (v != 27 && v != 28) {
+  78:             if (!(v == 27 || v == 28))
  • ZoneInteraction.sol#_assertRestrictedBasicOrderValidity()

Use !(!(uint256(orderType) > 1) || msg.sender == zone || msg.sender == offerer) instead of uint256(orderType) > 1 && msg.sender != zone && msg.sender != offerer:

contracts/lib/ZoneInteraction.sol:
   46          if (
-   47:             uint256(orderType) > 1 &&
-   48:             msg.sender != zone &&
-   49              msg.sender != offerer
+   47:             !(!(uint256(orderType) > 1) ||
+   48:             msg.sender == zone ||
+   49              msg.sender == offerer)
   50          ) {
  • ZoneInteraction.sol#_assertRestrictedAdvancedOrderValidity() (1)

Use !(!(uint256(orderType) > 1) || msg.sender == zone || msg.sender == offerer) instead of uint256(orderType) > 1 && msg.sender != zone && msg.sender != offerer:

  115          if (
-  116:             uint256(orderType) > 1 &&
-  117:             msg.sender != zone &&
-  118              msg.sender != offerer
+  116:             !(!(uint256(orderType) > 1) ||
+  117:             msg.sender == zone ||
+  118              msg.sender == offerer)
  119          ) {
  • ZoneInteraction.sol#_assertRestrictedAdvancedOrderValidity() (2)

Use !(advancedOrder.extraData.length != 0 || criteriaResolvers.length != 0) instead of advancedOrder.extraData.length == 0 && criteriaResolvers.length == 0:

  121              if (
-  122:                 advancedOrder.extraData.length == 0 &&
-  123                  criteriaResolvers.length == 0
+  122:                 !(advancedOrder.extraData.length != 0 ||
+  123                  criteriaResolvers.length != 0)
  124              ) {

Bytes constants are more efficient than string constants

From the Solidity doc:

If you can limit the length to a certain number of bytes, always use one of bytes1 to bytes32 because they are much cheaper.

Why do Solidity examples use bytes32 type instead of string?

bytes32 uses less gas because it fits in a single word of the EVM, and string is a dynamically sized-type which has current limitations in Solidity (such as can't be returned from a function to a contract).

If data can fit into 32 bytes, then you should use bytes32 datatype rather than bytes or strings as it is cheaper in solidity. Basically, any fixed size variable in solidity is cheaper than variable size. That will save gas on the contract.

Instances of string constant that can be replaced by bytes(1..32) constant :

reference/lib/ReferenceConsiderationBase.sol:
  29:     string internal constant _NAME = "Consideration";
  30:     string internal constant _VERSION = "rc.1";

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, lengths are only read from memory.

Consider storing the array's length in a variable before the for-loop, and use this newly created variable instead:

lib/OrderCombiner.sol:247:                for (uint256 j = 0; j < offer.length; ++j) {
lib/OrderCombiner.sol:291:                for (uint256 j = 0; j < consideration.length; ++j) {
lib/OrderCombiner.sol:598:                for (uint256 j = 0; j < consideration.length; ++j) {
lib/OrderCombiner.sol:621:        for (uint256 i = 0; i < executions.length; ) {
lib/OrderFulfiller.sol:217:            for (uint256 i = 0; i < orderParameters.offer.length; ) {
lib/OrderFulfiller.sol:306:            for (uint256 i = 0; i < orderParameters.consideration.length; ) {

Increments can be unchecked

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

Affected code:

lib/CriteriaResolution.sol:56:            for (uint256 i = 0; i < totalCriteriaResolvers; ++i) {
lib/CriteriaResolution.sol:166:            for (uint256 i = 0; i < totalAdvancedOrders; ++i) {
lib/CriteriaResolution.sol:184:                for (uint256 j = 0; j < totalItems; ++j) {
lib/CriteriaResolution.sol:199:                for (uint256 j = 0; j < totalItems; ++j) {
lib/OrderCombiner.sol:181:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderCombiner.sol:247:                for (uint256 j = 0; j < offer.length; ++j) {
lib/OrderCombiner.sol:291:                for (uint256 j = 0; j < consideration.length; ++j) {
lib/OrderCombiner.sol:373:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderCombiner.sol:473:            for (uint256 i = 0; i < totalOfferFulfillments; ++i) {
lib/OrderCombiner.sol:498:            for (uint256 i = 0; i < totalConsiderationFulfillments; ++i) {
lib/OrderCombiner.sol:577:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderCombiner.sol:598:                for (uint256 j = 0; j < consideration.length; ++j) {
lib/OrderCombiner.sol:754:            for (uint256 i = 0; i < totalFulfillments; ++i) {
lib/OrderFulfiller.sol:471:            for (uint256 i = 0; i < totalOrders; ++i) {

The code would go from:

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

to:

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

The risk of overflow is inexistant for uint256 here.

Note that this is already applied at some places in the solution. As an example:

contracts/conduit/Conduit.sol:
   66:         for (uint256 i = 0; i < totalStandardTransfers; ) {
   ...
   74:             unchecked {
   75                  ++i;
   76              }

  130:         for (uint256 i = 0; i < totalStandardTransfers; ) {
  ...
  138:             unchecked {
  139                  ++i;
  140              }

contracts/lib/BasicOrderFulfiller.sol:
   948:         for (uint256 i = 0; i < totalAdditionalRecipients; ) {
   ...
   975:             unchecked {
   976                  ++i;
   977              }

   
  1040:         for (uint256 i = 0; i < totalAdditionalRecipients; ) {
  ...
  1064:             unchecked {
  1065                  ++i;
  1066              }

No need to explicitly initialize variables with default values

This finding is only true without the Optimizer

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:

conduit/Conduit.sol:66:        for (uint256 i = 0; i < totalStandardTransfers; ) {
conduit/Conduit.sol:130:        for (uint256 i = 0; i < totalStandardTransfers; ) {
lib/AmountDeriver.sol:44:            uint256 extraCeiling = 0;
lib/BasicOrderFulfiller.sol:948:        for (uint256 i = 0; i < totalAdditionalRecipients; ) {
lib/BasicOrderFulfiller.sol:1040:        for (uint256 i = 0; i < totalAdditionalRecipients; ) {
lib/CriteriaResolution.sol:56:            for (uint256 i = 0; i < totalCriteriaResolvers; ++i) {
lib/CriteriaResolution.sol:166:            for (uint256 i = 0; i < totalAdvancedOrders; ++i) {
lib/CriteriaResolution.sol:184:                for (uint256 j = 0; j < totalItems; ++j) {
lib/CriteriaResolution.sol:199:                for (uint256 j = 0; j < totalItems; ++j) {
lib/OrderCombiner.sol:181:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderCombiner.sol:247:                for (uint256 j = 0; j < offer.length; ++j) {
lib/OrderCombiner.sol:291:                for (uint256 j = 0; j < consideration.length; ++j) {
lib/OrderCombiner.sol:373:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderCombiner.sol:470:            uint256 totalFilteredExecutions = 0;
lib/OrderCombiner.sol:473:            for (uint256 i = 0; i < totalOfferFulfillments; ++i) {
lib/OrderCombiner.sol:498:            for (uint256 i = 0; i < totalConsiderationFulfillments; ++i) {
lib/OrderCombiner.sol:577:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderCombiner.sol:598:                for (uint256 j = 0; j < consideration.length; ++j) {
lib/OrderCombiner.sol:621:        for (uint256 i = 0; i < executions.length; ) {
lib/OrderCombiner.sol:751:            uint256 totalFilteredExecutions = 0;
lib/OrderCombiner.sol:754:            for (uint256 i = 0; i < totalFulfillments; ++i) {
lib/OrderFulfiller.sol:217:            for (uint256 i = 0; i < orderParameters.offer.length; ) {
lib/OrderFulfiller.sol:306:            for (uint256 i = 0; i < orderParameters.consideration.length; ) {
lib/OrderFulfiller.sol:471:            for (uint256 i = 0; i < totalOrders; ++i) {
lib/OrderValidator.sol:272:            for (uint256 i = 0; i < totalOrders; ) {
lib/OrderValidator.sol:350:            for (uint256 i = 0; i < totalOrders; ) {

I suggest removing explicit initializations for default values.

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).

Consider using abi.encodePacked() here:

contracts/lib/ConsiderationBase.sol:
  77          return keccak256(
  78:             abi.encode(
  79                  _EIP_712_DOMAIN_TYPEHASH,
  80                  _NAME_HASH,
  81                  _VERSION_HASH,
  82                  block.chainid,
  83                  address(this)
  84              )
  85          );

Consider using the assembly equivalent for abi.encodePacked() here:

reference/lib/ReferenceBasicOrderFulfiller.sol:
  513          orderHash = keccak256(
  514:             abi.encode(
  515                  hashes.typeHash,
  516                  parameters.offerer,
  517                  parameters.zone,
  518                  hashes.offerItemsHash,
  519                  hashes.receivedItemsHash,
  520                  fulfillmentItemTypes.orderType,
  521                  parameters.startTime,
  522                  parameters.endTime,
  523                  parameters.zoneHash,
  524                  parameters.salt,
  525                  parameters.offererConduitKey,
  526                  nonce
  527              )
  528          );

  609              hashes.considerationHashes[0] = keccak256(
  610:                 abi.encode(
  611                      hashes.typeHash,
  612                      primaryConsiderationItem.itemType,
  613                      primaryConsiderationItem.token,
  614                      primaryConsiderationItem.identifierOrCriteria,
  615                      primaryConsiderationItem.startAmount,
  616                      primaryConsiderationItem.endAmount,
  617                      primaryConsiderationItem.recipient
  618                  )
  619              );

  684                  hashes.considerationHashes[recipientCount + 1] = keccak256(
  685:                     abi.encode(
  686                          hashes.typeHash,
  687                          additionalRecipientItem.itemType,
  688                          additionalRecipientItem.token,
  689                          additionalRecipientItem.identifierOrCriteria,
  690                          additionalRecipientItem.startAmount,
  691                          additionalRecipientItem.endAmount,
  692                          additionalRecipientItem.recipient
  693                      )
  694                  );
  695              }

  756                  keccak256(
  757:                     abi.encode(
  758                          hashes.typeHash,
  759                          offerItem.itemType,
  760                          offerItem.token,
  761                          offerItem.identifier,
  762                          offerItem.amount,
  763                          offerItem.amount //Assembly uses OfferItem instead of SpentItem.
  764                      )
  765                  )

reference/lib/ReferenceConsiderationBase.sol:
  117          return keccak256(
  118:             abi.encode(
  119                  _eip712DomainTypeHash,
  120                  _nameHash,
  121                  _versionHash,
  122                  block.chainid,
  123                  address(this)
  124              )
  125          );

reference/lib/ReferenceGettersAndDerivers.sol:
   41          return
   42              keccak256(
   43:                 abi.encode(
   44                      _OFFER_ITEM_TYPEHASH,
   45                      offerItem.itemType,
   46                      offerItem.token,
   47                      offerItem.identifierOrCriteria,
   48                      offerItem.startAmount,
   49                      offerItem.endAmount
   50                  )
   51              );
   52      }

   66          return
   67              keccak256(
   68:                 abi.encode(
   69                      _CONSIDERATION_ITEM_TYPEHASH,
   70                      considerationItem.itemType,
   71                      considerationItem.token,
   72                      considerationItem.identifierOrCriteria,
   73                      considerationItem.startAmount,
   74                      considerationItem.endAmount,
   75                      considerationItem.recipient
   76                  )
   77              );

  123          return
  124              keccak256(
  125:                 abi.encode(
  126                      _ORDER_TYPEHASH,
  127                      orderParameters.offerer,
  128                      orderParameters.zone,
  129                      keccak256(abi.encodePacked(offerHashes)),
  130                      keccak256(abi.encodePacked(considerationHashes)),
  131                      orderParameters.orderType,
  132                      orderParameters.startTime,
  133                      orderParameters.endTime,
  134                      orderParameters.zoneHash,
  135                      orderParameters.salt,
  136                      orderParameters.conduitKey,
  137                      nonce
  138                  )
  139              );

Notice that this is already used at other places for a similar situation:

reference/lib/ReferenceBasicOrderFulfiller.sol:
  769              hashes.offerItemsHash = keccak256(
  770                  abi.encodePacked(offerItemHashes)
  771              );

reference/lib/ReferenceGettersAndDerivers.sol:
  129                      keccak256(abi.encodePacked(offerHashes)),
  130                      keccak256(abi.encodePacked(considerationHashes)),
@code423n4 code423n4 added bug Something isn't working G (Gas Optimization) labels Jun 2, 2022
code423n4 added a commit that referenced this issue Jun 2, 2022
@HardlyDifficult
Copy link
Collaborator

HardlyDifficult commented Jun 26, 2022

Cheap Contract Deployment Through Clones

Deploying clones would save cost when Conduits are created, however it also increases the cost to use the conduits created. That increase has a tiny impact, but I assume it was intentional to favor the end-users here - creating conduits will be relatively rare and reserved for platforms and/or power users.

ConduitController.sol#createConduit(): Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

This general tactic often can often result in significant savings, but I ran the recommended change and here it only saves ~100 gas on createConduit.

ConduitController.sol#acceptOwnership(): Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

This will save some gas, but acceptOwnership is not a critical code path so an optimization here would not impact many transactions.

OrderValidator.sol#_validateBasicOrderAndUpdateStatus(): Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it

This is similar to the recommendation in #60 (although #60 appears to be a bit more thorough) and provides fairly significant savings on critical code paths.

OrderValidator.sol#_validateBasicOrderAndUpdateStatus(): avoid an unnecessary SSTORE by not writing a default value

This is a safe change to make since _verifyOrderStatus will first revert if the order is already canceled. Considered independently from the rec above, this saves ~300 gas on fulfillBasicOrder.

OrderCombiner.sol: ++totalFilteredExecutions costs less gas compared to totalFilteredExecutions += 1
OrderCombiner.sol: --maximumFulfilled costs less gas compared to maximumFulfilled--
FulfillmentApplier.sol#_applyFulfillment(): Unchecking arithmetics operations that can't underflow/overflow
/reference: Unchecking arithmetics operations that can't underflow/overflow
An array's length should be cached to save gas in for-loops
Increments can be unchecked

Yes these should provide some savings.

OR conditions cost less than their equivalent AND conditions ("NOT(something is false)" costs less than "everything is true")

It's unfortunate that the optimizer cannot handle scenarios like this automatically... There does appear to be a small win here, but it's debatable whether the impact to readability is worth it here.

Bytes constants are more efficient than string constants

These changes seem to be focused on getters, it's not clear it would impact gas for any transactions.

No need to explicitly initialize variables with default values

This appears to be handled by the optimizer automatically now. Testing did not change the gas results.

abi.encode() is less efficient than abi.encodePacked()

This recommendation causes tests to fail, suggesting this change violates the EIP-712 standard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working G (Gas Optimization)
Projects
None yet
Development

No branches or pull requests

2 participants