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: 12/59
Findings: 2
Award: $5,202.73
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Spearbit
Also found by: 0xsanson, Chom, IllIllI, OriDabush, Saw-mon_and_Natalie, broccoli, cccz, cmichel, csanuragjain, foobar, hack3r-0m, hickuphh3, hubble, hyh, ilan, kebabsec, mayo, oyc_109, peritoflores, rfa, scaraven, sces60107, shung, sorrynotsorry, tintin, twojoy, zkhorse, zzzitron
2652.5087 USDC - $2,652.51
OrderValidator._cancel
does not check if an order is already cancelled. This can cause
OrderCancelled
event to emit more than once for the same order. Consider either throwing
when _cancel
is called on an already cancelled order, or wrapping the event emission with
an if
statement.
OrderCombiner.sol:L365
: revertOnValid → revertOnInvalidConsiderationStructs.sol:L68
: an → and#0 - HardlyDifficult
2022-06-20T18:46:28Z
#1 - HardlyDifficult
2022-06-26T17:40:53Z
An order can be cancelled more than once
It does not seem harmful to allow orders to be canceled multiple times. However it may be worth considering to avoid confusion. It's pretty common for users to fire the same transaction multiple times, not realizing that there is a delay before the first transaction is mined.
Typos
It is nice to fix typos..
ERC721_WITH_CRITERIA items with an endAmount greater than 1 are problematic
See comments in https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/155
🌟 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
2550.2151 USDC - $2,550.22
Functions in OrderValidator
retreive _orderStatus[]
using a memory location.
This necessiates inefficient mapping lookup operations when writing to the storage.
For example, the following block performs the same offset calculation four times.
lib/OrderValidator.sol:70: _orderStatus[orderHash].isValidated = true; lib/OrderValidator.sol:71: _orderStatus[orderHash].isCancelled = false; lib/OrderValidator.sol:72: _orderStatus[orderHash].numerator = 1; lib/OrderValidator.sol:73: _orderStatus[orderHash].denominator = 1;
I suggest using a storage pointer instead of memory location when retreiving _orderStatus[]
.
Below is a diff file that applies the suggested change. This is for reference only.
diff --git a/contracts/lib/OrderValidator.sol b/contracts/lib/OrderValidator.sol index 09f10cb..7160ce0 100644 --- a/contracts/lib/OrderValidator.sol +++ b/contracts/lib/OrderValidator.sol @@ -51,7 +51,7 @@ contract OrderValidator is Executor, ZoneInteraction { bytes memory signature ) internal { // Retrieve the order status for the given order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and is not cancelled. _verifyOrderStatus( @@ -67,10 +67,10 @@ contract OrderValidator is Executor, ZoneInteraction { } // Update order status as fully filled, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = 1; - _orderStatus[orderHash].denominator = 1; + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = 1; + orderStatus.denominator = 1; } /** @@ -165,7 +165,7 @@ contract OrderValidator is Executor, ZoneInteraction { ); // Retrieve the order status using the derived order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and is not cancelled. if ( @@ -223,19 +223,19 @@ contract OrderValidator is Executor, ZoneInteraction { // Skip overflow check: checked above unless numerator is reduced. unchecked { // Update order status and fill amount, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = uint120( + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = uint120( filledNumerator + numerator ); - _orderStatus[orderHash].denominator = uint120(denominator); + orderStatus.denominator = uint120(denominator); } } else { // Update order status and fill amount, packing struct values. - _orderStatus[orderHash].isValidated = true; - _orderStatus[orderHash].isCancelled = false; - _orderStatus[orderHash].numerator = uint120(numerator); - _orderStatus[orderHash].denominator = uint120(denominator); + orderStatus.isValidated = true; + orderStatus.isCancelled = false; + orderStatus.numerator = uint120(numerator); + orderStatus.denominator = uint120(denominator); } // Return order hash, a modified numerator, and a modified denominator. @@ -300,8 +300,9 @@ contract OrderValidator is Executor, ZoneInteraction { ); // Update the order status as not valid and cancelled. - _orderStatus[orderHash].isValidated = false; - _orderStatus[orderHash].isCancelled = true; + OrderStatus storage orderStatus = _orderStatus[orderHash]; + orderStatus.isValidated = false; + orderStatus.isCancelled = true; // Emit an event signifying that the order has been cancelled. emit OrderCancelled(orderHash, offerer, zone); @@ -363,7 +364,7 @@ contract OrderValidator is Executor, ZoneInteraction { ); // Retrieve the order status using the derived order hash. - OrderStatus memory orderStatus = _orderStatus[orderHash]; + OrderStatus storage orderStatus = _orderStatus[orderHash]; // Ensure order is fillable and retrieve the filled amount. _verifyOrderStatus( @@ -379,7 +380,7 @@ contract OrderValidator is Executor, ZoneInteraction { _verifySignature(offerer, orderHash, order.signature); // Update order status to mark the order as valid. - _orderStatus[orderHash].isValidated = true; + orderStatus.isValidated = true; // Emit an event signifying the order has been validated. emit OrderValidated( diff --git a/contracts/lib/Verifiers.sol b/contracts/lib/Verifiers.sol index b0cf467..483bf08 100644 --- a/contracts/lib/Verifiers.sol +++ b/contracts/lib/Verifiers.sol @@ -85,7 +85,7 @@ contract Verifiers is Assertions, SignatureVerification { } /** - * @dev Internal pure function to validate that a given order is fillable + * @dev Internal view function to validate that a given order is fillable * and not cancelled based on the order status. * * @param orderHash The order hash. @@ -101,10 +101,10 @@ contract Verifiers is Assertions, SignatureVerification { */ function _verifyOrderStatus( bytes32 orderHash, - OrderStatus memory orderStatus, + OrderStatus storage orderStatus, bool onlyAllowUnused, bool revertOnInvalid - ) internal pure returns (bool valid) { + ) internal view returns (bool valid) { // Ensure that the order has not been cancelled. if (orderStatus.isCancelled) { // Only revert if revertOnInvalid has been supplied as true.
I have ran the provided yarn profile
script to compare the gas costs before and after applying the reference diff.
All external mutative Seaport
functions showed considerable gas savings, except incrementNonce()
, which has nothing to do with order validation.
Below is a table comparing minimum and maximum gas costs of all external mutative Seaport
functions, except incrementNonce()
, before and after applying the diff.
Function | Old Cost | New Cost |
---|---|---|
cancel | 44124–61296 | 43831–61003 |
fulfillAdvancedOrder | 104014–209645 | 103219–208885 |
fulfillAvailableAdvancedOrders | 173305–229465 | 172553–227921 |
fulfillAvailableOrders | 172879–229282 | 172088–227698 |
fulfillBasicOrder | 93563–1624267 | 92188–1622918 |
fulfillOrder | 102728–213402 | 101946–212633 |
matchAdvancedOrders | 206604–272820 | 205006–271277 |
matchOrders | 166719–366925 | 165177–365347 |
validate | 58025–69440 | 57765–69232 |
#0 - HardlyDifficult
2022-06-24T18:45:31Z
This is a straightforward recommendation that has a non-trivial impact on the core functions. It's also well explained. I ran the recommended change and confirmed decent gas savings across the board. This seems to be a worthwhile change to consider including.
#1 - IllIllI000
2022-07-01T21:07:15Z
This gas report is about a single issue and is ranked 85. This same exact issue, as well as many more instances the same issue are flagged in https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/144 as the third item "Multiple accesses of a mapping/array should use a local variable cache" which has a proof of concept showing the gas savings in a concise example, along with many other gas savings but is only ranked 60. @HardlyDifficult @0xleastwood can you provide more clarity on how the ranking has been performed?
#2 - HardlyDifficult
2022-07-01T21:30:03Z
This gas report is about a single issue and is ranked 85. This same exact issue, as well as many more instances the same issue are flagged in #144 as the third item "Multiple accesses of a mapping/array should use a local variable cache" which has a proof of concept showing the gas savings in a concise example, along with many other gas savings but is only ranked 60. @HardlyDifficult @0xleastwood can you provide more clarity on how the ranking has been performed?
They are similar, but this report is more useful. In 144 the report is focused on the access costs to save 42 gas per instance and dumps every instance where that may apply. This report zoomed into a specific change that results in a sizeable gas savings in total for critical functions in a way that is easy to confirm.
This report scores higher for focusing on just the most impactful changes, providing an easy to follow diff of the recommended changes, and highlighting the expected total benefit. The approach used here makes it easy to conclude the recommendation provided is worth close consideration. The sponsor's time should be respected so we scored reports which read like static analysis reports lower (and if they offered no targeted insights they were closed as invalid).