From f93d2da0e1b83dc664357178033a16a580ed947f Mon Sep 17 00:00:00 2001 From: d1ll0n Date: Fri, 16 Feb 2024 14:56:20 -0800 Subject: [PATCH] Move offer/consideration item checks for contract orders into decoder logic to reduce total loop iterations --- src/core/lib/ConsiderationDecoder.sol | 227 ++++++++++++++++++++++++-- src/core/lib/OrderValidator.sol | 167 +------------------ 2 files changed, 216 insertions(+), 178 deletions(-) diff --git a/src/core/lib/ConsiderationDecoder.sol b/src/core/lib/ConsiderationDecoder.sol index 0679c3c..172859d 100644 --- a/src/core/lib/ConsiderationDecoder.sol +++ b/src/core/lib/ConsiderationDecoder.sol @@ -23,6 +23,8 @@ import { AdvancedOrderPlusOrderParameters_head_size, Common_amount_offset, Common_endAmount_offset, + Common_identifier_offset, + Common_token_offset, ConsiderationItem_recipient_offset, ConsiderationItem_size_with_length, ConsiderationItem_size, @@ -866,7 +868,7 @@ contract ConsiderationDecoder { * @return offer The decoded offer array. * @return consideration The decoded consideration array. */ - function _decodeGenerateOrderReturndata() + function _decodeGenerateOrderReturndata(MemoryPointer originalOffer, MemoryPointer originalConsideration) internal pure returns ( @@ -943,23 +945,34 @@ contract ConsiderationDecoder { } if iszero(invalidEncoding) { - offer := + let invalidSpentItems, invalidReceivedItems + offer, invalidSpentItems := copySpentItemsAsOfferItems( - add(offsetOffer, OneWord), offerLength + originalOffer, + add(offsetOffer, OneWord), + offerLength ) - consideration := + consideration, invalidReceivedItems := copyReceivedItemsAsConsiderationItems( - add(offsetConsideration, OneWord), considerationLength + originalConsideration, + add(offsetConsideration, OneWord), + considerationLength ) + invalidEncoding := or(invalidSpentItems, invalidReceivedItems) } - function copySpentItemsAsOfferItems(rdPtrHead, length) -> mPtrLength + function copySpentItemsAsOfferItems( + mPtrLengthOriginal, rdPtrHeadSpentItems, length + ) -> mPtrLength, invalidSpentItems { // Retrieve the current free memory pointer. mPtrLength := mload(FreeMemoryPointerSlot) - // Allocate memory for the array. + // Cache the original offer array length + let originalOfferLength := mload(mPtrLengthOriginal) + + // Allocate memory for the new array. mstore( FreeMemoryPointerSlot, add( @@ -975,14 +988,58 @@ contract ConsiderationDecoder { let headOffsetFromLength := OneWord let headSizeWithLength := shl(OneWordShift, add(1, length)) let mPtrTailNext := add(mPtrLength, headSizeWithLength) + let mPtrTailOriginalNext := add( + mPtrLengthOriginal, + shl(OneWordShift, add(1, originalOfferLength)) + ) + + let headSizeToCompareWithLength := shl(OneWordShift, add(1, min(length, originalOfferLength))) + + // Iterate over each new element with a corresponding original item + // For each original offer item, check that: + // - There is a corresponding new spent item. + // - The original and new items match with compareItems. + // - The new offer's amount is greater than or equal to the original amount. + invalidSpentItems := gt(originalOfferLength, length) + for { } lt(headOffsetFromLength, headSizeToCompareWithLength) { } { + // Write the memory pointer to the accompanying head offset. + mstore(add(mPtrLength, headOffsetFromLength), mPtrTailNext) + + // Copy itemType, token, identifier and amount. + returndatacopy(mPtrTailNext, rdPtrHeadSpentItems, SpentItem_size) - // Iterate over each element. + let newAmount := mload(add(mPtrTailNext, Common_amount_offset)) + + // Copy amount to endAmount. + mstore( + add(mPtrTailNext, Common_endAmount_offset), + newAmount + ) + + let originalAmount := mload(add(mPtrTailOriginalNext, Common_amount_offset)) + invalidSpentItems := or( + invalidSpentItems, + or( + compareItems(mPtrTailOriginalNext, mPtrTailNext), + gt(originalAmount, newAmount) + ) + ) + + // Update read pointer, next tail pointer for new and + // original, and head offset. + rdPtrHeadSpentItems := add(rdPtrHeadSpentItems, SpentItem_size) + mPtrTailNext := add(mPtrTailNext, OfferItem_size) + mPtrTailOriginalNext := add(mPtrTailOriginalNext, OfferItem_size) + headOffsetFromLength := add(headOffsetFromLength, OneWord) + } + + // Iterate over each element without a corresponding original item for { } lt(headOffsetFromLength, headSizeWithLength) { } { // Write the memory pointer to the accompanying head offset. mstore(add(mPtrLength, headOffsetFromLength), mPtrTailNext) // Copy itemType, token, identifier and amount. - returndatacopy(mPtrTailNext, rdPtrHead, SpentItem_size) + returndatacopy(mPtrTailNext, rdPtrHeadSpentItems, SpentItem_size) // Copy amount to endAmount. mstore( @@ -991,17 +1048,20 @@ contract ConsiderationDecoder { ) // Update read pointer, next tail pointer, and head offset. - rdPtrHead := add(rdPtrHead, SpentItem_size) + rdPtrHeadSpentItems := add(rdPtrHeadSpentItems, SpentItem_size) mPtrTailNext := add(mPtrTailNext, OfferItem_size) headOffsetFromLength := add(headOffsetFromLength, OneWord) } } - function copyReceivedItemsAsConsiderationItems(rdPtrHead, length) -> - mPtrLength + function copyReceivedItemsAsConsiderationItems( + mPtrLengthOriginal, rdPtrHeadReceivedItems, length + ) -> mPtrLength, invalidReceivedItems { // Retrieve the current free memory pointer. mPtrLength := mload(FreeMemoryPointerSlot) + // Cache the original consideration array length + let originalConsiderationLength := mload(mPtrLengthOriginal) // Allocate memory for the array. mstore( @@ -1022,8 +1082,68 @@ contract ConsiderationDecoder { let headOffsetFromLength := OneWord let headSizeWithLength := shl(OneWordShift, add(1, length)) let mPtrTailNext := add(mPtrLength, headSizeWithLength) + let mPtrTailOriginalNext := add( + mPtrLengthOriginal, + shl(OneWordShift, add(1, originalConsiderationLength)) + ) + + let headSizeToCompareWithLength := shl( + OneWordShift, + add(1, min(length, originalConsiderationLength)) + ) + + // Iterate over each new element with a corresponding original item. + // For each new received item, check that: + // - There is a corresponding original consideration item. + // - The items match with compareItems. + // - The new amount is less than or equal to the original amount. + // - The items have the same recipient if the original's was not null. + invalidReceivedItems := gt(length, originalConsiderationLength) + for { } lt(headOffsetFromLength, headSizeToCompareWithLength) { } { + // Write the memory pointer to the accompanying head offset. + mstore(add(mPtrLength, headOffsetFromLength), mPtrTailNext) + + // Copy itemType, token, identifier, amount and recipient. + returndatacopy( + mPtrTailNext, + rdPtrHeadReceivedItems, + ReceivedItem_size + ) - // Iterate over each element. + // Copy amount to consideration item's recipient offset. + returndatacopy( + add(mPtrTailNext, ConsiderationItem_recipient_offset), + add(rdPtrHeadReceivedItems, Common_amount_offset), + OneWord + ) + + let newAmount := mload(add(mPtrTailNext, Common_amount_offset)) + let originalAmount := mload(add(mPtrTailOriginalNext, Common_amount_offset)) + + // Compare items' item type, token, and identifier, ensure they have the same + // recipient and that the new amount is less than or equal to the original amount. + invalidReceivedItems := or( + invalidReceivedItems, + or( + compareItems(mPtrTailOriginalNext, mPtrTailNext), + or( + gt(newAmount, originalAmount), + checkRecipients( + mload(add(mPtrTailOriginalNext, ReceivedItem_recipient_offset)), + mload(add(mPtrTailNext, ReceivedItem_recipient_offset)) + ) + ) + ) + ) + + // Update read pointer, next tail pointer, and head offset. + rdPtrHeadReceivedItems := add(rdPtrHeadReceivedItems, ReceivedItem_size) + mPtrTailNext := add(mPtrTailNext, ConsiderationItem_size) + mPtrTailOriginalNext := add(mPtrTailOriginalNext, ConsiderationItem_size) + headOffsetFromLength := add(headOffsetFromLength, OneWord) + } + + // Iterate over each new element without a corresponding original item for { } lt(headOffsetFromLength, headSizeWithLength) { } { // Write the memory pointer to the accompanying head offset. mstore(add(mPtrLength, headOffsetFromLength), mPtrTailNext) @@ -1031,23 +1151,94 @@ contract ConsiderationDecoder { // Copy itemType, token, identifier, amount and recipient. returndatacopy( mPtrTailNext, - rdPtrHead, + rdPtrHeadReceivedItems, ReceivedItem_size ) // Copy amount to consideration item's recipient offset. returndatacopy( add(mPtrTailNext, ConsiderationItem_recipient_offset), - add(rdPtrHead, Common_amount_offset), + add(rdPtrHeadReceivedItems, Common_amount_offset), OneWord ) // Update read pointer, next tail pointer, and head offset. - rdPtrHead := add(rdPtrHead, ReceivedItem_size) + rdPtrHeadReceivedItems := add(rdPtrHeadReceivedItems, ReceivedItem_size) mPtrTailNext := add(mPtrTailNext, ConsiderationItem_size) headOffsetFromLength := add(headOffsetFromLength, OneWord) } } + + /** + * @dev Yul function to check the compatibility of two offer or + * consideration items for contract orders. Note that the itemType + * and identifier are reset in cases where criteria = 0 (collection- + * wide offers), which means that a contract offerer has full latitude + * to choose any identifier it wants mid-flight, in contrast to the + * normal behavior, where the fulfiller can pick which identifier to + * receive by providing a CriteriaResolver. + * + * @param originalItem The original offer or consideration item. + * @param newItem The new offer or consideration item. + * + * @return isInvalid Error buffer indicating if items are incompatible. + */ + function compareItems(originalItem, newItem) -> isInvalid { + let itemType := mload(originalItem) + let identifier := mload(add(originalItem, Common_identifier_offset)) + + // Set returned identifier for criteria-based items w/ criteria = 0 + if and(gt(itemType, 3), iszero(identifier)) { + // replace item type + itemType := sub(3, eq(itemType, 4)) + identifier := mload(add(newItem, Common_identifier_offset)) + } + + isInvalid := + iszero( + and( + // originalItem.token == newItem.token && + // originalItem.itemType == newItem.itemType + and( + eq( + mload(add(originalItem, Common_token_offset)), + mload(add(newItem, Common_token_offset)) + ), + eq(itemType, mload(newItem)) + ), + // originalItem.identifier == newItem.identifier + eq( + identifier, + mload(add(newItem, Common_identifier_offset)) + ) + + ) + ) + } + + /** + * @dev Internal pure function to check the compatibility of two recipients + * on consideration items for contract orders. This check is skipped if + * no recipient is originally supplied. + * + * @param originalRecipient The original consideration item recipient. + * @param newRecipient The new consideration item recipient. + * + * @return isInvalid Error buffer indicating if recipients are incompatible. + */ + function checkRecipients(originalRecipient, newRecipient) -> isInvalid { + isInvalid := + iszero( + or( + iszero(originalRecipient), + eq(newRecipient, originalRecipient) + ) + ) + } + + function min(a, b) -> c { + c := add(b, mul(lt(a, b), sub(a, b))) + } } } @@ -1062,7 +1253,7 @@ contract ConsiderationDecoder { * error buffer, offer array, and consideration array. */ function _convertGetGeneratedOrderResult( - function() + function(MemoryPointer, MemoryPointer) internal pure returns (uint256, MemoryPointer, MemoryPointer) inFn @@ -1070,7 +1261,7 @@ contract ConsiderationDecoder { internal pure returns ( - function() internal pure returns (uint256, OfferItem[] memory, ConsiderationItem[] memory) + function(OfferItem[] memory, ConsiderationItem[] memory) internal pure returns (uint256, OfferItem[] memory, ConsiderationItem[] memory) outFn ) { diff --git a/src/core/lib/OrderValidator.sol b/src/core/lib/OrderValidator.sol index 0d360df..c99d15e 100644 --- a/src/core/lib/OrderValidator.sol +++ b/src/core/lib/OrderValidator.sol @@ -553,85 +553,6 @@ contract OrderValidator is Executor, ZoneInteraction { return true; } - /** - * @dev Internal pure function to check the compatibility of two offer - * or consideration items for contract orders. Note that the itemType - * and identifier are reset in cases where criteria = 0 (collection- - * wide offers), which means that a contract offerer has full latitude - * to choose any identifier it wants mid-flight, in contrast to the - * normal behavior, where the fulfiller can pick which identifier to - * receive by providing a CriteriaResolver. - * - * @param originalItem The original offer or consideration item. - * @param newItem The new offer or consideration item. - * - * @return isInvalid Error buffer indicating if items are incompatible. - */ - function _compareItems(MemoryPointer originalItem, MemoryPointer newItem) - internal - pure - returns (uint256 isInvalid) - { - assembly { - let itemType := mload(originalItem) - let identifier := mload(add(originalItem, Common_identifier_offset)) - - // Set returned identifier for criteria-based items w/ criteria = 0 - if and(gt(itemType, 3), iszero(identifier)) { - // replace item type - itemType := sub(3, eq(itemType, 4)) - identifier := mload(add(newItem, Common_identifier_offset)) - } - - isInvalid := - iszero( - and( - // originalItem.token == newItem.token && - // originalItem.itemType == newItem.itemType - and( - eq( - mload(add(originalItem, Common_token_offset)), - mload(add(newItem, Common_token_offset)) - ), - eq(itemType, mload(newItem)) - ), - // originalItem.identifier == newItem.identifier - eq( - identifier, - mload(add(newItem, Common_identifier_offset)) - ) - - ) - ) - } - } - - /** - * @dev Internal pure function to check the compatibility of two recipients - * on consideration items for contract orders. This check is skipped if - * no recipient is originally supplied. - * - * @param originalRecipient The original consideration item recipient. - * @param newRecipient The new consideration item recipient. - * - * @return isInvalid Error buffer indicating if recipients are incompatible. - */ - function _checkRecipients(address originalRecipient, address newRecipient) - internal - pure - returns (uint256 isInvalid) - { - assembly { - isInvalid := - iszero( - or( - iszero(originalRecipient), - eq(newRecipient, originalRecipient) - ) - ) - } - } - /** * @dev Internal function to generate a contract order. When a * collection-wide criteria-based item (criteria = 0) is provided as an @@ -717,94 +638,20 @@ contract OrderValidator is Executor, ZoneInteraction { uint256 errorBuffer, OfferItem[] memory offer, ConsiderationItem[] memory consideration - ) = _convertGetGeneratedOrderResult(_decodeGenerateOrderReturndata)(); + ) = _convertGetGeneratedOrderResult(_decodeGenerateOrderReturndata)( + orderParameters.offer, orderParameters.consideration + ); // Revert if the returndata could not be decoded correctly. if (errorBuffer != 0) { _revertInvalidContractOrder(orderHash); } - { - // Designate lengths. - uint256 originalOfferLength = orderParameters.offer.length; - uint256 newOfferLength = offer.length; - - // Explicitly specified offer items cannot be removed. - if (originalOfferLength > newOfferLength) { - _revertInvalidContractOrder(orderHash); - } - - // Iterate over each specified offer (e.g. minimumReceived) item. - for (uint256 i = 0; i < originalOfferLength;) { - // Retrieve the pointer to the originally supplied item. - MemoryPointer mPtrOriginal = - orderParameters.offer[i].toMemoryPointer(); + // Assign the returned offer item in place of the original item. + orderParameters.offer = offer; - // Retrieve the pointer to the newly returned item. - MemoryPointer mPtrNew = offer[i].toMemoryPointer(); - - // Compare the items and update the error buffer accordingly. - errorBuffer |= _cast( - mPtrOriginal.offset(Common_amount_offset).readUint256() - > mPtrNew.offset(Common_amount_offset).readUint256() - ) | _compareItems(mPtrOriginal, mPtrNew); - - // Increment the array (cannot overflow as index starts at 0). - unchecked { - ++i; - } - } - - // Assign the returned offer item in place of the original item. - orderParameters.offer = offer; - } - - { - // Designate lengths & memory locations. - ConsiderationItem[] memory originalConsiderationArray = - (orderParameters.consideration); - uint256 newConsiderationLength = consideration.length; - - // New consideration items cannot be created. - if (newConsiderationLength > originalConsiderationArray.length) { - _revertInvalidContractOrder(orderHash); - } - - // Iterate over returned consideration & do not exceed maximumSpent. - for (uint256 i = 0; i < newConsiderationLength;) { - // Retrieve the pointer to the originally supplied item. - MemoryPointer mPtrOriginal = - originalConsiderationArray[i].toMemoryPointer(); - - // Retrieve the pointer to the newly returned item. - MemoryPointer mPtrNew = consideration[i].toMemoryPointer(); - - // Compare the items and update the error buffer accordingly - // and ensure that the recipients are equal when provided. - errorBuffer |= _cast( - mPtrNew.offset(Common_amount_offset).readUint256() - > mPtrOriginal.offset(Common_amount_offset).readUint256() - ) | _compareItems(mPtrOriginal, mPtrNew) - | _checkRecipients( - mPtrOriginal.offset(ReceivedItem_recipient_offset) - .readAddress(), - mPtrNew.offset(ReceivedItem_recipient_offset).readAddress() - ); - - // Increment the array (cannot overflow as index starts at 0). - unchecked { - ++i; - } - } - - // Assign returned consideration item in place of the original item. - orderParameters.consideration = consideration; - } - - // Revert if any item comparison failed. - if (errorBuffer != 0) { - _revertInvalidContractOrder(orderHash); - } + // Assign returned consideration item in place of the original item. + orderParameters.consideration = consideration; // Return the order hash. return orderHash;