Platform: Code4rena
Start Date: 20/05/2022
Pot Size: $1,000,000 USDC
Total HM: 4
Participants: 59
Period: 14 days
Judge: leastwood
Id: 128
League: ETH
Rank: 14/59
Findings: 1
Award: $4,250.36
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Dravee
Also found by: 0x1f8b, 0x29A, 0xalpharush, Chom, Czar102, Hawkeye, IllIllI, MaratCerby, MiloTruck, NoamYakov, OriDabush, RoiEvenHaim, Spearbit, Tadashi, TerrierLover, TomJ, asutorufos, cccz, cmichel, csanuragjain, defsec, delfin454000, djxploit, ellahi, foobar, gzeon, hake, hickuphh3, ignacio, ilan, joestakey, kaden, mayo, ming, oyc_109, peritoflores, rfa, sach1r0, sashik_eth, shung, sirhashalot, twojoy, zer0dot, zkhorse
4250.3586 USDC - $4,250.36
Table of Contents:
ConduitController.sol#createConduit()
: Help the optimizer by saving a storage variable's reference instead of repeatedly fetching itConduitController.sol#acceptOwnership()
: Help the optimizer by saving a storage variable's reference instead of repeatedly fetching itOrderValidator.sol#_validateBasicOrderAndUpdateStatus()
: Help the optimizer by saving a storage variable's reference instead of repeatedly fetching itOrderValidator.sol#_validateBasicOrderAndUpdateStatus()
: avoid an unnecessary SSTORE by not writing a default valueOrderCombiner.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/overflowOR
conditions cost less than their equivalent AND
conditions ("NOT(something is false)" costs less than "everything is true")abi.encode()
is less efficient than abi.encodePacked()
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 itTo 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 itThis 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 itThis 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 valueThe 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 formi++
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 formi--
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/overflowSolidity 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/overflowThis 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:
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 }
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 }
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
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
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
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; } }
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 ) {
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";
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; ) {
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.
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 }
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)),
#0 - HardlyDifficult
2022-06-26T14:09:53Z
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 https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/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.