OpenSea Seaport contest - shung's results

A marketplace contract for safely and efficiently creating and fulfilling orders for ERC721 and ERC1155 items.

General Information

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

OpenSea

Findings Distribution

Researcher Performance

Rank: 12/59

Findings: 2

Award: $5,202.73

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

2652.5087 USDC - $2,652.51

Labels

bug
QA (Quality Assurance)

External Links

QA Report

An order can be cancelled more than once

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.

Typos

#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

Gas Report

Inefficient struct utilization

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[].

Reference diff

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.

Gas savings table

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.

FunctionOld CostNew Cost
cancel44124–6129643831–61003
fulfillAdvancedOrder104014–209645103219–208885
fulfillAvailableAdvancedOrders173305–229465172553–227921
fulfillAvailableOrders172879–229282172088–227698
fulfillBasicOrder93563–162426792188–1622918
fulfillOrder102728–213402101946–212633
matchAdvancedOrders206604–272820205006–271277
matchOrders166719–366925165177–365347
validate58025–6944057765–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).

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter